Convert polygerrit to es6-modules This change replace all HTML imports with es6-modules. The only exceptions are: * gr-app.html file, which can be deleted only after updating the gerrit/httpd/raw/PolyGerritIndexHtml.soy file. * dark-theme.html which is loaded via importHref. Must be updated manually later in a separate change. This change was produced automatically by ./es6-modules-converter.sh script. No manual changes were made. Change-Id: I0c447dd8c05757741e2c940720652d01d9fb7d67
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js index a421043..cfb28bb 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.js
@@ -14,291 +14,308 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-access-behavior/gr-access-behavior.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-permission/gr-permission.js'; +import '../../../scripts/util.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {htmlTemplate} from './gr-access-section_html.js'; + +/** + * Fired when the section has been modified or removed. + * + * @event access-modified + */ + +/** + * Fired when a section that was previously added was removed. + * + * @event added-section-removed + */ + +const GLOBAL_NAME = 'GLOBAL_CAPABILITIES'; + +// The name that gets automatically input when a new reference is added. +const NEW_NAME = 'refs/heads/*'; +const REFS_NAME = 'refs/'; +const ON_BEHALF_OF = '(On Behalf Of)'; +const LABEL = 'Label'; + +/** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrAccessSection extends mixinBehaviors( [ + Gerrit.AccessBehavior, /** - * Fired when the section has been modified or removed. - * - * @event access-modified + * Unused in this element, but called by other elements in tests + * e.g gr-repo-access_test. */ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** - * Fired when a section that was previously added was removed. - * - * @event added-section-removed - */ + static get is() { return 'gr-access-section'; } - const GLOBAL_NAME = 'GLOBAL_CAPABILITIES'; + static get properties() { + return { + capabilities: Object, + /** @type {?} */ + section: { + type: Object, + notify: true, + observer: '_updateSection', + }, + groups: Object, + labels: Object, + editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + canUpload: Boolean, + ownerOf: Array, + _originalId: String, + _editingRef: { + type: Boolean, + value: false, + }, + _deleted: { + type: Boolean, + value: false, + }, + _permissions: Array, + }; + } - // The name that gets automatically input when a new reference is added. - const NEW_NAME = 'refs/heads/*'; - const REFS_NAME = 'refs/'; - const ON_BEHALF_OF = '(On Behalf Of)'; - const LABEL = 'Label'; + /** @override */ + created() { + super.created(); + this.addEventListener('access-saved', + () => this._handleAccessSaved()); + } - /** - * @appliesMixin Gerrit.AccessMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrAccessSection extends Polymer.mixinBehaviors( [ - Gerrit.AccessBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-repo-access_test. - */ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-access-section'; } + _updateSection(section) { + this._permissions = this.toSortedArray(section.value.permissions); + this._originalId = section.id; + } - static get properties() { - return { - capabilities: Object, - /** @type {?} */ - section: { - type: Object, - notify: true, - observer: '_updateSection', - }, - groups: Object, - labels: Object, - editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - canUpload: Boolean, - ownerOf: Array, - _originalId: String, - _editingRef: { - type: Boolean, - value: false, - }, - _deleted: { - type: Boolean, - value: false, - }, - _permissions: Array, - }; + _handleAccessSaved() { + // Set a new 'original' value to keep track of after the value has been + // saved. + this._updateSection(this.section); + } + + _handleValueChange() { + if (!this.section.value.added) { + this.section.value.modified = this.section.id !== this._originalId; + // Allows overall access page to know a change has been made. + // For a new section, this is not fired because new permissions and + // rules have to be added in order to save, modifying the ref is not + // enough. + this.dispatchEvent(new CustomEvent( + 'access-modified', {bubbles: true, composed: true})); } + this.section.value.updatedId = this.section.id; + } - /** @override */ - created() { - super.created(); - this.addEventListener('access-saved', - () => this._handleAccessSaved()); - } - - _updateSection(section) { - this._permissions = this.toSortedArray(section.value.permissions); - this._originalId = section.id; - } - - _handleAccessSaved() { - // Set a new 'original' value to keep track of after the value has been - // saved. - this._updateSection(this.section); - } - - _handleValueChange() { - if (!this.section.value.added) { - this.section.value.modified = this.section.id !== this._originalId; - // Allows overall access page to know a change has been made. - // For a new section, this is not fired because new permissions and - // rules have to be added in order to save, modifying the ref is not - // enough. - this.dispatchEvent(new CustomEvent( - 'access-modified', {bubbles: true, composed: true})); - } - this.section.value.updatedId = this.section.id; - } - - _handleEditingChanged(editing, editingOld) { - // Ignore when editing gets set initially. - if (!editingOld) { return; } - // Restore original values if no longer editing. - if (!editing) { - this._editingRef = false; - this._deleted = false; - delete this.section.value.deleted; - // Restore section ref. - this.set(['section', 'id'], this._originalId); - // Remove any unsaved but added permissions. - this._permissions = this._permissions.filter(p => !p.value.added); - for (const key of Object.keys(this.section.value.permissions)) { - if (this.section.value.permissions[key].added) { - delete this.section.value.permissions[key]; - } - } - } - } - - _computePermissions(name, capabilities, labels) { - let allPermissions; - if (!this.section || !this.section.value) { - return []; - } - if (name === GLOBAL_NAME) { - allPermissions = this.toSortedArray(capabilities); - } else { - const labelOptions = this._computeLabelOptions(labels); - allPermissions = labelOptions.concat( - this.toSortedArray(this.permissionValues)); - } - return allPermissions - .filter(permission => !this.section.value.permissions[permission.id]); - } - - _computeHideEditClass(section) { - return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : ''; - } - - _handleAddedPermissionRemoved(e) { - const index = e.model.index; - this._permissions = this._permissions.slice(0, index).concat( - this._permissions.slice(index + 1, this._permissions.length)); - } - - _computeLabelOptions(labels) { - const labelOptions = []; - if (!labels) { return []; } - for (const labelName of Object.keys(labels)) { - labelOptions.push({ - id: 'label-' + labelName, - value: { - name: `${LABEL} ${labelName}`, - id: 'label-' + labelName, - }, - }); - labelOptions.push({ - id: 'labelAs-' + labelName, - value: { - name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`, - id: 'labelAs-' + labelName, - }, - }); - } - return labelOptions; - } - - _computePermissionName(name, permission, permissionValues, capabilities) { - if (name === GLOBAL_NAME) { - return capabilities[permission.id].name; - } else if (permissionValues[permission.id]) { - return permissionValues[permission.id].name; - } else if (permission.value.label) { - let behalfOf = ''; - if (permission.id.startsWith('labelAs-')) { - behalfOf = ON_BEHALF_OF; - } - return `${LABEL} ${permission.value.label}${behalfOf}`; - } - } - - _computeSectionName(name) { - // When a new section is created, it doesn't yet have a ref. Set into - // edit mode so that the user can input one. - if (!name) { - this._editingRef = true; - // Needed for the title value. This is the same default as GWT. - name = NEW_NAME; - // Needed for the input field value. - this.set('section.id', name); - } - if (name === GLOBAL_NAME) { - return 'Global Capabilities'; - } else if (name.startsWith(REFS_NAME)) { - return `Reference: ${name}`; - } - return name; - } - - _handleRemoveReference() { - if (this.section.value.added) { - this.dispatchEvent(new CustomEvent( - 'added-section-removed', {bubbles: true, composed: true})); - } - this._deleted = true; - this.section.value.deleted = true; - this.dispatchEvent( - new CustomEvent('access-modified', {bubbles: true, composed: true})); - } - - _handleUndoRemove() { + _handleEditingChanged(editing, editingOld) { + // Ignore when editing gets set initially. + if (!editingOld) { return; } + // Restore original values if no longer editing. + if (!editing) { + this._editingRef = false; this._deleted = false; delete this.section.value.deleted; - } - - editRefInput() { - return Polymer.dom(this.root).querySelector(Polymer.Element ? - 'iron-input.editRefInput' : - 'input[is=iron-input].editRefInput'); - } - - editReference() { - this._editingRef = true; - this.editRefInput().focus(); - } - - _isEditEnabled(canUpload, ownerOf, sectionId) { - return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0); - } - - _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) { - const classList = []; - if (editing - && this._isEditEnabled(canUpload, ownerOf, this.section.id)) { - classList.push('editing'); + // Restore section ref. + this.set(['section', 'id'], this._originalId); + // Remove any unsaved but added permissions. + this._permissions = this._permissions.filter(p => !p.value.added); + for (const key of Object.keys(this.section.value.permissions)) { + if (this.section.value.permissions[key].added) { + delete this.section.value.permissions[key]; + } } - if (editingRef) { - classList.push('editingRef'); - } - if (deleted) { - classList.push('deleted'); - } - return classList.join(' '); - } - - _computeEditBtnClass(name) { - return name === GLOBAL_NAME ? 'global' : ''; - } - - _handleAddPermission() { - const value = this.$.permissionSelect.value; - const permission = { - id: value, - value: {rules: {}, added: true}, - }; - - // This is needed to update the 'label' property of the - // 'label-<label-name>' permission. - // - // The value from the add permission dropdown will either be - // label-<label-name> or labelAs-<labelName>. - // But, the format of the API response is as such: - // "permissions": { - // "label-Code-Review": { - // "label": "Code-Review", - // "rules": {...} - // } - // } - // } - // When we add a new item, we have to push the new permission in the same - // format as the ones that have been returned by the API. - if (value.startsWith('label')) { - permission.value.label = - value.replace('label-', '').replace('labelAs-', ''); - } - // Add to the end of the array (used in dom-repeat) and also to the - // section object that is two way bound with its parent element. - this.push('_permissions', permission); - this.set(['section.value.permissions', permission.id], - permission.value); } } - customElements.define(GrAccessSection.is, GrAccessSection); -})(); + _computePermissions(name, capabilities, labels) { + let allPermissions; + if (!this.section || !this.section.value) { + return []; + } + if (name === GLOBAL_NAME) { + allPermissions = this.toSortedArray(capabilities); + } else { + const labelOptions = this._computeLabelOptions(labels); + allPermissions = labelOptions.concat( + this.toSortedArray(this.permissionValues)); + } + return allPermissions + .filter(permission => !this.section.value.permissions[permission.id]); + } + + _computeHideEditClass(section) { + return section.id === 'GLOBAL_CAPABILITIES' ? 'hide' : ''; + } + + _handleAddedPermissionRemoved(e) { + const index = e.model.index; + this._permissions = this._permissions.slice(0, index).concat( + this._permissions.slice(index + 1, this._permissions.length)); + } + + _computeLabelOptions(labels) { + const labelOptions = []; + if (!labels) { return []; } + for (const labelName of Object.keys(labels)) { + labelOptions.push({ + id: 'label-' + labelName, + value: { + name: `${LABEL} ${labelName}`, + id: 'label-' + labelName, + }, + }); + labelOptions.push({ + id: 'labelAs-' + labelName, + value: { + name: `${LABEL} ${labelName} ${ON_BEHALF_OF}`, + id: 'labelAs-' + labelName, + }, + }); + } + return labelOptions; + } + + _computePermissionName(name, permission, permissionValues, capabilities) { + if (name === GLOBAL_NAME) { + return capabilities[permission.id].name; + } else if (permissionValues[permission.id]) { + return permissionValues[permission.id].name; + } else if (permission.value.label) { + let behalfOf = ''; + if (permission.id.startsWith('labelAs-')) { + behalfOf = ON_BEHALF_OF; + } + return `${LABEL} ${permission.value.label}${behalfOf}`; + } + } + + _computeSectionName(name) { + // When a new section is created, it doesn't yet have a ref. Set into + // edit mode so that the user can input one. + if (!name) { + this._editingRef = true; + // Needed for the title value. This is the same default as GWT. + name = NEW_NAME; + // Needed for the input field value. + this.set('section.id', name); + } + if (name === GLOBAL_NAME) { + return 'Global Capabilities'; + } else if (name.startsWith(REFS_NAME)) { + return `Reference: ${name}`; + } + return name; + } + + _handleRemoveReference() { + if (this.section.value.added) { + this.dispatchEvent(new CustomEvent( + 'added-section-removed', {bubbles: true, composed: true})); + } + this._deleted = true; + this.section.value.deleted = true; + this.dispatchEvent( + new CustomEvent('access-modified', {bubbles: true, composed: true})); + } + + _handleUndoRemove() { + this._deleted = false; + delete this.section.value.deleted; + } + + editRefInput() { + return dom(this.root).querySelector(PolymerElement ? + 'iron-input.editRefInput' : + 'input[is=iron-input].editRefInput'); + } + + editReference() { + this._editingRef = true; + this.editRefInput().focus(); + } + + _isEditEnabled(canUpload, ownerOf, sectionId) { + return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0); + } + + _computeSectionClass(editing, canUpload, ownerOf, editingRef, deleted) { + const classList = []; + if (editing + && this._isEditEnabled(canUpload, ownerOf, this.section.id)) { + classList.push('editing'); + } + if (editingRef) { + classList.push('editingRef'); + } + if (deleted) { + classList.push('deleted'); + } + return classList.join(' '); + } + + _computeEditBtnClass(name) { + return name === GLOBAL_NAME ? 'global' : ''; + } + + _handleAddPermission() { + const value = this.$.permissionSelect.value; + const permission = { + id: value, + value: {rules: {}, added: true}, + }; + + // This is needed to update the 'label' property of the + // 'label-<label-name>' permission. + // + // The value from the add permission dropdown will either be + // label-<label-name> or labelAs-<labelName>. + // But, the format of the API response is as such: + // "permissions": { + // "label-Code-Review": { + // "label": "Code-Review", + // "rules": {...} + // } + // } + // } + // When we add a new item, we have to push the new permission in the same + // format as the ones that have been returned by the API. + if (value.startsWith('label')) { + permission.value.label = + value.replace('label-', '').replace('labelAs-', ''); + } + // Add to the end of the array (used in dom-repeat) and also to the + // section object that is two way bound with its parent element. + this.push('_permissions', permission); + this.set(['section.value.permissions', permission.id], + permission.value); + } +} + +customElements.define(GrAccessSection.is, GrAccessSection);
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js index a52cb1a..5f35f55 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.js
@@ -1,36 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-permission/gr-permission.html"> - -<script src="../../../scripts/util.js"></script> - -<dom-module id="gr-access-section"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -89,50 +75,23 @@ <style include="gr-form-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <fieldset id="section" - class$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]"> + <fieldset id="section" class\$="gr-form-styles [[_computeSectionClass(editing, canUpload, ownerOf, _editingRef, _deleted)]]"> <div id="mainContainer"> <div class="header"> <div class="name"> <h3>[[_computeSectionName(section.id)]]</h3> - <gr-button - id="editBtn" - link - class$="[[_computeEditBtnClass(section.id)]]" - on-click="editReference"> + <gr-button id="editBtn" link="" class\$="[[_computeEditBtnClass(section.id)]]" on-click="editReference"> <iron-icon id="icon" icon="gr-icons:create"></iron-icon> </gr-button> </div> - <iron-input - class="editRefInput" - bind-value="{{section.id}}" - type="text" - on-input="_handleValueChange"> - <input - class="editRefInput" - bind-value="{{section.id}}" - is="iron-input" - type="text" - on-input="_handleValueChange"> + <iron-input class="editRefInput" bind-value="{{section.id}}" type="text" on-input="_handleValueChange"> + <input class="editRefInput" bind-value="{{section.id}}" is="iron-input" type="text" on-input="_handleValueChange"> </iron-input> - <gr-button - link - id="deleteBtn" - on-click="_handleRemoveReference">Remove</gr-button> + <gr-button link="" id="deleteBtn" on-click="_handleRemoveReference">Remove</gr-button> </div><!-- end header --> <div class="sectionContent"> - <template - is="dom-repeat" - items="{{_permissions}}" - as="permission"> - <gr-permission - name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]" - permission="{{permission}}" - labels="[[labels]]" - section="[[section.id]]" - editing="[[editing]]" - groups="[[groups]]" - on-added-permission-removed="_handleAddedPermissionRemoved"> + <template is="dom-repeat" items="{{_permissions}}" as="permission"> + <gr-permission name="[[_computePermissionName(section.id, permission, permissionValues, capabilities)]]" permission="{{permission}}" labels="[[labels]]" section="[[section.id]]" editing="[[editing]]" groups="[[groups]]" on-added-permission-removed="_handleAddedPermissionRemoved"> </gr-permission> </template> <div id="addPermission"> @@ -140,29 +99,19 @@ <select id="permissionSelect"> <!-- called with a third parameter so that permissions update after a new section is added. --> - <template - is="dom-repeat" - items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]"> + <template is="dom-repeat" items="[[_computePermissions(section.id, capabilities, labels, section.value.permissions.*)]]"> <option value="[[item.value.id]]">[[item.value.name]]</option> </template> </select> - <gr-button - link - id="addBtn" - on-click="_handleAddPermission">Add</gr-button> + <gr-button link="" id="addBtn" on-click="_handleAddPermission">Add</gr-button> </div> <!-- end addPermission --> </div><!-- end sectionContent --> </div><!-- end mainContainer --> <div id="deletedContainer"> <span>[[_computeSectionName(section.id)]] was deleted</span> - <gr-button - link - id="undoRemoveBtn" - on-click="_handleUndoRemove">Undo</gr-button> + <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button> </div><!-- end deletedContainer --> </fieldset> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-access-section.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html index 2a3044e..4754c4a 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-access-section</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-access-section.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-access-section.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-access-section.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,28 +41,310 @@ </template> </test-fixture> -<script> - suite('gr-access-section tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-access-section.js'; +suite('gr-access-section tests', () => { + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('unit tests', () => { setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + element.section = { + id: 'refs/*', + value: { + permissions: { + read: { + rules: {}, + }, + }, + }, + }; + element.capabilities = { + accessDatabase: { + id: 'accessDatabase', + name: 'Access Database', + }, + administrateServer: { + id: 'administrateServer', + name: 'Administrate Server', + }, + batchChangesLimit: { + id: 'batchChangesLimit', + name: 'Batch Changes Limit', + }, + createAccount: { + id: 'createAccount', + name: 'Create Account', + }, + }; + element.labels = { + 'Code-Review': { + values: { + ' 0': 'No score', + '-1': 'I would prefer this is not merged as is', + '-2': 'This shall not be merged', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + default_value: 0, + }, + }; + element._updateSection(element.section); + flushAsynchronousOperations(); }); - teardown(() => { - sandbox.restore(); + test('_updateSection', () => { + // _updateSection was called in setup, so just make assertions. + const expectedPermissions = [ + { + id: 'read', + value: { + rules: {}, + }, + }, + ]; + assert.deepEqual(element._permissions, expectedPermissions); + assert.equal(element._originalId, element.section.id); }); - suite('unit tests', () => { + test('_computeLabelOptions', () => { + const expectedLabelOptions = [ + { + id: 'label-Code-Review', + value: { + name: 'Label Code-Review', + id: 'label-Code-Review', + }, + }, + { + id: 'labelAs-Code-Review', + value: { + name: 'Label Code-Review (On Behalf Of)', + id: 'labelAs-Code-Review', + }, + }, + ]; + + assert.deepEqual(element._computeLabelOptions(element.labels), + expectedLabelOptions); + }); + + test('_handleAccessSaved', () => { + assert.equal(element._originalId, 'refs/*'); + element.section.id = 'refs/for/bar'; + element._handleAccessSaved(); + assert.equal(element._originalId, 'refs/for/bar'); + }); + + test('_computePermissions', () => { + sandbox.stub(element, 'toSortedArray').returns( + [{ + id: 'push', + value: { + rules: {}, + }, + }, + { + id: 'read', + value: { + rules: {}, + }, + }, + ]); + + const expectedPermissions = [{ + id: 'push', + value: { + rules: {}, + }, + }, + ]; + const labelOptions = [ + { + id: 'label-Code-Review', + value: { + name: 'Label Code-Review', + id: 'label-Code-Review', + }, + }, + { + id: 'labelAs-Code-Review', + value: { + name: 'Label Code-Review (On Behalf Of)', + id: 'labelAs-Code-Review', + }, + }, + ]; + + // For global capabilities, just return the sorted array filtered by + // existing permissions. + let name = 'GLOBAL_CAPABILITIES'; + assert.deepEqual(element._computePermissions(name, element.capabilities, + element.labels), expectedPermissions); + + // Uses the capabilities array to come up with possible values. + assert.isTrue(element.toSortedArray.lastCall. + calledWithExactly(element.capabilities)); + + // For everything else, include possible label values before filtering. + name = 'refs/for/*'; + assert.deepEqual(element._computePermissions(name, element.capabilities, + element.labels), labelOptions.concat(expectedPermissions)); + + // Uses permissionValues (defined in gr-access-behavior) to come up with + // possible values. + assert.isTrue(element.toSortedArray.lastCall. + calledWithExactly(element.permissionValues)); + }); + + test('_computePermissionName', () => { + let name = 'GLOBAL_CAPABILITIES'; + let permission = { + id: 'administrateServer', + value: {}, + }; + assert.equal(element._computePermissionName(name, permission, + element.permissionValues, element.capabilities), + element.capabilities[permission.id].name); + + name = 'refs/for/*'; + permission = { + id: 'abandon', + value: {}, + }; + + assert.equal(element._computePermissionName( + name, permission, element.permissionValues, element.capabilities), + element.permissionValues[permission.id].name); + + name = 'refs/for/*'; + permission = { + id: 'label-Code-Review', + value: { + label: 'Code-Review', + }, + }; + + assert.equal(element._computePermissionName(name, permission, + element.permissionValues, element.capabilities), + 'Label Code-Review'); + + permission = { + id: 'labelAs-Code-Review', + value: { + label: 'Code-Review', + }, + }; + + assert.equal(element._computePermissionName(name, permission, + element.permissionValues, element.capabilities), + 'Label Code-Review(On Behalf Of)'); + }); + + test('_computeSectionName', () => { + let name; + // When computing the section name for an undefined name, it means a + // new section is being added. In this case, it should defualt to + // 'refs/heads/*'. + element._editingRef = false; + assert.equal(element._computeSectionName(name), + 'Reference: refs/heads/*'); + assert.isTrue(element._editingRef); + assert.equal(element.section.id, 'refs/heads/*'); + + // Reset editing to false. + element._editingRef = false; + name = 'GLOBAL_CAPABILITIES'; + assert.equal(element._computeSectionName(name), 'Global Capabilities'); + assert.isFalse(element._editingRef); + + name = 'refs/for/*'; + assert.equal(element._computeSectionName(name), + 'Reference: refs/for/*'); + assert.isFalse(element._editingRef); + }); + + test('editReference', () => { + element.editReference(); + assert.isTrue(element._editingRef); + }); + + test('_computeSectionClass', () => { + let editingRef = false; + let canUpload = false; + let ownerOf = []; + let editing = false; + let deleted = false; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), ''); + + editing = true; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), ''); + + ownerOf = ['refs/*']; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), 'editing'); + + ownerOf = []; + canUpload = true; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), 'editing'); + + editingRef = true; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), 'editing editingRef'); + + deleted = true; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), 'editing editingRef deleted'); + + editingRef = false; + assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, + editingRef, deleted), 'editing deleted'); + }); + + test('_computeEditBtnClass', () => { + let name = 'GLOBAL_CAPABILITIES'; + assert.equal(element._computeEditBtnClass(name), 'global'); + name = 'refs/for/*'; + assert.equal(element._computeEditBtnClass(name), ''); + }); + }); + + suite('interactive tests', () => { + setup(() => { + element.labels = { + 'Code-Review': { + values: { + ' 0': 'No score', + '-1': 'I would prefer this is not merged as is', + '-2': 'This shall not be merged', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + default_value: 0, + }, + }; + }); + suite('Global section', () => { setup(() => { element.section = { - id: 'refs/*', + id: 'GLOBAL_CAPABILITIES', value: { permissions: { - read: { + accessDatabase: { rules: {}, }, }, @@ -81,476 +368,196 @@ name: 'Create Account', }, }; - element.labels = { - 'Code-Review': { - values: { - ' 0': 'No score', - '-1': 'I would prefer this is not merged as is', - '-2': 'This shall not be merged', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', - }, - default_value: 0, - }, - }; element._updateSection(element.section); flushAsynchronousOperations(); }); - test('_updateSection', () => { - // _updateSection was called in setup, so just make assertions. - const expectedPermissions = [ - { - id: 'read', - value: { - rules: {}, - }, - }, - ]; - assert.deepEqual(element._permissions, expectedPermissions); - assert.equal(element._originalId, element.section.id); - }); - - test('_computeLabelOptions', () => { - const expectedLabelOptions = [ - { - id: 'label-Code-Review', - value: { - name: 'Label Code-Review', - id: 'label-Code-Review', - }, - }, - { - id: 'labelAs-Code-Review', - value: { - name: 'Label Code-Review (On Behalf Of)', - id: 'labelAs-Code-Review', - }, - }, - ]; - - assert.deepEqual(element._computeLabelOptions(element.labels), - expectedLabelOptions); - }); - - test('_handleAccessSaved', () => { - assert.equal(element._originalId, 'refs/*'); - element.section.id = 'refs/for/bar'; - element._handleAccessSaved(); - assert.equal(element._originalId, 'refs/for/bar'); - }); - - test('_computePermissions', () => { - sandbox.stub(element, 'toSortedArray').returns( - [{ - id: 'push', - value: { - rules: {}, - }, - }, - { - id: 'read', - value: { - rules: {}, - }, - }, - ]); - - const expectedPermissions = [{ - id: 'push', - value: { - rules: {}, - }, - }, - ]; - const labelOptions = [ - { - id: 'label-Code-Review', - value: { - name: 'Label Code-Review', - id: 'label-Code-Review', - }, - }, - { - id: 'labelAs-Code-Review', - value: { - name: 'Label Code-Review (On Behalf Of)', - id: 'labelAs-Code-Review', - }, - }, - ]; - - // For global capabilities, just return the sorted array filtered by - // existing permissions. - let name = 'GLOBAL_CAPABILITIES'; - assert.deepEqual(element._computePermissions(name, element.capabilities, - element.labels), expectedPermissions); - - // Uses the capabilities array to come up with possible values. - assert.isTrue(element.toSortedArray.lastCall. - calledWithExactly(element.capabilities)); - - // For everything else, include possible label values before filtering. - name = 'refs/for/*'; - assert.deepEqual(element._computePermissions(name, element.capabilities, - element.labels), labelOptions.concat(expectedPermissions)); - - // Uses permissionValues (defined in gr-access-behavior) to come up with - // possible values. - assert.isTrue(element.toSortedArray.lastCall. - calledWithExactly(element.permissionValues)); - }); - - test('_computePermissionName', () => { - let name = 'GLOBAL_CAPABILITIES'; - let permission = { - id: 'administrateServer', - value: {}, - }; - assert.equal(element._computePermissionName(name, permission, - element.permissionValues, element.capabilities), - element.capabilities[permission.id].name); - - name = 'refs/for/*'; - permission = { - id: 'abandon', - value: {}, - }; - - assert.equal(element._computePermissionName( - name, permission, element.permissionValues, element.capabilities), - element.permissionValues[permission.id].name); - - name = 'refs/for/*'; - permission = { - id: 'label-Code-Review', - value: { - label: 'Code-Review', - }, - }; - - assert.equal(element._computePermissionName(name, permission, - element.permissionValues, element.capabilities), - 'Label Code-Review'); - - permission = { - id: 'labelAs-Code-Review', - value: { - label: 'Code-Review', - }, - }; - - assert.equal(element._computePermissionName(name, permission, - element.permissionValues, element.capabilities), - 'Label Code-Review(On Behalf Of)'); - }); - - test('_computeSectionName', () => { - let name; - // When computing the section name for an undefined name, it means a - // new section is being added. In this case, it should defualt to - // 'refs/heads/*'. - element._editingRef = false; - assert.equal(element._computeSectionName(name), - 'Reference: refs/heads/*'); - assert.isTrue(element._editingRef); - assert.equal(element.section.id, 'refs/heads/*'); - - // Reset editing to false. - element._editingRef = false; - name = 'GLOBAL_CAPABILITIES'; - assert.equal(element._computeSectionName(name), 'Global Capabilities'); - assert.isFalse(element._editingRef); - - name = 'refs/for/*'; - assert.equal(element._computeSectionName(name), - 'Reference: refs/for/*'); - assert.isFalse(element._editingRef); - }); - - test('editReference', () => { - element.editReference(); - assert.isTrue(element._editingRef); - }); - - test('_computeSectionClass', () => { - let editingRef = false; - let canUpload = false; - let ownerOf = []; - let editing = false; - let deleted = false; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), ''); - - editing = true; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), ''); - - ownerOf = ['refs/*']; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), 'editing'); - - ownerOf = []; - canUpload = true; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), 'editing'); - - editingRef = true; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), 'editing editingRef'); - - deleted = true; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), 'editing editingRef deleted'); - - editingRef = false; - assert.equal(element._computeSectionClass(editing, canUpload, ownerOf, - editingRef, deleted), 'editing deleted'); - }); - - test('_computeEditBtnClass', () => { - let name = 'GLOBAL_CAPABILITIES'; - assert.equal(element._computeEditBtnClass(name), 'global'); - name = 'refs/for/*'; - assert.equal(element._computeEditBtnClass(name), ''); + test('classes are assigned correctly', () => { + assert.isFalse(element.$.section.classList.contains('editing')); + assert.isFalse(element.$.section.classList.contains('deleted')); + assert.isTrue(element.$.editBtn.classList.contains('global')); + element.editing = true; + element.canUpload = true; + element.ownerOf = []; + assert.equal(getComputedStyle(element.$.editBtn).display, 'none'); }); }); - suite('interactive tests', () => { + suite('Non-global section', () => { setup(() => { - element.labels = { - 'Code-Review': { - values: { - ' 0': 'No score', - '-1': 'I would prefer this is not merged as is', - '-2': 'This shall not be merged', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', + element.section = { + id: 'refs/*', + value: { + permissions: { + read: { + rules: {}, + }, }, - default_value: 0, }, }; - }); - suite('Global section', () => { - setup(() => { - element.section = { - id: 'GLOBAL_CAPABILITIES', - value: { - permissions: { - accessDatabase: { - rules: {}, - }, - }, - }, - }; - element.capabilities = { - accessDatabase: { - id: 'accessDatabase', - name: 'Access Database', - }, - administrateServer: { - id: 'administrateServer', - name: 'Administrate Server', - }, - batchChangesLimit: { - id: 'batchChangesLimit', - name: 'Batch Changes Limit', - }, - createAccount: { - id: 'createAccount', - name: 'Create Account', - }, - }; - element._updateSection(element.section); - flushAsynchronousOperations(); - }); - - test('classes are assigned correctly', () => { - assert.isFalse(element.$.section.classList.contains('editing')); - assert.isFalse(element.$.section.classList.contains('deleted')); - assert.isTrue(element.$.editBtn.classList.contains('global')); - element.editing = true; - element.canUpload = true; - element.ownerOf = []; - assert.equal(getComputedStyle(element.$.editBtn).display, 'none'); - }); + element.capabilities = {}; + element._updateSection(element.section); + flushAsynchronousOperations(); }); - suite('Non-global section', () => { - setup(() => { - element.section = { - id: 'refs/*', - value: { - permissions: { - read: { - rules: {}, - }, - }, - }, - }; - element.capabilities = {}; - element._updateSection(element.section); - flushAsynchronousOperations(); - }); + test('classes are assigned correctly', () => { + assert.isFalse(element.$.section.classList.contains('editing')); + assert.isFalse(element.$.section.classList.contains('deleted')); + assert.isFalse(element.$.editBtn.classList.contains('global')); + element.editing = true; + element.canUpload = true; + element.ownerOf = []; + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none'); + }); - test('classes are assigned correctly', () => { - assert.isFalse(element.$.section.classList.contains('editing')); - assert.isFalse(element.$.section.classList.contains('deleted')); - assert.isFalse(element.$.editBtn.classList.contains('global')); - element.editing = true; - element.canUpload = true; - element.ownerOf = []; - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none'); - }); + test('add permission', () => { + element.editing = true; + element.$.permissionSelect.value = 'label-Code-Review'; + assert.equal(element._permissions.length, 1); + assert.equal(Object.keys(element.section.value.permissions).length, + 1); + MockInteractions.tap(element.$.addBtn); + flushAsynchronousOperations(); - test('add permission', () => { - element.editing = true; - element.$.permissionSelect.value = 'label-Code-Review'; - assert.equal(element._permissions.length, 1); - assert.equal(Object.keys(element.section.value.permissions).length, - 1); - MockInteractions.tap(element.$.addBtn); - flushAsynchronousOperations(); + // The permission is added to both the permissions array and also + // the section's permission object. + assert.equal(element._permissions.length, 2); + let permission = { + id: 'label-Code-Review', + value: { + added: true, + label: 'Code-Review', + rules: {}, + }, + }; + assert.equal(element._permissions.length, 2); + assert.deepEqual(element._permissions[1], permission); + assert.equal(Object.keys(element.section.value.permissions).length, + 2); + assert.deepEqual( + element.section.value.permissions['label-Code-Review'], + permission.value); - // The permission is added to both the permissions array and also - // the section's permission object. - assert.equal(element._permissions.length, 2); - let permission = { - id: 'label-Code-Review', - value: { - added: true, - label: 'Code-Review', - rules: {}, - }, - }; - assert.equal(element._permissions.length, 2); - assert.deepEqual(element._permissions[1], permission); - assert.equal(Object.keys(element.section.value.permissions).length, - 2); - assert.deepEqual( - element.section.value.permissions['label-Code-Review'], - permission.value); + element.$.permissionSelect.value = 'abandon'; + MockInteractions.tap(element.$.addBtn); + flushAsynchronousOperations(); - element.$.permissionSelect.value = 'abandon'; - MockInteractions.tap(element.$.addBtn); - flushAsynchronousOperations(); + permission = { + id: 'abandon', + value: { + added: true, + rules: {}, + }, + }; - permission = { - id: 'abandon', - value: { - added: true, - rules: {}, - }, - }; + assert.equal(element._permissions.length, 3); + assert.deepEqual(element._permissions[2], permission); + assert.equal(Object.keys(element.section.value.permissions).length, + 3); + assert.deepEqual(element.section.value.permissions['abandon'], + permission.value); - assert.equal(element._permissions.length, 3); - assert.deepEqual(element._permissions[2], permission); - assert.equal(Object.keys(element.section.value.permissions).length, - 3); - assert.deepEqual(element.section.value.permissions['abandon'], - permission.value); + // Unsaved changes are discarded when editing is cancelled. + element.editing = false; + assert.equal(element._permissions.length, 1); + assert.equal(Object.keys(element.section.value.permissions).length, + 1); + }); - // Unsaved changes are discarded when editing is cancelled. + test('edit section reference', done => { + element.canUpload = true; + element.ownerOf = []; + element.section = {id: 'refs/for/bar', value: {permissions: {}}}; + assert.isFalse(element.$.section.classList.contains('editing')); + element.editing = true; + assert.isTrue(element.$.section.classList.contains('editing')); + assert.isFalse(element._editingRef); + MockInteractions.tap(element.$.editBtn); + element.editRefInput().bindValue='new/ref'; + setTimeout(() => { + assert.equal(element.section.id, 'new/ref'); + assert.isTrue(element._editingRef); + assert.isTrue(element.$.section.classList.contains('editingRef')); element.editing = false; - assert.equal(element._permissions.length, 1); - assert.equal(Object.keys(element.section.value.permissions).length, - 1); - }); - - test('edit section reference', done => { - element.canUpload = true; - element.ownerOf = []; - element.section = {id: 'refs/for/bar', value: {permissions: {}}}; - assert.isFalse(element.$.section.classList.contains('editing')); - element.editing = true; - assert.isTrue(element.$.section.classList.contains('editing')); assert.isFalse(element._editingRef); - MockInteractions.tap(element.$.editBtn); - element.editRefInput().bindValue='new/ref'; - setTimeout(() => { - assert.equal(element.section.id, 'new/ref'); - assert.isTrue(element._editingRef); - assert.isTrue(element.$.section.classList.contains('editingRef')); - element.editing = false; - assert.isFalse(element._editingRef); - assert.equal(element.section.id, 'refs/for/bar'); - done(); - }); + assert.equal(element.section.id, 'refs/for/bar'); + done(); }); + }); - test('_handleValueChange', () => { - // For an exising section. - const modifiedHandler = sandbox.stub(); - element.section = {id: 'refs/for/bar', value: {permissions: {}}}; - assert.notOk(element.section.value.updatedId); - element.section.id = 'refs/for/baz'; - element.addEventListener('access-modified', modifiedHandler); - assert.isNotOk(element.section.value.modified); - element._handleValueChange(); - assert.equal(element.section.value.updatedId, 'refs/for/baz'); - assert.isTrue(element.section.value.modified); - assert.equal(modifiedHandler.callCount, 1); - element.section.id = 'refs/for/bar'; - element._handleValueChange(); - assert.isFalse(element.section.value.modified); - assert.equal(modifiedHandler.callCount, 2); + test('_handleValueChange', () => { + // For an exising section. + const modifiedHandler = sandbox.stub(); + element.section = {id: 'refs/for/bar', value: {permissions: {}}}; + assert.notOk(element.section.value.updatedId); + element.section.id = 'refs/for/baz'; + element.addEventListener('access-modified', modifiedHandler); + assert.isNotOk(element.section.value.modified); + element._handleValueChange(); + assert.equal(element.section.value.updatedId, 'refs/for/baz'); + assert.isTrue(element.section.value.modified); + assert.equal(modifiedHandler.callCount, 1); + element.section.id = 'refs/for/bar'; + element._handleValueChange(); + assert.isFalse(element.section.value.modified); + assert.equal(modifiedHandler.callCount, 2); - // For a new section. - element.section.value.added = true; - element._handleValueChange(); - assert.isFalse(element.section.value.modified); - assert.equal(modifiedHandler.callCount, 2); - element.section.id = 'refs/for/bar'; - element._handleValueChange(); - assert.isFalse(element.section.value.modified); - assert.equal(modifiedHandler.callCount, 2); - }); + // For a new section. + element.section.value.added = true; + element._handleValueChange(); + assert.isFalse(element.section.value.modified); + assert.equal(modifiedHandler.callCount, 2); + element.section.id = 'refs/for/bar'; + element._handleValueChange(); + assert.isFalse(element.section.value.modified); + assert.equal(modifiedHandler.callCount, 2); + }); - test('remove section', () => { - element.editing = true; - element.canUpload = true; - element.ownerOf = []; - assert.isFalse(element._deleted); - assert.isNotOk(element.section.value.deleted); - MockInteractions.tap(element.$.deleteBtn); - flushAsynchronousOperations(); - assert.isTrue(element._deleted); - assert.isTrue(element.section.value.deleted); - assert.isTrue(element.$.section.classList.contains('deleted')); - assert.isTrue(element.section.value.deleted); + test('remove section', () => { + element.editing = true; + element.canUpload = true; + element.ownerOf = []; + assert.isFalse(element._deleted); + assert.isNotOk(element.section.value.deleted); + MockInteractions.tap(element.$.deleteBtn); + flushAsynchronousOperations(); + assert.isTrue(element._deleted); + assert.isTrue(element.section.value.deleted); + assert.isTrue(element.$.section.classList.contains('deleted')); + assert.isTrue(element.section.value.deleted); - MockInteractions.tap(element.$.undoRemoveBtn); - flushAsynchronousOperations(); - assert.isFalse(element._deleted); - assert.isNotOk(element.section.value.deleted); + MockInteractions.tap(element.$.undoRemoveBtn); + flushAsynchronousOperations(); + assert.isFalse(element._deleted); + assert.isNotOk(element.section.value.deleted); - MockInteractions.tap(element.$.deleteBtn); - assert.isTrue(element._deleted); - assert.isTrue(element.section.value.deleted); - element.editing = false; - assert.isFalse(element._deleted); - assert.isNotOk(element.section.value.deleted); - }); + MockInteractions.tap(element.$.deleteBtn); + assert.isTrue(element._deleted); + assert.isTrue(element.section.value.deleted); + element.editing = false; + assert.isFalse(element._deleted); + assert.isNotOk(element.section.value.deleted); + }); - test('removing an added permission', () => { - element.editing = true; - assert.equal(element._permissions.length, 1); - element.shadowRoot - .querySelector('gr-permission').fire('added-permission-removed'); - flushAsynchronousOperations(); - assert.equal(element._permissions.length, 0); - }); + test('removing an added permission', () => { + element.editing = true; + assert.equal(element._permissions.length, 1); + element.shadowRoot + .querySelector('gr-permission').fire('added-permission-removed'); + flushAsynchronousOperations(); + assert.equal(element._permissions.length, 0); + }); - test('remove an added section', () => { - const removeStub = sandbox.stub(); - element.addEventListener('added-section-removed', removeStub); - element.editing = true; - element.section.value.added = true; - MockInteractions.tap(element.$.deleteBtn); - assert.isTrue(removeStub.called); - }); + test('remove an added section', () => { + const removeStub = sandbox.stub(); + element.addEventListener('added-section-removed', removeStub); + element.editing = true; + element.section.value.added = true; + MockInteractions.tap(element.$.deleteBtn); + assert.isTrue(removeStub.called); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js index 96008b7..bdf64de 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list.js
@@ -14,155 +14,171 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-list-view/gr-list-view.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-create-group-dialog/gr-create-group-dialog.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-admin-group-list_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.ListViewMixin + * @extends Polymer.Element + */ +class GrAdminGroupList extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.ListViewBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-admin-group-list'; } + + static get properties() { + return { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + + /** + * Offset of currently visible query results. + */ + _offset: Number, + _path: { + type: String, + readOnly: true, + value: '/admin/groups', + }, + _hasNewGroupName: Boolean, + _createNewCapability: { + type: Boolean, + value: false, + }, + _groups: Array, + + /** + * Because we request one more than the groupsPerPage, _shownGroups + * may be one less than _groups. + * */ + _shownGroups: { + type: Array, + computed: 'computeShownItems(_groups)', + }, + + _groupsPerPage: { + type: Number, + value: 25, + }, + + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + }; + } + + /** @override */ + attached() { + super.attached(); + this._getCreateGroupCapability(); + this.fire('title-change', {title: 'Groups'}); + this._maybeOpenCreateOverlay(this.params); + } + + _paramsChanged(params) { + this._loading = true; + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getGroups(this._filter, this._groupsPerPage, + this._offset); + } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.ListViewMixin - * @extends Polymer.Element + * Opens the create overlay if the route has a hash 'create' + * + * @param {!Object} params */ - class GrAdminGroupList extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.ListViewBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-admin-group-list'; } - - static get properties() { - return { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - - /** - * Offset of currently visible query results. - */ - _offset: Number, - _path: { - type: String, - readOnly: true, - value: '/admin/groups', - }, - _hasNewGroupName: Boolean, - _createNewCapability: { - type: Boolean, - value: false, - }, - _groups: Array, - - /** - * Because we request one more than the groupsPerPage, _shownGroups - * may be one less than _groups. - * */ - _shownGroups: { - type: Array, - computed: 'computeShownItems(_groups)', - }, - - _groupsPerPage: { - type: Number, - value: 25, - }, - - _loading: { - type: Boolean, - value: true, - }, - _filter: String, - }; - } - - /** @override */ - attached() { - super.attached(); - this._getCreateGroupCapability(); - this.fire('title-change', {title: 'Groups'}); - this._maybeOpenCreateOverlay(this.params); - } - - _paramsChanged(params) { - this._loading = true; - this._filter = this.getFilterValue(params); - this._offset = this.getOffsetValue(params); - - return this._getGroups(this._filter, this._groupsPerPage, - this._offset); - } - - /** - * Opens the create overlay if the route has a hash 'create' - * - * @param {!Object} params - */ - _maybeOpenCreateOverlay(params) { - if (params && params.openCreateModal) { - this.$.createOverlay.open(); - } - } - - _computeGroupUrl(id) { - return Gerrit.Nav.getUrlForGroup(id); - } - - _getCreateGroupCapability() { - return this.$.restAPI.getAccount().then(account => { - if (!account) { return; } - return this.$.restAPI.getAccountCapabilities(['createGroup']) - .then(capabilities => { - if (capabilities.createGroup) { - this._createNewCapability = true; - } - }); - }); - } - - _getGroups(filter, groupsPerPage, offset) { - this._groups = []; - return this.$.restAPI.getGroups(filter, groupsPerPage, offset) - .then(groups => { - if (!groups) { - return; - } - this._groups = Object.keys(groups) - .map(key => { - const group = groups[key]; - group.name = key; - return group; - }); - this._loading = false; - }); - } - - _refreshGroupsList() { - this.$.restAPI.invalidateGroupsCache(); - return this._getGroups(this._filter, this._groupsPerPage, - this._offset); - } - - _handleCreateGroup() { - this.$.createNewModal.handleCreateGroup().then(() => { - this._refreshGroupsList(); - }); - } - - _handleCloseCreate() { - this.$.createOverlay.close(); - } - - _handleCreateClicked() { + _maybeOpenCreateOverlay(params) { + if (params && params.openCreateModal) { this.$.createOverlay.open(); } - - _visibleToAll(item) { - return item.options.visible_to_all === true ? 'Y' : 'N'; - } } - customElements.define(GrAdminGroupList.is, GrAdminGroupList); -})(); + _computeGroupUrl(id) { + return Gerrit.Nav.getUrlForGroup(id); + } + + _getCreateGroupCapability() { + return this.$.restAPI.getAccount().then(account => { + if (!account) { return; } + return this.$.restAPI.getAccountCapabilities(['createGroup']) + .then(capabilities => { + if (capabilities.createGroup) { + this._createNewCapability = true; + } + }); + }); + } + + _getGroups(filter, groupsPerPage, offset) { + this._groups = []; + return this.$.restAPI.getGroups(filter, groupsPerPage, offset) + .then(groups => { + if (!groups) { + return; + } + this._groups = Object.keys(groups) + .map(key => { + const group = groups[key]; + group.name = key; + return group; + }); + this._loading = false; + }); + } + + _refreshGroupsList() { + this.$.restAPI.invalidateGroupsCache(); + return this._getGroups(this._filter, this._groupsPerPage, + this._offset); + } + + _handleCreateGroup() { + this.$.createNewModal.handleCreateGroup().then(() => { + this._refreshGroupsList(); + }); + } + + _handleCloseCreate() { + this.$.createOverlay.close(); + } + + _handleCreateClicked() { + this.$.createOverlay.open(); + } + + _visibleToAll(item) { + return item.options.visible_to_all === true ? 'Y' : 'N'; + } +} + +customElements.define(GrAdminGroupList.is, GrAdminGroupList);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js index 5207717..ffc10d7 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_html.js
@@ -1,64 +1,43 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-create-group-dialog/gr-create-group-dialog.html"> - -<dom-module id="gr-admin-group-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <style include="gr-table-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <gr-list-view - create-new="[[_createNewCapability]]" - filter="[[_filter]]" - items="[[_groups]]" - items-per-page="[[_groupsPerPage]]" - loading="[[_loading]]" - offset="[[_offset]]" - on-create-clicked="_handleCreateClicked" - path="[[_path]]"> + <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items="[[_groups]]" items-per-page="[[_groupsPerPage]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]"> <table id="list" class="genericList"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="name topHeader">Group Name</th> <th class="description topHeader">Group Description</th> <th class="visibleToAll topHeader">Visible To All</th> </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]"> <td>Loading...</td> </tr> - <tbody class$="[[computeLoadingClass(_loading)]]"> + </tbody><tbody class\$="[[computeLoadingClass(_loading)]]"> <template is="dom-repeat" items="[[_shownGroups]]"> <tr class="table"> <td class="name"> - <a href$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a> + <a href\$="[[_computeGroupUrl(item.group_id)]]">[[item.name]]</a> </td> <td class="description">[[item.description]]</td> <td class="visibleToAll">[[_visibleToAll(item)]]</td> @@ -67,27 +46,15 @@ </tbody> </table> </gr-list-view> - <gr-overlay id="createOverlay" with-backdrop> - <gr-dialog - id="createDialog" - class="confirmDialog" - disabled="[[!_hasNewGroupName]]" - confirm-label="Create" - confirm-on-enter - on-confirm="_handleCreateGroup" - on-cancel="_handleCloseCreate"> + <gr-overlay id="createOverlay" with-backdrop=""> + <gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewGroupName]]" confirm-label="Create" confirm-on-enter="" on-confirm="_handleCreateGroup" on-cancel="_handleCloseCreate"> <div class="header" slot="header"> Create Group </div> <div class="main" slot="main"> - <gr-create-group-dialog - has-new-group-name="{{_hasNewGroupName}}" - params="[[params]]" - id="createNewModal"></gr-create-group-dialog> + <gr-create-group-dialog has-new-group-name="{{_hasNewGroupName}}" params="[[params]]" id="createNewModal"></gr-create-group-dialog> </div> </gr-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-admin-group-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html index c0558d2..36c2081 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.html
@@ -19,18 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-group-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-admin-group-list.html"> +<script type="module" src="./gr-admin-group-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-admin-group-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -38,153 +43,155 @@ </template> </test-fixture> -<script> - let counter = 0; - const groupGenerator = () => { - return { - name: `test${++counter}`, - id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b', - url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b', - options: { - visible_to_all: false, - }, - description: 'Gerrit Site Administrators', - group_id: 1, - owner: 'Administrators', - owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc', - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-admin-group-list.js'; +let counter = 0; +const groupGenerator = () => { + return { + name: `test${++counter}`, + id: '59b92f35489e62c80d1ab1bf0c2d17843038df8b', + url: '#/admin/groups/uuid-59b92f35489e62c80d1ab1bf0c2d17843038df8b', + options: { + visible_to_all: false, + }, + description: 'Gerrit Site Administrators', + group_id: 1, + owner: 'Administrators', + owner_id: '7ca042f4d5847936fcb90ca91057673157fd06fc', }; +}; - suite('gr-admin-group-list tests', async () => { - await readyToTest(); - let element; - let groups; - let sandbox; - let value; +suite('gr-admin-group-list tests', () => { + let element; + let groups; + let sandbox; + let value; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list with groups', () => { + setup(done => { + groups = _.times(26, groupGenerator); + + stub('gr-rest-api-interface', { + getGroups(num, offset) { + return Promise.resolve(groups); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); }); - teardown(() => { - sandbox.restore(); - }); - - suite('list with groups', () => { - setup(done => { - groups = _.times(26, groupGenerator); - - stub('gr-rest-api-interface', { - getGroups(num, offset) { - return Promise.resolve(groups); - }, - }); - - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('test for test group in the list', done => { - flush(() => { - assert.equal(element._groups[1].name, '1'); - assert.equal(element._groups[1].options.visible_to_all, false); - done(); - }); - }); - - test('_shownGroups', () => { - assert.equal(element._shownGroups.length, 25); - }); - - test('_maybeOpenCreateOverlay', () => { - const overlayOpen = sandbox.stub(element.$.createOverlay, 'open'); - element._maybeOpenCreateOverlay(); - assert.isFalse(overlayOpen.called); - const params = {}; - element._maybeOpenCreateOverlay(params); - assert.isFalse(overlayOpen.called); - params.openCreateModal = true; - element._maybeOpenCreateOverlay(params); - assert.isTrue(overlayOpen.called); + test('test for test group in the list', done => { + flush(() => { + assert.equal(element._groups[1].name, '1'); + assert.equal(element._groups[1].options.visible_to_all, false); + done(); }); }); - suite('test with less then 25 groups', () => { - setup(done => { - groups = _.times(25, groupGenerator); - - stub('gr-rest-api-interface', { - getGroups(num, offset) { - return Promise.resolve(groups); - }, - }); - - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('_shownGroups', () => { - assert.equal(element._shownGroups.length, 25); - }); + test('_shownGroups', () => { + assert.equal(element._shownGroups.length, 25); }); - suite('filter', () => { - test('_paramsChanged', done => { - sandbox.stub( - element.$.restAPI, - 'getGroups', - () => Promise.resolve(groups)); - const value = { - filter: 'test', - offset: 25, - }; - element._paramsChanged(value).then(() => { - assert.isTrue(element.$.restAPI.getGroups.lastCall - .calledWithExactly('test', 25, 25)); - done(); - }); + test('_maybeOpenCreateOverlay', () => { + const overlayOpen = sandbox.stub(element.$.createOverlay, 'open'); + element._maybeOpenCreateOverlay(); + assert.isFalse(overlayOpen.called); + const params = {}; + element._maybeOpenCreateOverlay(params); + assert.isFalse(overlayOpen.called); + params.openCreateModal = true; + element._maybeOpenCreateOverlay(params); + assert.isTrue(overlayOpen.called); + }); + }); + + suite('test with less then 25 groups', () => { + setup(done => { + groups = _.times(25, groupGenerator); + + stub('gr-rest-api-interface', { + getGroups(num, offset) { + return Promise.resolve(groups); + }, }); + + element._paramsChanged(value).then(() => { flush(done); }); }); - suite('loading', () => { - test('correct contents are displayed', () => { - assert.isTrue(element._loading); - assert.equal(element.computeLoadingClass(element._loading), 'loading'); - assert.equal(getComputedStyle(element.$.loading).display, 'block'); - - element._loading = false; - element._groups = _.times(25, groupGenerator); - - flushAsynchronousOperations(); - assert.equal(element.computeLoadingClass(element._loading), ''); - assert.equal(getComputedStyle(element.$.loading).display, 'none'); - }); + test('_shownGroups', () => { + assert.equal(element._shownGroups.length, 25); }); + }); - suite('create new', () => { - test('_handleCreateClicked called when create-click fired', () => { - sandbox.stub(element, '_handleCreateClicked'); - element.shadowRoot - .querySelector('gr-list-view').fire('create-clicked'); - assert.isTrue(element._handleCreateClicked.called); - }); - - test('_handleCreateClicked opens modal', () => { - const openStub = sandbox.stub(element.$.createOverlay, 'open'); - element._handleCreateClicked(); - assert.isTrue(openStub.called); - }); - - test('_handleCreateGroup called when confirm fired', () => { - sandbox.stub(element, '_handleCreateGroup'); - element.$.createDialog.fire('confirm'); - assert.isTrue(element._handleCreateGroup.called); - }); - - test('_handleCloseCreate called when cancel fired', () => { - sandbox.stub(element, '_handleCloseCreate'); - element.$.createDialog.fire('cancel'); - assert.isTrue(element._handleCloseCreate.called); + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub( + element.$.restAPI, + 'getGroups', + () => Promise.resolve(groups)); + const value = { + filter: 'test', + offset: 25, + }; + element._paramsChanged(value).then(() => { + assert.isTrue(element.$.restAPI.getGroups.lastCall + .calledWithExactly('test', 25, 25)); + done(); }); }); }); + + suite('loading', () => { + test('correct contents are displayed', () => { + assert.isTrue(element._loading); + assert.equal(element.computeLoadingClass(element._loading), 'loading'); + assert.equal(getComputedStyle(element.$.loading).display, 'block'); + + element._loading = false; + element._groups = _.times(25, groupGenerator); + + flushAsynchronousOperations(); + assert.equal(element.computeLoadingClass(element._loading), ''); + assert.equal(getComputedStyle(element.$.loading).display, 'none'); + }); + }); + + suite('create new', () => { + test('_handleCreateClicked called when create-click fired', () => { + sandbox.stub(element, '_handleCreateClicked'); + element.shadowRoot + .querySelector('gr-list-view').fire('create-clicked'); + assert.isTrue(element._handleCreateClicked.called); + }); + + test('_handleCreateClicked opens modal', () => { + const openStub = sandbox.stub(element.$.createOverlay, 'open'); + element._handleCreateClicked(); + assert.isTrue(openStub.called); + }); + + test('_handleCreateGroup called when confirm fired', () => { + sandbox.stub(element, '_handleCreateGroup'); + element.$.createDialog.fire('confirm'); + assert.isTrue(element._handleCreateGroup.called); + }); + + test('_handleCloseCreate called when cancel fired', () => { + sandbox.stub(element, '_handleCloseCreate'); + element.$.createDialog.fire('cancel'); + assert.isTrue(element._handleCloseCreate.called); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js index e300c90..d03df39 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.js
@@ -14,281 +14,310 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../styles/gr-menu-page-styles.js'; +import '../../../styles/gr-page-nav-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-dropdown-list/gr-dropdown-list.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../../shared/gr-page-nav/gr-page-nav.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-admin-group-list/gr-admin-group-list.js'; +import '../gr-group/gr-group.js'; +import '../gr-group-audit-log/gr-group-audit-log.js'; +import '../gr-group-members/gr-group-members.js'; +import '../gr-plugin-list/gr-plugin-list.js'; +import '../gr-repo/gr-repo.js'; +import '../gr-repo-access/gr-repo-access.js'; +import '../gr-repo-commands/gr-repo-commands.js'; +import '../gr-repo-dashboards/gr-repo-dashboards.js'; +import '../gr-repo-detail-list/gr-repo-detail-list.js'; +import '../gr-repo-list/gr-repo-list.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-admin-view_html.js'; - /** - * @appliesMixin Gerrit.AdminNavMixin - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrAdminView extends Polymer.mixinBehaviors( [ - Gerrit.AdminNavBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-admin-view'; } +const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/; - static get properties() { - return { - /** @type {?} */ - params: Object, - path: String, - adminView: String, +/** + * @appliesMixin Gerrit.AdminNavMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrAdminView extends mixinBehaviors( [ + Gerrit.AdminNavBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _breadcrumbParentName: String, - _repoName: String, - _groupId: { - type: Number, - observer: '_computeGroupName', - }, - _groupIsInternal: Boolean, - _groupName: String, - _groupOwner: { - type: Boolean, - value: false, - }, - _subsectionLinks: Array, - _filteredLinks: Array, - _showDownload: { - type: Boolean, - value: false, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - _showGroup: Boolean, - _showGroupAuditLog: Boolean, - _showGroupList: Boolean, - _showGroupMembers: Boolean, - _showRepoAccess: Boolean, - _showRepoCommands: Boolean, - _showRepoDashboards: Boolean, - _showRepoDetailList: Boolean, - _showRepoMain: Boolean, - _showRepoList: Boolean, - _showPluginList: Boolean, - }; - } + static get is() { return 'gr-admin-view'; } - static get observers() { - return [ - '_paramsChanged(params)', - ]; - } + static get properties() { + return { + /** @type {?} */ + params: Object, + path: String, + adminView: String, - /** @override */ - attached() { - super.attached(); - this.reload(); - } - - reload() { - const promises = [ - this.$.restAPI.getAccount(), - Gerrit.awaitPluginsLoaded(), - ]; - return Promise.all(promises).then(result => { - this._account = result[0]; - let options; - if (this._repoName) { - options = {repoName: this._repoName}; - } else if (this._groupId) { - options = { - groupId: this._groupId, - groupName: this._groupName, - groupIsInternal: this._groupIsInternal, - isAdmin: this._isAdmin, - groupOwner: this._groupOwner, - }; - } - - return this.getAdminLinks(this._account, - this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI), - this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI), - options) - .then(res => { - this._filteredLinks = res.links; - this._breadcrumbParentName = res.expandedSection ? - res.expandedSection.name : ''; - - if (!res.expandedSection) { - this._subsectionLinks = []; - return; - } - this._subsectionLinks = [res.expandedSection] - .concat(res.expandedSection.children).map(section => { - return { - text: !section.detailType ? 'Home' : section.name, - value: section.view + (section.detailType || ''), - view: section.view, - url: section.url, - detailType: section.detailType, - parent: this._groupId || this._repoName || '', - }; - }); - }); - }); - } - - _computeSelectValue(params) { - if (!params || !params.view) { return; } - return params.view + (params.detail || ''); - } - - _selectedIsCurrentPage(selected) { - return (selected.parent === (this._repoName || this._groupId) && - selected.view === this.params.view && - selected.detailType === this.params.detail); - } - - _handleSubsectionChange(e) { - const selected = this._subsectionLinks - .find(section => section.value === e.detail.value); - - // This is when it gets set initially. - if (this._selectedIsCurrentPage(selected)) { - return; - } - Gerrit.Nav.navigateToRelativeUrl(selected.url); - } - - _paramsChanged(params) { - const isGroupView = params.view === Gerrit.Nav.View.GROUP; - const isRepoView = params.view === Gerrit.Nav.View.REPO; - const isAdminView = params.view === Gerrit.Nav.View.ADMIN; - - this.set('_showGroup', isGroupView && !params.detail); - this.set('_showGroupAuditLog', isGroupView && - params.detail === Gerrit.Nav.GroupDetailView.LOG); - this.set('_showGroupMembers', isGroupView && - params.detail === Gerrit.Nav.GroupDetailView.MEMBERS); - - this.set('_showGroupList', isAdminView && - params.adminView === 'gr-admin-group-list'); - - this.set('_showRepoAccess', isRepoView && - params.detail === Gerrit.Nav.RepoDetailView.ACCESS); - this.set('_showRepoCommands', isRepoView && - params.detail === Gerrit.Nav.RepoDetailView.COMMANDS); - this.set('_showRepoDetailList', isRepoView && - (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES || - params.detail === Gerrit.Nav.RepoDetailView.TAGS)); - this.set('_showRepoDashboards', isRepoView && - params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS); - this.set('_showRepoMain', isRepoView && !params.detail); - - this.set('_showRepoList', isAdminView && - params.adminView === 'gr-repo-list'); - - this.set('_showPluginList', isAdminView && - params.adminView === 'gr-plugin-list'); - - let needsReload = false; - if (params.repo !== this._repoName) { - this._repoName = params.repo || ''; - // Reloads the admin menu. - needsReload = true; - } - if (params.groupId !== this._groupId) { - this._groupId = params.groupId || ''; - // Reloads the admin menu. - needsReload = true; - } - if (this._breadcrumbParentName && !params.groupId && !params.repo) { - needsReload = true; - } - if (!needsReload) { return; } - this.reload(); - } - - // TODO (beckysiegel): Update these functions after router abstraction is - // updated. They are currently copied from gr-dropdown (and should be - // updated there as well once complete). - _computeURLHelper(host, path) { - return '//' + host + this.getBaseUrl() + path; - } - - _computeRelativeURL(path) { - const host = window.location.host; - return this._computeURLHelper(host, path); - } - - _computeLinkURL(link) { - if (!link || typeof link.url === 'undefined') { return ''; } - if (link.target || !link.noBaseUrl) { - return link.url; - } - return this._computeRelativeURL(link.url); - } - - /** - * @param {string} itemView - * @param {Object} params - * @param {string=} opt_detailType - */ - _computeSelectedClass(itemView, params, opt_detailType) { - if (!params) return ''; - // Group params are structured differently from admin params. Compute - // selected differently for groups. - // TODO(wyatta): Simplify this when all routes work like group params. - if (params.view === Gerrit.Nav.View.GROUP && - itemView === Gerrit.Nav.View.GROUP) { - if (!params.detail && !opt_detailType) { return 'selected'; } - if (params.detail === opt_detailType) { return 'selected'; } - return ''; - } - - if (params.view === Gerrit.Nav.View.REPO && - itemView === Gerrit.Nav.View.REPO) { - if (!params.detail && !opt_detailType) { return 'selected'; } - if (params.detail === opt_detailType) { return 'selected'; } - return ''; - } - - if (params.detailType && params.detailType !== opt_detailType) { - return ''; - } - return itemView === params.adminView ? 'selected' : ''; - } - - _computeGroupName(groupId) { - if (!groupId) { return ''; } - - const promises = []; - this.$.restAPI.getGroupConfig(groupId).then(group => { - if (!group || !group.name) { return; } - - this._groupName = group.name; - this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX); - this.reload(); - - promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => { - this._isAdmin = isAdmin; - })); - - promises.push(this.$.restAPI.getIsGroupOwner(group.name).then( - isOwner => { - this._groupOwner = isOwner; - })); - - return Promise.all(promises).then(() => { - this.reload(); - }); - }); - } - - _updateGroupName(e) { - this._groupName = e.detail.name; - this.reload(); - } + _breadcrumbParentName: String, + _repoName: String, + _groupId: { + type: Number, + observer: '_computeGroupName', + }, + _groupIsInternal: Boolean, + _groupName: String, + _groupOwner: { + type: Boolean, + value: false, + }, + _subsectionLinks: Array, + _filteredLinks: Array, + _showDownload: { + type: Boolean, + value: false, + }, + _isAdmin: { + type: Boolean, + value: false, + }, + _showGroup: Boolean, + _showGroupAuditLog: Boolean, + _showGroupList: Boolean, + _showGroupMembers: Boolean, + _showRepoAccess: Boolean, + _showRepoCommands: Boolean, + _showRepoDashboards: Boolean, + _showRepoDetailList: Boolean, + _showRepoMain: Boolean, + _showRepoList: Boolean, + _showPluginList: Boolean, + }; } - customElements.define(GrAdminView.is, GrAdminView); -})(); + static get observers() { + return [ + '_paramsChanged(params)', + ]; + } + + /** @override */ + attached() { + super.attached(); + this.reload(); + } + + reload() { + const promises = [ + this.$.restAPI.getAccount(), + Gerrit.awaitPluginsLoaded(), + ]; + return Promise.all(promises).then(result => { + this._account = result[0]; + let options; + if (this._repoName) { + options = {repoName: this._repoName}; + } else if (this._groupId) { + options = { + groupId: this._groupId, + groupName: this._groupName, + groupIsInternal: this._groupIsInternal, + isAdmin: this._isAdmin, + groupOwner: this._groupOwner, + }; + } + + return this.getAdminLinks(this._account, + this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI), + this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI), + options) + .then(res => { + this._filteredLinks = res.links; + this._breadcrumbParentName = res.expandedSection ? + res.expandedSection.name : ''; + + if (!res.expandedSection) { + this._subsectionLinks = []; + return; + } + this._subsectionLinks = [res.expandedSection] + .concat(res.expandedSection.children).map(section => { + return { + text: !section.detailType ? 'Home' : section.name, + value: section.view + (section.detailType || ''), + view: section.view, + url: section.url, + detailType: section.detailType, + parent: this._groupId || this._repoName || '', + }; + }); + }); + }); + } + + _computeSelectValue(params) { + if (!params || !params.view) { return; } + return params.view + (params.detail || ''); + } + + _selectedIsCurrentPage(selected) { + return (selected.parent === (this._repoName || this._groupId) && + selected.view === this.params.view && + selected.detailType === this.params.detail); + } + + _handleSubsectionChange(e) { + const selected = this._subsectionLinks + .find(section => section.value === e.detail.value); + + // This is when it gets set initially. + if (this._selectedIsCurrentPage(selected)) { + return; + } + Gerrit.Nav.navigateToRelativeUrl(selected.url); + } + + _paramsChanged(params) { + const isGroupView = params.view === Gerrit.Nav.View.GROUP; + const isRepoView = params.view === Gerrit.Nav.View.REPO; + const isAdminView = params.view === Gerrit.Nav.View.ADMIN; + + this.set('_showGroup', isGroupView && !params.detail); + this.set('_showGroupAuditLog', isGroupView && + params.detail === Gerrit.Nav.GroupDetailView.LOG); + this.set('_showGroupMembers', isGroupView && + params.detail === Gerrit.Nav.GroupDetailView.MEMBERS); + + this.set('_showGroupList', isAdminView && + params.adminView === 'gr-admin-group-list'); + + this.set('_showRepoAccess', isRepoView && + params.detail === Gerrit.Nav.RepoDetailView.ACCESS); + this.set('_showRepoCommands', isRepoView && + params.detail === Gerrit.Nav.RepoDetailView.COMMANDS); + this.set('_showRepoDetailList', isRepoView && + (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES || + params.detail === Gerrit.Nav.RepoDetailView.TAGS)); + this.set('_showRepoDashboards', isRepoView && + params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS); + this.set('_showRepoMain', isRepoView && !params.detail); + + this.set('_showRepoList', isAdminView && + params.adminView === 'gr-repo-list'); + + this.set('_showPluginList', isAdminView && + params.adminView === 'gr-plugin-list'); + + let needsReload = false; + if (params.repo !== this._repoName) { + this._repoName = params.repo || ''; + // Reloads the admin menu. + needsReload = true; + } + if (params.groupId !== this._groupId) { + this._groupId = params.groupId || ''; + // Reloads the admin menu. + needsReload = true; + } + if (this._breadcrumbParentName && !params.groupId && !params.repo) { + needsReload = true; + } + if (!needsReload) { return; } + this.reload(); + } + + // TODO (beckysiegel): Update these functions after router abstraction is + // updated. They are currently copied from gr-dropdown (and should be + // updated there as well once complete). + _computeURLHelper(host, path) { + return '//' + host + this.getBaseUrl() + path; + } + + _computeRelativeURL(path) { + const host = window.location.host; + return this._computeURLHelper(host, path); + } + + _computeLinkURL(link) { + if (!link || typeof link.url === 'undefined') { return ''; } + if (link.target || !link.noBaseUrl) { + return link.url; + } + return this._computeRelativeURL(link.url); + } + + /** + * @param {string} itemView + * @param {Object} params + * @param {string=} opt_detailType + */ + _computeSelectedClass(itemView, params, opt_detailType) { + if (!params) return ''; + // Group params are structured differently from admin params. Compute + // selected differently for groups. + // TODO(wyatta): Simplify this when all routes work like group params. + if (params.view === Gerrit.Nav.View.GROUP && + itemView === Gerrit.Nav.View.GROUP) { + if (!params.detail && !opt_detailType) { return 'selected'; } + if (params.detail === opt_detailType) { return 'selected'; } + return ''; + } + + if (params.view === Gerrit.Nav.View.REPO && + itemView === Gerrit.Nav.View.REPO) { + if (!params.detail && !opt_detailType) { return 'selected'; } + if (params.detail === opt_detailType) { return 'selected'; } + return ''; + } + + if (params.detailType && params.detailType !== opt_detailType) { + return ''; + } + return itemView === params.adminView ? 'selected' : ''; + } + + _computeGroupName(groupId) { + if (!groupId) { return ''; } + + const promises = []; + this.$.restAPI.getGroupConfig(groupId).then(group => { + if (!group || !group.name) { return; } + + this._groupName = group.name; + this._groupIsInternal = !!group.id.match(INTERNAL_GROUP_REGEX); + this.reload(); + + promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => { + this._isAdmin = isAdmin; + })); + + promises.push(this.$.restAPI.getIsGroupOwner(group.name).then( + isOwner => { + this._groupOwner = isOwner; + })); + + return Promise.all(promises).then(() => { + this.reload(); + }); + }); + } + + _updateGroupName(e) { + this._groupName = e.detail.name; + this.reload(); + } +} + +customElements.define(GrAdminView.is, GrAdminView);
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js index aae11d3..0bc9431 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_html.js
@@ -1,48 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../styles/gr-menu-page-styles.html"> -<link rel="import" href="../../../styles/gr-page-nav-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-admin-group-list/gr-admin-group-list.html"> -<link rel="import" href="../gr-group/gr-group.html"> -<link rel="import" href="../gr-group-audit-log/gr-group-audit-log.html"> -<link rel="import" href="../gr-group-members/gr-group-members.html"> -<link rel="import" href="../gr-plugin-list/gr-plugin-list.html"> -<link rel="import" href="../gr-repo/gr-repo.html"> -<link rel="import" href="../gr-repo-access/gr-repo-access.html"> -<link rel="import" href="../gr-repo-commands/gr-repo-commands.html"> -<link rel="import" href="../gr-repo-dashboards/gr-repo-dashboards.html"> -<link rel="import" href="../gr-repo-detail-list/gr-repo-detail-list.html"> -<link rel="import" href="../gr-repo-list/gr-repo-list.html"> - -<dom-module id="gr-admin-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -84,28 +58,24 @@ <gr-page-nav class="navStyles"> <ul class="sectionContent"> <template id="adminNav" is="dom-repeat" items="[[_filteredLinks]]"> - <li class$="sectionTitle [[_computeSelectedClass(item.view, params)]]"> - <a class="title" href="[[_computeLinkURL(item)]]" - rel="noopener">[[item.name]]</a> + <li class\$="sectionTitle [[_computeSelectedClass(item.view, params)]]"> + <a class="title" href="[[_computeLinkURL(item)]]" rel="noopener">[[item.name]]</a> </li> <template is="dom-repeat" items="[[item.children]]" as="child"> - <li class$="[[_computeSelectedClass(child.view, params)]]"> - <a href$="[[_computeLinkURL(child)]]" - rel="noopener">[[child.name]]</a> + <li class\$="[[_computeSelectedClass(child.view, params)]]"> + <a href\$="[[_computeLinkURL(child)]]" rel="noopener">[[child.name]]</a> </li> </template> <template is="dom-if" if="[[item.subsection]]"> <!--If a section has a subsection, render that.--> - <li class$="[[_computeSelectedClass(item.subsection.view, params)]]"> - <a class="title" href$="[[_computeLinkURL(item.subsection)]]" - rel="noopener"> + <li class\$="[[_computeSelectedClass(item.subsection.view, params)]]"> + <a class="title" href\$="[[_computeLinkURL(item.subsection)]]" rel="noopener"> [[item.subsection.name]]</a> </li> <!--Loop through the links in the sub-section.--> - <template is="dom-repeat" - items="[[item.subsection.children]]" as="child"> - <li class$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]"> - <a href$="[[_computeLinkURL(child)]]">[[child.name]]</a> + <template is="dom-repeat" items="[[item.subsection.children]]" as="child"> + <li class\$="subsectionItem [[_computeSelectedClass(child.view, params, child.detailType)]]"> + <a href\$="[[_computeLinkURL(child)]]">[[child.name]]</a> </li> </template> </template> @@ -118,12 +88,7 @@ <span class="breadcrumbText">[[_breadcrumbParentName]]</span> <iron-icon icon="gr-icons:chevron-right"></iron-icon> </span> - <gr-dropdown-list - lowercase - id="pageSelect" - value="[[_computeSelectValue(params)]]" - items="[[_subsectionLinks]]" - on-value-change="_handleSubsectionChange"> + <gr-dropdown-list lowercase="" id="pageSelect" value="[[_computeSelectValue(params)]]" items="[[_subsectionLinks]]" on-value-change="_handleSubsectionChange"> </gr-dropdown-list> </section> </template> @@ -150,42 +115,32 @@ </template> <template is="dom-if" if="[[_showGroup]]" restamp="true"> <main class="breadcrumbs"> - <gr-group - group-id="[[params.groupId]]" - on-name-changed="_updateGroupName"></gr-group> + <gr-group group-id="[[params.groupId]]" on-name-changed="_updateGroupName"></gr-group> </main> </template> <template is="dom-if" if="[[_showGroupMembers]]" restamp="true"> <main class="breadcrumbs"> - <gr-group-members - group-id="[[params.groupId]]"></gr-group-members> + <gr-group-members group-id="[[params.groupId]]"></gr-group-members> </main> </template> <template is="dom-if" if="[[_showRepoDetailList]]" restamp="true"> <main class="table breadcrumbs"> - <gr-repo-detail-list - params="[[params]]" - class="table"></gr-repo-detail-list> + <gr-repo-detail-list params="[[params]]" class="table"></gr-repo-detail-list> </main> </template> <template is="dom-if" if="[[_showGroupAuditLog]]" restamp="true"> <main class="table breadcrumbs"> - <gr-group-audit-log - group-id="[[params.groupId]]" - class="table"></gr-group-audit-log> + <gr-group-audit-log group-id="[[params.groupId]]" class="table"></gr-group-audit-log> </main> </template> <template is="dom-if" if="[[_showRepoCommands]]" restamp="true"> <main class="breadcrumbs"> - <gr-repo-commands - repo="[[params.repo]]"></gr-repo-commands> + <gr-repo-commands repo="[[params.repo]]"></gr-repo-commands> </main> </template> <template is="dom-if" if="[[_showRepoAccess]]" restamp="true"> <main class="breadcrumbs"> - <gr-repo-access - path="[[path]]" - repo="[[params.repo]]"></gr-repo-access> + <gr-repo-access path="[[path]]" repo="[[params.repo]]"></gr-repo-access> </main> </template> <template is="dom-if" if="[[_showRepoDashboards]]" restamp="true"> @@ -195,6 +150,4 @@ </template> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> - </template> - <script src="gr-admin-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html index 416099d..584eb88 100644 --- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html +++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-admin-view.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-admin-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-admin-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,645 +40,648 @@ </template> </test-fixture> -<script> - suite('gr-admin-view tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-admin-view.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-admin-view tests', () => { + let element; + let sandbox; - setup(done => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - stub('gr-rest-api-interface', { - getProjectConfig() { - return Promise.resolve({}); - }, + setup(done => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + stub('gr-rest-api-interface', { + getProjectConfig() { + return Promise.resolve({}); + }, + }); + const pluginsLoaded = Promise.resolve(); + sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded); + pluginsLoaded.then(() => flush(done)); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeURLHelper', () => { + const path = '/test'; + const host = 'http://www.testsite.com'; + const computedPath = element._computeURLHelper(host, path); + assert.equal(computedPath, '//http://www.testsite.com/test'); + }); + + test('link URLs', () => { + assert.equal( + element._computeLinkURL({url: '/test', noBaseUrl: true}), + '//' + window.location.host + '/test'); + + sandbox.stub(element, 'getBaseUrl').returns('/foo'); + assert.equal( + element._computeLinkURL({url: '/test', noBaseUrl: true}), + '//' + window.location.host + '/foo/test'); + assert.equal(element._computeLinkURL({url: '/test'}), '/test'); + assert.equal( + element._computeLinkURL({url: '/test', target: '_blank'}), + '/test'); + }); + + test('current page gets selected and is displayed', () => { + element._filteredLinks = [{ + name: 'Repositories', + url: '/admin/repos', + view: 'gr-repo-list', + }]; + + element.params = { + view: 'admin', + adminView: 'gr-repo-list', + }; + + flushAsynchronousOperations(); + assert.equal(dom(element.root).querySelectorAll( + '.selected').length, 1); + assert.ok(element.shadowRoot + .querySelector('gr-repo-list')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-admin-create-repo')); + }); + + test('_filteredLinks admin', done => { + sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ + name: 'test-user', + })); + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + }) + ); + element.reload().then(() => { + assert.equal(element._filteredLinks.length, 3); + + // Repos + assert.isNotOk(element._filteredLinks[0].subsection); + + // Groups + assert.isNotOk(element._filteredLinks[0].subsection); + + // Plugins + assert.isNotOk(element._filteredLinks[0].subsection); + done(); + }); + }); + + test('_filteredLinks non admin authenticated', done => { + sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ + name: 'test-user', + })); + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({}) + ); + element.reload().then(() => { + assert.equal(element._filteredLinks.length, 2); + + // Repos + assert.isNotOk(element._filteredLinks[0].subsection); + + // Groups + assert.isNotOk(element._filteredLinks[0].subsection); + done(); + }); + }); + + test('_filteredLinks non admin unathenticated', done => { + element.reload().then(() => { + assert.equal(element._filteredLinks.length, 1); + + // Repos + assert.isNotOk(element._filteredLinks[0].subsection); + done(); + }); + }); + + test('_filteredLinks from plugin', () => { + sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([ + {text: 'internal link text', url: '/internal/link/url'}, + {text: 'external link text', url: 'http://external/link/url'}, + ]); + return element.reload().then(() => { + assert.equal(element._filteredLinks.length, 3); + assert.deepEqual(element._filteredLinks[1], { + capability: null, + url: '/internal/link/url', + name: 'internal link text', + noBaseUrl: true, + view: null, + viewableToAll: true, + target: null, }); - const pluginsLoaded = Promise.resolve(); - sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(pluginsLoaded); - pluginsLoaded.then(() => flush(done)); + assert.deepEqual(element._filteredLinks[2], { + capability: null, + url: 'http://external/link/url', + name: 'external link text', + noBaseUrl: false, + view: null, + viewableToAll: true, + target: '_blank', + }); }); + }); - teardown(() => { - sandbox.restore(); - }); - - test('_computeURLHelper', () => { - const path = '/test'; - const host = 'http://www.testsite.com'; - const computedPath = element._computeURLHelper(host, path); - assert.equal(computedPath, '//http://www.testsite.com/test'); - }); - - test('link URLs', () => { + test('Repo shows up in nav', done => { + element._repoName = 'Test Repo'; + sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ + name: 'test-user', + })); + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + })); + element.reload().then(() => { + flushAsynchronousOperations(); + assert.equal(dom(element.root) + .querySelectorAll('.sectionTitle').length, 3); + assert.equal(element.shadowRoot + .querySelector('.breadcrumbText').innerText, 'Test Repo'); assert.equal( - element._computeLinkURL({url: '/test', noBaseUrl: true}), - '//' + window.location.host + '/test'); - - sandbox.stub(element, 'getBaseUrl').returns('/foo'); - assert.equal( - element._computeLinkURL({url: '/test', noBaseUrl: true}), - '//' + window.location.host + '/foo/test'); - assert.equal(element._computeLinkURL({url: '/test'}), '/test'); - assert.equal( - element._computeLinkURL({url: '/test', target: '_blank'}), - '/test'); + element.shadowRoot.querySelector('#pageSelect').items.length, + 6 + ); + done(); }); + }); - test('current page gets selected and is displayed', () => { - element._filteredLinks = [{ + test('Group shows up in nav', done => { + element._groupId = 'a15262'; + element._groupName = 'my-group'; + element._groupIsInternal = true; + element._isAdmin = true; + element._groupOwner = false; + sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ + name: 'test-user', + })); + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + })); + element.reload().then(() => { + flushAsynchronousOperations(); + assert.equal(element._filteredLinks.length, 3); + + // Repos + assert.isNotOk(element._filteredLinks[0].subsection); + + // Groups + assert.equal(element._filteredLinks[1].subsection.children.length, 2); + assert.equal(element._filteredLinks[1].subsection.name, 'my-group'); + + // Plugins + assert.isNotOk(element._filteredLinks[2].subsection); + done(); + }); + }); + + test('Nav is reloaded when repo changes', () => { + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + })); + sandbox.stub( + element.$.restAPI, + 'getAccount', + () => Promise.resolve({_id: 1})); + sandbox.stub(element, 'reload'); + element.params = {repo: 'Test Repo', adminView: 'gr-repo'}; + assert.equal(element.reload.callCount, 1); + element.params = {repo: 'Test Repo 2', + adminView: 'gr-repo'}; + assert.equal(element.reload.callCount, 2); + }); + + test('Nav is reloaded when group changes', () => { + sandbox.stub(element, '_computeGroupName'); + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + })); + sandbox.stub( + element.$.restAPI, + 'getAccount', + () => Promise.resolve({_id: 1})); + sandbox.stub(element, 'reload'); + element.params = {groupId: '1', adminView: 'gr-group'}; + assert.equal(element.reload.callCount, 1); + }); + + test('Nav is reloaded when group name changes', done => { + const newName = 'newName'; + sandbox.stub(element, '_computeGroupName'); + sandbox.stub(element, 'reload', () => { + assert.equal(element._groupName, newName); + assert.isTrue(element.reload.called); + done(); + }); + element.params = {group: 1, view: Gerrit.Nav.View.GROUP}; + element._groupName = 'oldName'; + flushAsynchronousOperations(); + element.shadowRoot + .querySelector('gr-group').fire('name-changed', {name: newName}); + }); + + test('dropdown displays if there is a subsection', () => { + assert.isNotOk(element.shadowRoot + .querySelector('.mainHeader')); + element._subsectionLinks = [ + { + text: 'Home', + value: 'repo', + view: 'repo', + url: '', + parent: 'my-repo', + detailType: undefined, + }, + ]; + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('.mainHeader')); + element._subsectionLinks = undefined; + flushAsynchronousOperations(); + assert.equal( + getComputedStyle(element.shadowRoot + .querySelector('.mainHeader')).display, + 'none'); + }); + + test('Dropdown only triggers navigation on explicit select', done => { + element._repoName = 'my-repo'; + element.params = { + repo: 'my-repo', + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + }; + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + })); + sandbox.stub( + element.$.restAPI, + 'getAccount', + () => Promise.resolve({_id: 1})); + flushAsynchronousOperations(); + const expectedFilteredLinks = [ + { name: 'Repositories', + noBaseUrl: true, url: '/admin/repos', view: 'gr-repo-list', - }]; - - element.params = { - view: 'admin', - adminView: 'gr-repo-list', - }; - - flushAsynchronousOperations(); - assert.equal(Polymer.dom(element.root).querySelectorAll( - '.selected').length, 1); - assert.ok(element.shadowRoot - .querySelector('gr-repo-list')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-admin-create-repo')); - }); - - test('_filteredLinks admin', done => { - sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ - name: 'test-user', - })); - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - }) - ); - element.reload().then(() => { - assert.equal(element._filteredLinks.length, 3); - - // Repos - assert.isNotOk(element._filteredLinks[0].subsection); - - // Groups - assert.isNotOk(element._filteredLinks[0].subsection); - - // Plugins - assert.isNotOk(element._filteredLinks[0].subsection); - done(); - }); - }); - - test('_filteredLinks non admin authenticated', done => { - sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ - name: 'test-user', - })); - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({}) - ); - element.reload().then(() => { - assert.equal(element._filteredLinks.length, 2); - - // Repos - assert.isNotOk(element._filteredLinks[0].subsection); - - // Groups - assert.isNotOk(element._filteredLinks[0].subsection); - done(); - }); - }); - - test('_filteredLinks non admin unathenticated', done => { - element.reload().then(() => { - assert.equal(element._filteredLinks.length, 1); - - // Repos - assert.isNotOk(element._filteredLinks[0].subsection); - done(); - }); - }); - - test('_filteredLinks from plugin', () => { - sandbox.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([ - {text: 'internal link text', url: '/internal/link/url'}, - {text: 'external link text', url: 'http://external/link/url'}, - ]); - return element.reload().then(() => { - assert.equal(element._filteredLinks.length, 3); - assert.deepEqual(element._filteredLinks[1], { - capability: null, - url: '/internal/link/url', - name: 'internal link text', - noBaseUrl: true, - view: null, - viewableToAll: true, - target: null, - }); - assert.deepEqual(element._filteredLinks[2], { - capability: null, - url: 'http://external/link/url', - name: 'external link text', - noBaseUrl: false, - view: null, - viewableToAll: true, - target: '_blank', - }); - }); - }); - - test('Repo shows up in nav', done => { - element._repoName = 'Test Repo'; - sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ - name: 'test-user', - })); - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - })); - element.reload().then(() => { - flushAsynchronousOperations(); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('.sectionTitle').length, 3); - assert.equal(element.shadowRoot - .querySelector('.breadcrumbText').innerText, 'Test Repo'); - assert.equal( - element.shadowRoot.querySelector('#pageSelect').items.length, - 6 - ); - done(); - }); - }); - - test('Group shows up in nav', done => { - element._groupId = 'a15262'; - element._groupName = 'my-group'; - element._groupIsInternal = true; - element._isAdmin = true; - element._groupOwner = false; - sandbox.stub(element.$.restAPI, 'getAccount').returns(Promise.resolve({ - name: 'test-user', - })); - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - })); - element.reload().then(() => { - flushAsynchronousOperations(); - assert.equal(element._filteredLinks.length, 3); - - // Repos - assert.isNotOk(element._filteredLinks[0].subsection); - - // Groups - assert.equal(element._filteredLinks[1].subsection.children.length, 2); - assert.equal(element._filteredLinks[1].subsection.name, 'my-group'); - - // Plugins - assert.isNotOk(element._filteredLinks[2].subsection); - done(); - }); - }); - - test('Nav is reloaded when repo changes', () => { - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - })); - sandbox.stub( - element.$.restAPI, - 'getAccount', - () => Promise.resolve({_id: 1})); - sandbox.stub(element, 'reload'); - element.params = {repo: 'Test Repo', adminView: 'gr-repo'}; - assert.equal(element.reload.callCount, 1); - element.params = {repo: 'Test Repo 2', - adminView: 'gr-repo'}; - assert.equal(element.reload.callCount, 2); - }); - - test('Nav is reloaded when group changes', () => { - sandbox.stub(element, '_computeGroupName'); - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - })); - sandbox.stub( - element.$.restAPI, - 'getAccount', - () => Promise.resolve({_id: 1})); - sandbox.stub(element, 'reload'); - element.params = {groupId: '1', adminView: 'gr-group'}; - assert.equal(element.reload.callCount, 1); - }); - - test('Nav is reloaded when group name changes', done => { - const newName = 'newName'; - sandbox.stub(element, '_computeGroupName'); - sandbox.stub(element, 'reload', () => { - assert.equal(element._groupName, newName); - assert.isTrue(element.reload.called); - done(); - }); - element.params = {group: 1, view: Gerrit.Nav.View.GROUP}; - element._groupName = 'oldName'; - flushAsynchronousOperations(); - element.shadowRoot - .querySelector('gr-group').fire('name-changed', {name: newName}); - }); - - test('dropdown displays if there is a subsection', () => { - assert.isNotOk(element.shadowRoot - .querySelector('.mainHeader')); - element._subsectionLinks = [ - { - text: 'Home', - value: 'repo', + viewableToAll: true, + subsection: { + name: 'my-repo', view: 'repo', url: '', - parent: 'my-repo', - detailType: undefined, + children: [ + { + name: 'Access', + view: 'repo', + detailType: 'access', + url: '', + }, + { + name: 'Commands', + view: 'repo', + detailType: 'commands', + url: '', + }, + { + name: 'Branches', + view: 'repo', + detailType: 'branches', + url: '', + }, + { + name: 'Tags', + view: 'repo', + detailType: 'tags', + url: '', + }, + { + name: 'Dashboards', + view: 'repo', + detailType: 'dashboards', + url: '', + }, + ], }, - ]; - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('.mainHeader')); - element._subsectionLinks = undefined; - flushAsynchronousOperations(); - assert.equal( - getComputedStyle(element.shadowRoot - .querySelector('.mainHeader')).display, - 'none'); - }); - - test('Dropdown only triggers navigation on explicit select', done => { - element._repoName = 'my-repo'; - element.params = { - repo: 'my-repo', - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - }; - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - })); - sandbox.stub( - element.$.restAPI, - 'getAccount', - () => Promise.resolve({_id: 1})); - flushAsynchronousOperations(); - const expectedFilteredLinks = [ - { - name: 'Repositories', - noBaseUrl: true, - url: '/admin/repos', - view: 'gr-repo-list', - viewableToAll: true, - subsection: { - name: 'my-repo', - view: 'repo', - url: '', - children: [ - { - name: 'Access', - view: 'repo', - detailType: 'access', - url: '', - }, - { - name: 'Commands', - view: 'repo', - detailType: 'commands', - url: '', - }, - { - name: 'Branches', - view: 'repo', - detailType: 'branches', - url: '', - }, - { - name: 'Tags', - view: 'repo', - detailType: 'tags', - url: '', - }, - { - name: 'Dashboards', - view: 'repo', - detailType: 'dashboards', - url: '', - }, - ], - }, - }, - { - name: 'Groups', - section: 'Groups', - noBaseUrl: true, - url: '/admin/groups', - view: 'gr-admin-group-list', - }, - { - name: 'Plugins', - capability: 'viewPlugins', - section: 'Plugins', - noBaseUrl: true, - url: '/admin/plugins', - view: 'gr-plugin-list', - }, - ]; - const expectedSubsectionLinks = [ - { - text: 'Home', - value: 'repo', - view: 'repo', - url: '', - parent: 'my-repo', - detailType: undefined, - }, - { - text: 'Access', - value: 'repoaccess', - view: 'repo', - url: '', - detailType: 'access', - parent: 'my-repo', - }, - { - text: 'Commands', - value: 'repocommands', - view: 'repo', - url: '', - detailType: 'commands', - parent: 'my-repo', - }, - { - text: 'Branches', - value: 'repobranches', - view: 'repo', - url: '', - detailType: 'branches', - parent: 'my-repo', - }, - { - text: 'Tags', - value: 'repotags', - view: 'repo', - url: '', - detailType: 'tags', - parent: 'my-repo', - }, - { - text: 'Dashboards', - value: 'repodashboards', - view: 'repo', - url: '', - detailType: 'dashboards', - parent: 'my-repo', - }, - ]; - sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); - sandbox.spy(element, '_selectedIsCurrentPage'); - sandbox.spy(element, '_handleSubsectionChange'); - element.reload().then(() => { - assert.deepEqual(element._filteredLinks, expectedFilteredLinks); - assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks); - assert.equal( - element.shadowRoot.querySelector('#pageSelect').value, - 'repoaccess' - ); - assert.isTrue(element._selectedIsCurrentPage.calledOnce); - // Doesn't trigger navigation from the page select menu. - assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called); - - // When explicitly changed, navigation is called - element.shadowRoot.querySelector('#pageSelect').value = 'repo'; - assert.isTrue(element._selectedIsCurrentPage.calledTwice); - assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce); - done(); - }); - }); - - test('_selectedIsCurrentPage', () => { - element._repoName = 'my-repo'; - element.params = {view: 'repo', repo: 'my-repo'}; - const selected = { + }, + { + name: 'Groups', + section: 'Groups', + noBaseUrl: true, + url: '/admin/groups', + view: 'gr-admin-group-list', + }, + { + name: 'Plugins', + capability: 'viewPlugins', + section: 'Plugins', + noBaseUrl: true, + url: '/admin/plugins', + view: 'gr-plugin-list', + }, + ]; + const expectedSubsectionLinks = [ + { + text: 'Home', + value: 'repo', view: 'repo', - detailType: undefined, + url: '', parent: 'my-repo', - }; - assert.isTrue(element._selectedIsCurrentPage(selected)); - selected.parent = 'my-second-repo'; - assert.isFalse(element._selectedIsCurrentPage(selected)); - selected.detailType = 'detailType'; - assert.isFalse(element._selectedIsCurrentPage(selected)); + detailType: undefined, + }, + { + text: 'Access', + value: 'repoaccess', + view: 'repo', + url: '', + detailType: 'access', + parent: 'my-repo', + }, + { + text: 'Commands', + value: 'repocommands', + view: 'repo', + url: '', + detailType: 'commands', + parent: 'my-repo', + }, + { + text: 'Branches', + value: 'repobranches', + view: 'repo', + url: '', + detailType: 'branches', + parent: 'my-repo', + }, + { + text: 'Tags', + value: 'repotags', + view: 'repo', + url: '', + detailType: 'tags', + parent: 'my-repo', + }, + { + text: 'Dashboards', + value: 'repodashboards', + view: 'repo', + url: '', + detailType: 'dashboards', + parent: 'my-repo', + }, + ]; + sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); + sandbox.spy(element, '_selectedIsCurrentPage'); + sandbox.spy(element, '_handleSubsectionChange'); + element.reload().then(() => { + assert.deepEqual(element._filteredLinks, expectedFilteredLinks); + assert.deepEqual(element._subsectionLinks, expectedSubsectionLinks); + assert.equal( + element.shadowRoot.querySelector('#pageSelect').value, + 'repoaccess' + ); + assert.isTrue(element._selectedIsCurrentPage.calledOnce); + // Doesn't trigger navigation from the page select menu. + assert.isFalse(Gerrit.Nav.navigateToRelativeUrl.called); + + // When explicitly changed, navigation is called + element.shadowRoot.querySelector('#pageSelect').value = 'repo'; + assert.isTrue(element._selectedIsCurrentPage.calledTwice); + assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.calledOnce); + done(); + }); + }); + + test('_selectedIsCurrentPage', () => { + element._repoName = 'my-repo'; + element.params = {view: 'repo', repo: 'my-repo'}; + const selected = { + view: 'repo', + detailType: undefined, + parent: 'my-repo', + }; + assert.isTrue(element._selectedIsCurrentPage(selected)); + selected.parent = 'my-second-repo'; + assert.isFalse(element._selectedIsCurrentPage(selected)); + selected.detailType = 'detailType'; + assert.isFalse(element._selectedIsCurrentPage(selected)); + }); + + suite('_computeSelectedClass', () => { + setup(() => { + sandbox.stub( + element.$.restAPI, + 'getAccountCapabilities', + () => Promise.resolve({ + createGroup: true, + createProject: true, + viewPlugins: true, + })); + sandbox.stub( + element.$.restAPI, + 'getAccount', + () => Promise.resolve({_id: 1})); + + return element.reload(); }); - suite('_computeSelectedClass', () => { + suite('repos', () => { setup(() => { - sandbox.stub( - element.$.restAPI, - 'getAccountCapabilities', - () => Promise.resolve({ - createGroup: true, - createProject: true, - viewPlugins: true, - })); - sandbox.stub( - element.$.restAPI, - 'getAccount', - () => Promise.resolve({_id: 1})); + stub('gr-repo-access', { + _repoChanged: () => {}, + }); + }); + test('repo list', () => { + element.params = { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + openCreateModal: false, + }; + flushAsynchronousOperations(); + const selected = element.shadowRoot + .querySelector('gr-page-nav .selected'); + assert.isOk(selected); + assert.equal(selected.textContent.trim(), 'Repositories'); + }); + + test('repo', () => { + element.params = { + view: Gerrit.Nav.View.REPO, + repoName: 'foo', + }; + element._repoName = 'foo'; + return element.reload().then(() => { + flushAsynchronousOperations(); + const selected = element.shadowRoot + .querySelector('gr-page-nav .selected'); + assert.isOk(selected); + assert.equal(selected.textContent.trim(), 'foo'); + }); + }); + + test('repo access', () => { + element.params = { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + repoName: 'foo', + }; + element._repoName = 'foo'; + return element.reload().then(() => { + flushAsynchronousOperations(); + const selected = element.shadowRoot + .querySelector('gr-page-nav .selected'); + assert.isOk(selected); + assert.equal(selected.textContent.trim(), 'Access'); + }); + }); + + test('repo dashboards', () => { + element.params = { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, + repoName: 'foo', + }; + element._repoName = 'foo'; + return element.reload().then(() => { + flushAsynchronousOperations(); + const selected = element.shadowRoot + .querySelector('gr-page-nav .selected'); + assert.isOk(selected); + assert.equal(selected.textContent.trim(), 'Dashboards'); + }); + }); + }); + + suite('groups', () => { + setup(() => { + stub('gr-group', { + _loadGroup: () => Promise.resolve({}), + }); + stub('gr-group-members', { + _loadGroupDetails: () => {}, + }); + + sandbox.stub(element.$.restAPI, 'getGroupConfig') + .returns(Promise.resolve({ + name: 'foo', + id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9', + })); + sandbox.stub(element.$.restAPI, 'getIsGroupOwner') + .returns(Promise.resolve(true)); return element.reload(); }); - suite('repos', () => { - setup(() => { - stub('gr-repo-access', { - _repoChanged: () => {}, - }); - }); + test('group list', () => { + element.params = { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + openCreateModal: false, + }; + flushAsynchronousOperations(); + const selected = element.shadowRoot + .querySelector('gr-page-nav .selected'); + assert.isOk(selected); + assert.equal(selected.textContent.trim(), 'Groups'); + }); - test('repo list', () => { - element.params = { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - openCreateModal: false, - }; + test('internal group', () => { + element.params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + }; + element._groupName = 'foo'; + return element.reload().then(() => { flushAsynchronousOperations(); + const subsectionItems = dom(element.root) + .querySelectorAll('.subsectionItem'); + assert.equal(subsectionItems.length, 2); + assert.isTrue(element._groupIsInternal); const selected = element.shadowRoot .querySelector('gr-page-nav .selected'); assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'Repositories'); - }); - - test('repo', () => { - element.params = { - view: Gerrit.Nav.View.REPO, - repoName: 'foo', - }; - element._repoName = 'foo'; - return element.reload().then(() => { - flushAsynchronousOperations(); - const selected = element.shadowRoot - .querySelector('gr-page-nav .selected'); - assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'foo'); - }); - }); - - test('repo access', () => { - element.params = { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - repoName: 'foo', - }; - element._repoName = 'foo'; - return element.reload().then(() => { - flushAsynchronousOperations(); - const selected = element.shadowRoot - .querySelector('gr-page-nav .selected'); - assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'Access'); - }); - }); - - test('repo dashboards', () => { - element.params = { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, - repoName: 'foo', - }; - element._repoName = 'foo'; - return element.reload().then(() => { - flushAsynchronousOperations(); - const selected = element.shadowRoot - .querySelector('gr-page-nav .selected'); - assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'Dashboards'); - }); + assert.equal(selected.textContent.trim(), 'foo'); }); }); - suite('groups', () => { - setup(() => { - stub('gr-group', { - _loadGroup: () => Promise.resolve({}), - }); - stub('gr-group-members', { - _loadGroupDetails: () => {}, - }); - - sandbox.stub(element.$.restAPI, 'getGroupConfig') - .returns(Promise.resolve({ - name: 'foo', - id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9', - })); - sandbox.stub(element.$.restAPI, 'getIsGroupOwner') - .returns(Promise.resolve(true)); - return element.reload(); + test('external group', () => { + element.$.restAPI.getGroupConfig.restore(); + sandbox.stub(element.$.restAPI, 'getGroupConfig') + .returns(Promise.resolve({ + name: 'foo', + id: 'external-id', + })); + element.params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + }; + element._groupName = 'foo'; + return element.reload().then(() => { + flushAsynchronousOperations(); + const subsectionItems = dom(element.root) + .querySelectorAll('.subsectionItem'); + assert.equal(subsectionItems.length, 0); + assert.isFalse(element._groupIsInternal); + const selected = element.shadowRoot + .querySelector('gr-page-nav .selected'); + assert.isOk(selected); + assert.equal(selected.textContent.trim(), 'foo'); }); + }); - test('group list', () => { - element.params = { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - openCreateModal: false, - }; + test('group members', () => { + element.params = { + view: Gerrit.Nav.View.GROUP, + detail: Gerrit.Nav.GroupDetailView.MEMBERS, + groupId: 1234, + }; + element._groupName = 'foo'; + return element.reload().then(() => { flushAsynchronousOperations(); const selected = element.shadowRoot .querySelector('gr-page-nav .selected'); assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'Groups'); - }); - - test('internal group', () => { - element.params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - }; - element._groupName = 'foo'; - return element.reload().then(() => { - flushAsynchronousOperations(); - const subsectionItems = Polymer.dom(element.root) - .querySelectorAll('.subsectionItem'); - assert.equal(subsectionItems.length, 2); - assert.isTrue(element._groupIsInternal); - const selected = element.shadowRoot - .querySelector('gr-page-nav .selected'); - assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'foo'); - }); - }); - - test('external group', () => { - element.$.restAPI.getGroupConfig.restore(); - sandbox.stub(element.$.restAPI, 'getGroupConfig') - .returns(Promise.resolve({ - name: 'foo', - id: 'external-id', - })); - element.params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - }; - element._groupName = 'foo'; - return element.reload().then(() => { - flushAsynchronousOperations(); - const subsectionItems = Polymer.dom(element.root) - .querySelectorAll('.subsectionItem'); - assert.equal(subsectionItems.length, 0); - assert.isFalse(element._groupIsInternal); - const selected = element.shadowRoot - .querySelector('gr-page-nav .selected'); - assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'foo'); - }); - }); - - test('group members', () => { - element.params = { - view: Gerrit.Nav.View.GROUP, - detail: Gerrit.Nav.GroupDetailView.MEMBERS, - groupId: 1234, - }; - element._groupName = 'foo'; - return element.reload().then(() => { - flushAsynchronousOperations(); - const selected = element.shadowRoot - .querySelector('gr-page-nav .selected'); - assert.isOk(selected); - assert.equal(selected.textContent.trim(), 'Members'); - }); + assert.equal(selected.textContent.trim(), 'Members'); }); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js index 3fde410..5ebf00e 100644 --- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js
@@ -14,67 +14,76 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DETAIL_TYPES = { - BRANCHES: 'branches', - ID: 'id', - TAGS: 'tags', - }; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-delete-item-dialog_html.js'; + +const DETAIL_TYPES = { + BRANCHES: 'branches', + ID: 'id', + TAGS: 'tags', +}; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrConfirmDeleteItemDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-delete-item-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmDeleteItemDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-delete-item-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - item: String, - itemType: String, - }; - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', null, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } - - _computeItemName(detailType) { - if (detailType === DETAIL_TYPES.BRANCHES) { - return 'Branch'; - } else if (detailType === DETAIL_TYPES.TAGS) { - return 'Tag'; - } else if (detailType === DETAIL_TYPES.ID) { - return 'ID'; - } - } + static get properties() { + return { + item: String, + itemType: String, + }; } - customElements.define(GrConfirmDeleteItemDialog.is, - GrConfirmDeleteItemDialog); -})(); + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } + + _computeItemName(detailType) { + if (detailType === DETAIL_TYPES.BRANCHES) { + return 'Branch'; + } else if (detailType === DETAIL_TYPES.TAGS) { + return 'Tag'; + } else if (detailType === DETAIL_TYPES.ID) { + return 'ID'; + } + } +} + +customElements.define(GrConfirmDeleteItemDialog.is, + GrConfirmDeleteItemDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js index 9d8ee18..12dc29c 100644 --- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js +++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_html.js
@@ -1,38 +1,29 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-confirm-delete-item-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; width: 30em; } </style> - <gr-dialog - confirm-label="Delete [[_computeItemName(itemType)]]" - confirm-on-enter - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Delete [[_computeItemName(itemType)]]" confirm-on-enter="" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">[[_computeItemName(itemType)]] Deletion</div> <div class="main" slot="main"> <label for="branchInput"> @@ -43,6 +34,4 @@ </div> </div> </gr-dialog> - </template> - <script src="gr-confirm-delete-item-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html index b937e76..e948a31 100644 --- a/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-delete-item-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-delete-item-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-delete-item-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-delete-item-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,53 +40,55 @@ </template> </test-fixture> -<script> - suite('gr-confirm-delete-item-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-delete-item-dialog.js'; +suite('gr-confirm-delete-item-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('_handleConfirmTap', () => { - const confirmHandler = sandbox.stub(); - element.addEventListener('confirm', confirmHandler); - sandbox.spy(element, '_handleConfirmTap'); - element.shadowRoot - .querySelector('gr-dialog').fire('confirm'); - assert.isTrue(confirmHandler.called); - assert.isTrue(confirmHandler.calledOnce); - assert.isTrue(element._handleConfirmTap.called); - assert.isTrue(element._handleConfirmTap.calledOnce); - }); - - test('_handleCancelTap', () => { - const cancelHandler = sandbox.stub(); - element.addEventListener('cancel', cancelHandler); - sandbox.spy(element, '_handleCancelTap'); - element.shadowRoot - .querySelector('gr-dialog').fire('cancel'); - assert.isTrue(cancelHandler.called); - assert.isTrue(cancelHandler.calledOnce); - assert.isTrue(element._handleCancelTap.called); - assert.isTrue(element._handleCancelTap.calledOnce); - }); - - test('_computeItemName function for branches', () => { - assert.deepEqual(element._computeItemName('branches'), 'Branch'); - assert.notEqual(element._computeItemName('branches'), 'Tag'); - }); - - test('_computeItemName function for tags', () => { - assert.deepEqual(element._computeItemName('tags'), 'Tag'); - assert.notEqual(element._computeItemName('tags'), 'Branch'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('_handleConfirmTap', () => { + const confirmHandler = sandbox.stub(); + element.addEventListener('confirm', confirmHandler); + sandbox.spy(element, '_handleConfirmTap'); + element.shadowRoot + .querySelector('gr-dialog').fire('confirm'); + assert.isTrue(confirmHandler.called); + assert.isTrue(confirmHandler.calledOnce); + assert.isTrue(element._handleConfirmTap.called); + assert.isTrue(element._handleConfirmTap.calledOnce); + }); + + test('_handleCancelTap', () => { + const cancelHandler = sandbox.stub(); + element.addEventListener('cancel', cancelHandler); + sandbox.spy(element, '_handleCancelTap'); + element.shadowRoot + .querySelector('gr-dialog').fire('cancel'); + assert.isTrue(cancelHandler.called); + assert.isTrue(cancelHandler.calledOnce); + assert.isTrue(element._handleCancelTap.called); + assert.isTrue(element._handleCancelTap.calledOnce); + }); + + test('_computeItemName function for branches', () => { + assert.deepEqual(element._computeItemName('branches'), 'Branch'); + assert.notEqual(element._computeItemName('branches'), 'Tag'); + }); + + test('_computeItemName function for tags', () => { + assert.deepEqual(element._computeItemName('tags'), 'Tag'); + assert.notEqual(element._computeItemName('tags'), 'Branch'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js index 3b85304..94de228 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.js
@@ -14,148 +14,166 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; - const SUGGESTIONS_LIMIT = 15; - const REF_PREFIX = 'refs/heads/'; +import '@polymer/iron-input/iron-input.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-change-dialog_html.js'; +const SUGGESTIONS_LIMIT = 15; +const REF_PREFIX = 'refs/heads/'; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrCreateChangeDialog extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * Unused in this element, but called by other elements in tests + * e.g gr-repo-commands_test. */ - class GrCreateChangeDialog extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-repo-commands_test. - */ - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-create-change-dialog'; } + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - static get properties() { - return { - repoName: String, - branch: String, - /** @type {?} */ - _repoConfig: Object, - subject: String, - topic: String, - _query: { - type: Function, - value() { - return this._getRepoBranchesSuggestions.bind(this); - }, + static get is() { return 'gr-create-change-dialog'; } + + static get properties() { + return { + repoName: String, + branch: String, + /** @type {?} */ + _repoConfig: Object, + subject: String, + topic: String, + _query: { + type: Function, + value() { + return this._getRepoBranchesSuggestions.bind(this); }, - baseChange: String, - baseCommit: String, - privateByDefault: String, - canCreate: { - type: Boolean, - notify: true, - value: false, - }, - _privateChangesEnabled: Boolean, - }; + }, + baseChange: String, + baseCommit: String, + privateByDefault: String, + canCreate: { + type: Boolean, + notify: true, + value: false, + }, + _privateChangesEnabled: Boolean, + }; + } + + /** @override */ + attached() { + super.attached(); + if (!this.repoName) { return Promise.resolve(); } + + const promises = []; + + promises.push(this.$.restAPI.getProjectConfig(this.repoName) + .then(config => { + this.privateByDefault = config.private_by_default; + })); + + promises.push(this.$.restAPI.getConfig().then(config => { + if (!config) { return; } + + this._privateConfig = config && config.change && + config.change.disable_private_changes; + })); + + return Promise.all(promises); + } + + static get observers() { + return [ + '_allowCreate(branch, subject)', + ]; + } + + _computeBranchClass(baseChange) { + return baseChange ? 'hide' : ''; + } + + _allowCreate(branch, subject) { + this.canCreate = !!branch && !!subject; + } + + handleCreateChange() { + const isPrivate = this.$.privateChangeCheckBox.checked; + const isWip = true; + return this.$.restAPI.createChange(this.repoName, this.branch, + this.subject, this.topic, isPrivate, isWip, this.baseChange, + this.baseCommit || null) + .then(changeCreated => { + if (!changeCreated) { return; } + Gerrit.Nav.navigateToChange(changeCreated); + }); + } + + _getRepoBranchesSuggestions(input) { + if (input.startsWith(REF_PREFIX)) { + input = input.substring(REF_PREFIX.length); } - - /** @override */ - attached() { - super.attached(); - if (!this.repoName) { return Promise.resolve(); } - - const promises = []; - - promises.push(this.$.restAPI.getProjectConfig(this.repoName) - .then(config => { - this.privateByDefault = config.private_by_default; - })); - - promises.push(this.$.restAPI.getConfig().then(config => { - if (!config) { return; } - - this._privateConfig = config && config.change && - config.change.disable_private_changes; - })); - - return Promise.all(promises); - } - - static get observers() { - return [ - '_allowCreate(branch, subject)', - ]; - } - - _computeBranchClass(baseChange) { - return baseChange ? 'hide' : ''; - } - - _allowCreate(branch, subject) { - this.canCreate = !!branch && !!subject; - } - - handleCreateChange() { - const isPrivate = this.$.privateChangeCheckBox.checked; - const isWip = true; - return this.$.restAPI.createChange(this.repoName, this.branch, - this.subject, this.topic, isPrivate, isWip, this.baseChange, - this.baseCommit || null) - .then(changeCreated => { - if (!changeCreated) { return; } - Gerrit.Nav.navigateToChange(changeCreated); - }); - } - - _getRepoBranchesSuggestions(input) { - if (input.startsWith(REF_PREFIX)) { - input = input.substring(REF_PREFIX.length); - } - return this.$.restAPI.getRepoBranches( - input, this.repoName, SUGGESTIONS_LIMIT).then(response => { - const branches = []; - let branch; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - if (response[key].ref.startsWith('refs/heads/')) { - branch = response[key].ref.substring('refs/heads/'.length); - } else { - branch = response[key].ref; - } - branches.push({ - name: branch, - }); - } - return branches; - }); - } - - _formatBooleanString(config) { - if (config && config.configured_value === 'TRUE') { - return true; - } else if (config && config.configured_value === 'FALSE') { - return false; - } else if (config && config.configured_value === 'INHERIT') { - if (config && config.inherited_value) { - return true; + return this.$.restAPI.getRepoBranches( + input, this.repoName, SUGGESTIONS_LIMIT).then(response => { + const branches = []; + let branch; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + if (response[key].ref.startsWith('refs/heads/')) { + branch = response[key].ref.substring('refs/heads/'.length); } else { - return false; + branch = response[key].ref; } + branches.push({ + name: branch, + }); + } + return branches; + }); + } + + _formatBooleanString(config) { + if (config && config.configured_value === 'TRUE') { + return true; + } else if (config && config.configured_value === 'FALSE') { + return false; + } else if (config && config.configured_value === 'INHERIT') { + if (config && config.inherited_value) { + return true; } else { return false; } - } - - _computePrivateSectionClass(config) { - return config ? 'hide' : ''; + } else { + return false; } } - customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog); -})(); + _computePrivateSectionClass(config) { + return config ? 'hide' : ''; + } +} + +customElements.define(GrCreateChangeDialog.is, GrCreateChangeDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js index 1d6e706..8f11af3 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js +++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_html.js
@@ -1,36 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<dom-module id="gr-create-change-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -53,76 +39,42 @@ } </style> <div class="gr-form-styles"> - <section class$="[[_computeBranchClass(baseChange)]]"> + <section class\$="[[_computeBranchClass(baseChange)]]"> <span class="title">Select branch for new change</span> <span class="value"> - <gr-autocomplete - id="branchInput" - text="{{branch}}" - query="[[_query]]" - placeholder="Destination branch"> + <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch"> </gr-autocomplete> </span> </section> - <section class$="[[_computeBranchClass(baseChange)]]"> + <section class\$="[[_computeBranchClass(baseChange)]]"> <span class="title">Provide base commit sha1 for change</span> <span class="value"> - <iron-input - maxlength="40" - placeholder="(optional)" - bind-value="{{baseCommit}}"> - <input - is="iron-input" - id="baseCommitInput" - maxlength="40" - placeholder="(optional)" - bind-value="{{baseCommit}}"> + <iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}"> + <input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}"> </iron-input> </span> </section> <section> <span class="title">Enter topic for new change</span> <span class="value"> - <iron-input - maxlength="1024" - placeholder="(optional)" - bind-value="{{topic}}"> - <input - is="iron-input" - id="tagNameInput" - maxlength="1024" - placeholder="(optional)" - bind-value="{{topic}}"> + <iron-input maxlength="1024" placeholder="(optional)" bind-value="{{topic}}"> + <input is="iron-input" id="tagNameInput" maxlength="1024" placeholder="(optional)" bind-value="{{topic}}"> </iron-input> </span> </section> <section id="description"> <span class="title">Description</span> <span class="value"> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - rows="4" - max-rows="15" - bind-value="{{subject}}" - placeholder="Insert the description of the change."> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{subject}}" placeholder="Insert the description of the change."> </iron-autogrow-textarea> </span> </section> - <section class$="[[_computePrivateSectionClass(_privateChangesEnabled)]]"> - <label - class="title" - for="privateChangeCheckBox">Private change</label> + <section class\$="[[_computePrivateSectionClass(_privateChangesEnabled)]]"> + <label class="title" for="privateChangeCheckBox">Private change</label> <span class="value"> - <input - type="checkbox" - id="privateChangeCheckBox" - checked$="[[_formatBooleanString(privateByDefault)]]"> + <input type="checkbox" id="privateChangeCheckBox" checked\$="[[_formatBooleanString(privateByDefault)]]"> </span> </section> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-create-change-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html index aad2428f..20226dd 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-change-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-create-change-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-create-change-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-change-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,136 +40,138 @@ </template> </test-fixture> -<script> - suite('gr-create-change-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-change-dialog.js'; +suite('gr-create-change-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getRepoBranches(input) { - if (input.startsWith('test')) { - return Promise.resolve([ - { - ref: 'refs/heads/test-branch', - revision: '67ebf73496383c6777035e374d2d664009e2aa5c', - can_delete: true, - }, - ]); - } else { - return Promise.resolve({}); - } - }, - }); - element = fixture('basic'); - element.repoName = 'test-repo', - element._repoConfig = { - private_by_default: { - configured_value: 'FALSE', - inherited_value: false, - }, - }; + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getRepoBranches(input) { + if (input.startsWith('test')) { + return Promise.resolve([ + { + ref: 'refs/heads/test-branch', + revision: '67ebf73496383c6777035e374d2d664009e2aa5c', + can_delete: true, + }, + ]); + } else { + return Promise.resolve({}); + } + }, }); - - teardown(() => { - sandbox.restore(); - }); - - test('new change created with default', done => { - const configInputObj = { - branch: 'test-branch', - subject: 'first change created with polygerrit ui', - topic: 'test-topic', - is_private: false, - work_in_progress: true, - }; - - const saveStub = sandbox.stub(element.$.restAPI, - 'createChange', () => Promise.resolve({})); - - element.branch = 'test-branch'; - element.topic = 'test-topic'; - element.subject = 'first change created with polygerrit ui'; - assert.isFalse(element.$.privateChangeCheckBox.checked); - - element.$.branchInput.bindValue = configInputObj.branch; - element.$.tagNameInput.bindValue = configInputObj.topic; - element.$.messageInput.bindValue = configInputObj.subject; - - element.handleCreateChange().then(() => { - // Private change - assert.isFalse(saveStub.lastCall.args[4]); - // WIP Change - assert.isTrue(saveStub.lastCall.args[5]); - assert.isTrue(saveStub.called); - done(); - }); - }); - - test('new change created with private', done => { - element.privateByDefault = { - configured_value: 'TRUE', + element = fixture('basic'); + element.repoName = 'test-repo', + element._repoConfig = { + private_by_default: { + configured_value: 'FALSE', inherited_value: false, - }; - sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true)); - flushAsynchronousOperations(); + }, + }; + }); - const configInputObj = { - branch: 'test-branch', - subject: 'first change created with polygerrit ui', - topic: 'test-topic', - is_private: true, - work_in_progress: true, - }; + teardown(() => { + sandbox.restore(); + }); - const saveStub = sandbox.stub(element.$.restAPI, - 'createChange', () => Promise.resolve({})); + test('new change created with default', done => { + const configInputObj = { + branch: 'test-branch', + subject: 'first change created with polygerrit ui', + topic: 'test-topic', + is_private: false, + work_in_progress: true, + }; - element.branch = 'test-branch'; - element.topic = 'test-topic'; - element.subject = 'first change created with polygerrit ui'; - assert.isTrue(element.$.privateChangeCheckBox.checked); + const saveStub = sandbox.stub(element.$.restAPI, + 'createChange', () => Promise.resolve({})); - element.$.branchInput.bindValue = configInputObj.branch; - element.$.tagNameInput.bindValue = configInputObj.topic; - element.$.messageInput.bindValue = configInputObj.subject; + element.branch = 'test-branch'; + element.topic = 'test-topic'; + element.subject = 'first change created with polygerrit ui'; + assert.isFalse(element.$.privateChangeCheckBox.checked); - element.handleCreateChange().then(() => { - // Private change - assert.isTrue(saveStub.lastCall.args[4]); - // WIP Change - assert.isTrue(saveStub.lastCall.args[5]); - assert.isTrue(saveStub.called); - done(); - }); - }); + element.$.branchInput.bindValue = configInputObj.branch; + element.$.tagNameInput.bindValue = configInputObj.topic; + element.$.messageInput.bindValue = configInputObj.subject; - test('_getRepoBranchesSuggestions empty', done => { - element._getRepoBranchesSuggestions('nonexistent').then(branches => { - assert.equal(branches.length, 0); - done(); - }); - }); - - test('_getRepoBranchesSuggestions non-empty', done => { - element._getRepoBranchesSuggestions('test-branch').then(branches => { - assert.equal(branches.length, 1); - assert.equal(branches[0].name, 'test-branch'); - done(); - }); - }); - - test('_computeBranchClass', () => { - assert.equal(element._computeBranchClass(true), 'hide'); - assert.equal(element._computeBranchClass(false), ''); - }); - - test('_computePrivateSectionClass', () => { - assert.equal(element._computePrivateSectionClass(true), 'hide'); - assert.equal(element._computePrivateSectionClass(false), ''); + element.handleCreateChange().then(() => { + // Private change + assert.isFalse(saveStub.lastCall.args[4]); + // WIP Change + assert.isTrue(saveStub.lastCall.args[5]); + assert.isTrue(saveStub.called); + done(); }); }); + + test('new change created with private', done => { + element.privateByDefault = { + configured_value: 'TRUE', + inherited_value: false, + }; + sandbox.stub(element, '_formatBooleanString', () => Promise.resolve(true)); + flushAsynchronousOperations(); + + const configInputObj = { + branch: 'test-branch', + subject: 'first change created with polygerrit ui', + topic: 'test-topic', + is_private: true, + work_in_progress: true, + }; + + const saveStub = sandbox.stub(element.$.restAPI, + 'createChange', () => Promise.resolve({})); + + element.branch = 'test-branch'; + element.topic = 'test-topic'; + element.subject = 'first change created with polygerrit ui'; + assert.isTrue(element.$.privateChangeCheckBox.checked); + + element.$.branchInput.bindValue = configInputObj.branch; + element.$.tagNameInput.bindValue = configInputObj.topic; + element.$.messageInput.bindValue = configInputObj.subject; + + element.handleCreateChange().then(() => { + // Private change + assert.isTrue(saveStub.lastCall.args[4]); + // WIP Change + assert.isTrue(saveStub.lastCall.args[5]); + assert.isTrue(saveStub.called); + done(); + }); + }); + + test('_getRepoBranchesSuggestions empty', done => { + element._getRepoBranchesSuggestions('nonexistent').then(branches => { + assert.equal(branches.length, 0); + done(); + }); + }); + + test('_getRepoBranchesSuggestions non-empty', done => { + element._getRepoBranchesSuggestions('test-branch').then(branches => { + assert.equal(branches.length, 1); + assert.equal(branches[0].name, 'test-branch'); + done(); + }); + }); + + test('_computeBranchClass', () => { + assert.equal(element._computeBranchClass(true), 'hide'); + assert.equal(element._computeBranchClass(false), ''); + }); + + test('_computePrivateSectionClass', () => { + assert.equal(element._computePrivateSectionClass(true), 'hide'); + assert.equal(element._computePrivateSectionClass(false), ''); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js index 8a4edab..0860fdb 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog.js
@@ -14,65 +14,77 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrCreateGroupDialog extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-create-group-dialog'; } +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-group-dialog_html.js'; - static get properties() { - return { - params: Object, - hasNewGroupName: { - type: Boolean, - notify: true, - value: false, - }, - _name: Object, - _groupCreated: { - type: Boolean, - value: false, - }, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrCreateGroupDialog extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - static get observers() { - return [ - '_updateGroupName(_name)', - ]; - } + static get is() { return 'gr-create-group-dialog'; } - _computeGroupUrl(groupId) { - return this.getBaseUrl() + '/admin/groups/' + - this.encodeURL(groupId, true); - } - - _updateGroupName(name) { - this.hasNewGroupName = !!name; - } - - handleCreateGroup() { - return this.$.restAPI.createGroup({name: this._name}) - .then(groupRegistered => { - if (groupRegistered.status !== 201) { return; } - this._groupCreated = true; - return this.$.restAPI.getGroupConfig(this._name) - .then(group => { - page.show(this._computeGroupUrl(group.group_id)); - }); - }); - } + static get properties() { + return { + params: Object, + hasNewGroupName: { + type: Boolean, + notify: true, + value: false, + }, + _name: Object, + _groupCreated: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog); -})(); + static get observers() { + return [ + '_updateGroupName(_name)', + ]; + } + + _computeGroupUrl(groupId) { + return this.getBaseUrl() + '/admin/groups/' + + this.encodeURL(groupId, true); + } + + _updateGroupName(name) { + this.hasNewGroupName = !!name; + } + + handleCreateGroup() { + return this.$.restAPI.createGroup({name: this._name}) + .then(groupRegistered => { + if (groupRegistered.status !== 201) { return; } + this._groupCreated = true; + return this.$.restAPI.getGroupConfig(this._name) + .then(group => { + page.show(this._computeGroupUrl(group.group_id)); + }); + }); + } +} + +customElements.define(GrCreateGroupDialog.is, GrCreateGroupDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js index d0a1fca..2cdde81 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js +++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-create-group-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -41,16 +32,11 @@ <div id="form"> <section> <span class="title">Group name</span> - <iron-input - bind-value="{{_name}}"> - <input - is="iron-input" - bind-value="{{_name}}"> + <iron-input bind-value="{{_name}}"> + <input is="iron-input" bind-value="{{_name}}"> </iron-input> </section> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-create-group-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html index d630556..e9585d3 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-group-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-create-group-dialog.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-create-group-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-group-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,66 +41,68 @@ </template> </test-fixture> -<script> - suite('gr-create-group-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; - const GROUP_NAME = 'test-group'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-group-dialog.js'; +suite('gr-create-group-dialog tests', () => { + let element; + let sandbox; + const GROUP_NAME = 'test-group'; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, }); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('name is updated correctly', done => { - assert.isFalse(element.hasNewGroupName); + test('name is updated correctly', done => { + assert.isFalse(element.hasNewGroupName); - const inputEl = element.root.querySelector('iron-input'); - inputEl.bindValue = GROUP_NAME; + const inputEl = element.root.querySelector('iron-input'); + inputEl.bindValue = GROUP_NAME; - setTimeout(() => { - assert.isTrue(element.hasNewGroupName); - assert.deepEqual(element._name, GROUP_NAME); - done(); - }); - }); - - test('test for redirecting to group on successful creation', done => { - sandbox.stub(element.$.restAPI, 'createGroup') - .returns(Promise.resolve({status: 201})); - - sandbox.stub(element.$.restAPI, 'getGroupConfig') - .returns(Promise.resolve({group_id: 551})); - - const showStub = sandbox.stub(page, 'show'); - element.handleCreateGroup() - .then(() => { - assert.isTrue(showStub.calledWith('/admin/groups/551')); - done(); - }); - }); - - test('test for unsuccessful group creation', done => { - sandbox.stub(element.$.restAPI, 'createGroup') - .returns(Promise.resolve({status: 409})); - - sandbox.stub(element.$.restAPI, 'getGroupConfig') - .returns(Promise.resolve({group_id: 551})); - - const showStub = sandbox.stub(page, 'show'); - element.handleCreateGroup() - .then(() => { - assert.isFalse(showStub.called); - done(); - }); + setTimeout(() => { + assert.isTrue(element.hasNewGroupName); + assert.deepEqual(element._name, GROUP_NAME); + done(); }); }); + + test('test for redirecting to group on successful creation', done => { + sandbox.stub(element.$.restAPI, 'createGroup') + .returns(Promise.resolve({status: 201})); + + sandbox.stub(element.$.restAPI, 'getGroupConfig') + .returns(Promise.resolve({group_id: 551})); + + const showStub = sandbox.stub(page, 'show'); + element.handleCreateGroup() + .then(() => { + assert.isTrue(showStub.calledWith('/admin/groups/551')); + done(); + }); + }); + + test('test for unsuccessful group creation', done => { + sandbox.stub(element.$.restAPI, 'createGroup') + .returns(Promise.resolve({status: 409})); + + sandbox.stub(element.$.restAPI, 'getGroupConfig') + .returns(Promise.resolve({group_id: 551})); + + const showStub = sandbox.stub(page, 'show'); + element.handleCreateGroup() + .then(() => { + assert.isFalse(showStub.called); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js index 2d6b4aa..40ddb66 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog.js
@@ -14,89 +14,103 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DETAIL_TYPES = { - branches: 'branches', - tags: 'tags', - }; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-pointer-dialog_html.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrCreatePointerDialog extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-create-pointer-dialog'; } +const DETAIL_TYPES = { + branches: 'branches', + tags: 'tags', +}; - static get properties() { - return { - detailType: String, - repoName: String, - hasNewItemName: { - type: Boolean, - notify: true, - value: false, - }, - itemDetail: String, - _itemName: String, - _itemRevision: String, - _itemAnnotation: String, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrCreatePointerDialog extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - static get observers() { - return [ - '_updateItemName(_itemName)', - ]; - } + static get is() { return 'gr-create-pointer-dialog'; } - _updateItemName(name) { - this.hasNewItemName = !!name; - } + static get properties() { + return { + detailType: String, + repoName: String, + hasNewItemName: { + type: Boolean, + notify: true, + value: false, + }, + itemDetail: String, + _itemName: String, + _itemRevision: String, + _itemAnnotation: String, + }; + } - _computeItemUrl(project) { - if (this.itemDetail === DETAIL_TYPES.branches) { - return this.getBaseUrl() + '/admin/repos/' + - this.encodeURL(this.repoName, true) + ',branches'; - } else if (this.itemDetail === DETAIL_TYPES.tags) { - return this.getBaseUrl() + '/admin/repos/' + - this.encodeURL(this.repoName, true) + ',tags'; - } - } + static get observers() { + return [ + '_updateItemName(_itemName)', + ]; + } - handleCreateItem() { - const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD'; - if (this.itemDetail === DETAIL_TYPES.branches) { - return this.$.restAPI.createRepoBranch(this.repoName, - this._itemName, {revision: USE_HEAD}) - .then(itemRegistered => { - if (itemRegistered.status === 201) { - page.show(this._computeItemUrl(this.itemDetail)); - } - }); - } else if (this.itemDetail === DETAIL_TYPES.tags) { - return this.$.restAPI.createRepoTag(this.repoName, - this._itemName, - {revision: USE_HEAD, message: this._itemAnnotation || null}) - .then(itemRegistered => { - if (itemRegistered.status === 201) { - page.show(this._computeItemUrl(this.itemDetail)); - } - }); - } - } + _updateItemName(name) { + this.hasNewItemName = !!name; + } - _computeHideItemClass(type) { - return type === DETAIL_TYPES.branches ? 'hideItem' : ''; + _computeItemUrl(project) { + if (this.itemDetail === DETAIL_TYPES.branches) { + return this.getBaseUrl() + '/admin/repos/' + + this.encodeURL(this.repoName, true) + ',branches'; + } else if (this.itemDetail === DETAIL_TYPES.tags) { + return this.getBaseUrl() + '/admin/repos/' + + this.encodeURL(this.repoName, true) + ',tags'; } } - customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog); -})(); + handleCreateItem() { + const USE_HEAD = this._itemRevision ? this._itemRevision : 'HEAD'; + if (this.itemDetail === DETAIL_TYPES.branches) { + return this.$.restAPI.createRepoBranch(this.repoName, + this._itemName, {revision: USE_HEAD}) + .then(itemRegistered => { + if (itemRegistered.status === 201) { + page.show(this._computeItemUrl(this.itemDetail)); + } + }); + } else if (this.itemDetail === DETAIL_TYPES.tags) { + return this.$.restAPI.createRepoTag(this.repoName, + this._itemName, + {revision: USE_HEAD, message: this._itemAnnotation || null}) + .then(itemRegistered => { + if (itemRegistered.status === 201) { + page.show(this._computeItemUrl(this.itemDetail)); + } + }); + } + } + + _computeHideItemClass(type) { + return type === DETAIL_TYPES.branches ? 'hideItem' : ''; + } +} + +customElements.define(GrCreatePointerDialog.is, GrCreatePointerDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js index d1980a5..3a6df2f 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js +++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_html.js
@@ -1,33 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<dom-module id="gr-create-pointer-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -49,41 +38,23 @@ <div id="form"> <section id="itemNameSection"> <span class="title">[[detailType]] name</span> - <iron-input - placeholder="[[detailType]] Name" - bind-value="{{_itemName}}"> - <input - is="iron-input" - placeholder="[[detailType]] Name" - bind-value="{{_itemName}}"> + <iron-input placeholder="[[detailType]] Name" bind-value="{{_itemName}}"> + <input is="iron-input" placeholder="[[detailType]] Name" bind-value="{{_itemName}}"> </iron-input> </section> <section id="itemRevisionSection"> <span class="title">Initial Revision</span> - <iron-input - placeholder="Revision (Branch or SHA-1)" - bind-value="{{_itemRevision}}"> - <input - is="iron-input" - placeholder="Revision (Branch or SHA-1)" - bind-value="{{_itemRevision}}"> + <iron-input placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}"> + <input is="iron-input" placeholder="Revision (Branch or SHA-1)" bind-value="{{_itemRevision}}"> </iron-input> </section> - <section id="itemAnnotationSection" - class$="[[_computeHideItemClass(itemDetail)]]"> + <section id="itemAnnotationSection" class\$="[[_computeHideItemClass(itemDetail)]]"> <span class="title">Annotation</span> - <iron-input - placeholder="Annotation (Optional)" - bind-value="{{_itemAnnotation}}"> - <input - is="iron-input" - placeholder="Annotation (Optional)" - bind-value="{{_itemAnnotation}}"> + <iron-input placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}"> + <input is="iron-input" placeholder="Annotation (Optional)" bind-value="{{_itemAnnotation}}"> </iron-input> </section> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-create-pointer-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html index db33587..28cf2e8 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-pointer-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-create-pointer-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-create-pointer-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-pointer-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,103 +40,106 @@ </template> </test-fixture> -<script> - suite('gr-create-pointer-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-pointer-dialog.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-create-pointer-dialog tests', () => { + let element; + let sandbox; - const ironInput = function(element) { - return Polymer.dom(element).querySelector('iron-input'); - }; + const ironInput = function(element) { + return dom(element).querySelector('iron-input'); + }; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, }); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('branch created', done => { - sandbox.stub( - element.$.restAPI, - 'createRepoBranch', - () => Promise.resolve({})); + test('branch created', done => { + sandbox.stub( + element.$.restAPI, + 'createRepoBranch', + () => Promise.resolve({})); - assert.isFalse(element.hasNewItemName); + assert.isFalse(element.hasNewItemName); - element._itemName = 'test-branch'; - element.itemDetail = 'branches'; + element._itemName = 'test-branch'; + element.itemDetail = 'branches'; - ironInput(element.$.itemNameSection).bindValue = 'test-branch2'; - ironInput(element.$.itemRevisionSection).bindValue = 'HEAD'; + ironInput(element.$.itemNameSection).bindValue = 'test-branch2'; + ironInput(element.$.itemRevisionSection).bindValue = 'HEAD'; - setTimeout(() => { - assert.isTrue(element.hasNewItemName); - assert.equal(element._itemName, 'test-branch2'); - assert.equal(element._itemRevision, 'HEAD'); - done(); - }); - }); - - test('tag created', done => { - sandbox.stub( - element.$.restAPI, - 'createRepoTag', - () => Promise.resolve({})); - - assert.isFalse(element.hasNewItemName); - - element._itemName = 'test-tag'; - element.itemDetail = 'tags'; - - ironInput(element.$.itemNameSection).bindValue = 'test-tag2'; - ironInput(element.$.itemRevisionSection).bindValue = 'HEAD'; - - setTimeout(() => { - assert.isTrue(element.hasNewItemName); - assert.equal(element._itemName, 'test-tag2'); - assert.equal(element._itemRevision, 'HEAD'); - done(); - }); - }); - - test('tag created with annotations', done => { - sandbox.stub( - element.$.restAPI, - 'createRepoTag', - () => Promise.resolve({})); - - assert.isFalse(element.hasNewItemName); - - element._itemName = 'test-tag'; - element._itemAnnotation = 'test-message'; - element.itemDetail = 'tags'; - - ironInput(element.$.itemNameSection).bindValue = 'test-tag2'; - ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2'; - ironInput(element.$.itemRevisionSection).bindValue = 'HEAD'; - - setTimeout(() => { - assert.isTrue(element.hasNewItemName); - assert.equal(element._itemName, 'test-tag2'); - assert.equal(element._itemAnnotation, 'test-message2'); - assert.equal(element._itemRevision, 'HEAD'); - done(); - }); - }); - - test('_computeHideItemClass returns hideItem if type is branches', () => { - assert.equal(element._computeHideItemClass('branches'), 'hideItem'); - }); - - test('_computeHideItemClass returns strings if not branches', () => { - assert.equal(element._computeHideItemClass('tags'), ''); + setTimeout(() => { + assert.isTrue(element.hasNewItemName); + assert.equal(element._itemName, 'test-branch2'); + assert.equal(element._itemRevision, 'HEAD'); + done(); }); }); + + test('tag created', done => { + sandbox.stub( + element.$.restAPI, + 'createRepoTag', + () => Promise.resolve({})); + + assert.isFalse(element.hasNewItemName); + + element._itemName = 'test-tag'; + element.itemDetail = 'tags'; + + ironInput(element.$.itemNameSection).bindValue = 'test-tag2'; + ironInput(element.$.itemRevisionSection).bindValue = 'HEAD'; + + setTimeout(() => { + assert.isTrue(element.hasNewItemName); + assert.equal(element._itemName, 'test-tag2'); + assert.equal(element._itemRevision, 'HEAD'); + done(); + }); + }); + + test('tag created with annotations', done => { + sandbox.stub( + element.$.restAPI, + 'createRepoTag', + () => Promise.resolve({})); + + assert.isFalse(element.hasNewItemName); + + element._itemName = 'test-tag'; + element._itemAnnotation = 'test-message'; + element.itemDetail = 'tags'; + + ironInput(element.$.itemNameSection).bindValue = 'test-tag2'; + ironInput(element.$.itemAnnotationSection).bindValue = 'test-message2'; + ironInput(element.$.itemRevisionSection).bindValue = 'HEAD'; + + setTimeout(() => { + assert.isTrue(element.hasNewItemName); + assert.equal(element._itemName, 'test-tag2'); + assert.equal(element._itemAnnotation, 'test-message2'); + assert.equal(element._itemRevision, 'HEAD'); + done(); + }); + }); + + test('_computeHideItemClass returns hideItem if type is branches', () => { + assert.equal(element._computeHideItemClass('branches'), 'hideItem'); + }); + + test('_computeHideItemClass returns strings if not branches', () => { + assert.equal(element._computeHideItemClass('tags'), ''); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js index 290f025..7a77874 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js +++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog.js
@@ -14,130 +14,145 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrCreateRepoDialog extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-create-repo-dialog'; } +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-repo-dialog_html.js'; - static get properties() { - return { - params: Object, - hasNewRepoName: { - type: Boolean, - notify: true, - value: false, +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrCreateRepoDialog extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-create-repo-dialog'; } + + static get properties() { + return { + params: Object, + hasNewRepoName: { + type: Boolean, + notify: true, + value: false, + }, + + /** @type {?} */ + _repoConfig: { + type: Object, + value: () => { + // Set default values for dropdowns. + return { + create_empty_commit: true, + permissions_only: false, + }; }, + }, + _repoCreated: { + type: Boolean, + value: false, + }, + _repoOwner: String, + _repoOwnerId: { + type: String, + observer: '_repoOwnerIdUpdate', + }, - /** @type {?} */ - _repoConfig: { - type: Object, - value: () => { - // Set default values for dropdowns. - return { - create_empty_commit: true, - permissions_only: false, - }; - }, + _query: { + type: Function, + value() { + return this._getRepoSuggestions.bind(this); }, - _repoCreated: { - type: Boolean, - value: false, + }, + _queryGroups: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); }, - _repoOwner: String, - _repoOwnerId: { - type: String, - observer: '_repoOwnerIdUpdate', - }, + }, + }; + } - _query: { - type: Function, - value() { - return this._getRepoSuggestions.bind(this); - }, - }, - _queryGroups: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); - }, - }, - }; - } + static get observers() { + return [ + '_updateRepoName(_repoConfig.name)', + ]; + } - static get observers() { - return [ - '_updateRepoName(_repoConfig.name)', - ]; - } + _computeRepoUrl(repoName) { + return this.getBaseUrl() + '/admin/repos/' + + this.encodeURL(repoName, true); + } - _computeRepoUrl(repoName) { - return this.getBaseUrl() + '/admin/repos/' + - this.encodeURL(repoName, true); - } + _updateRepoName(name) { + this.hasNewRepoName = !!name; + } - _updateRepoName(name) { - this.hasNewRepoName = !!name; - } - - _repoOwnerIdUpdate(id) { - if (id) { - this.set('_repoConfig.owners', [id]); - } else { - this.set('_repoConfig.owners', undefined); - } - } - - handleCreateRepo() { - return this.$.restAPI.createRepo(this._repoConfig) - .then(repoRegistered => { - if (repoRegistered.status === 201) { - this._repoCreated = true; - page.show(this._computeRepoUrl(this._repoConfig.name)); - } - }); - } - - _getRepoSuggestions(input) { - return this.$.restAPI.getSuggestedProjects(input) - .then(response => { - const repos = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - repos.push({ - name: key, - value: response[key], - }); - } - return repos; - }); - } - - _getGroupSuggestions(input) { - return this.$.restAPI.getSuggestedGroups(input) - .then(response => { - const groups = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - groups.push({ - name: key, - value: decodeURIComponent(response[key].id), - }); - } - return groups; - }); + _repoOwnerIdUpdate(id) { + if (id) { + this.set('_repoConfig.owners', [id]); + } else { + this.set('_repoConfig.owners', undefined); } } - customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog); -})(); + handleCreateRepo() { + return this.$.restAPI.createRepo(this._repoConfig) + .then(repoRegistered => { + if (repoRegistered.status === 201) { + this._repoCreated = true; + page.show(this._computeRepoUrl(this._repoConfig.name)); + } + }); + } + + _getRepoSuggestions(input) { + return this.$.restAPI.getSuggestedProjects(input) + .then(response => { + const repos = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + repos.push({ + name: key, + value: response[key], + }); + } + return repos; + }); + } + + _getGroupSuggestions(input) { + return this.$.restAPI.getSuggestedGroups(input) + .then(response => { + const groups = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + groups.push({ + name: key, + value: decodeURIComponent(response[key].id), + }); + } + return groups; + }); + } +} + +customElements.define(GrCreateRepoDialog.is, GrCreateRepoDialog);
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js index b78090c..65666ea 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js +++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<dom-module id="gr-create-repo-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -48,42 +36,28 @@ <div id="form"> <section> <span class="title">Repository name</span> - <iron-input autocomplete="on" - bind-value="{{_repoConfig.name}}"> - <input is="iron-input" - id="repoNameInput" - autocomplete="on" - bind-value="{{_repoConfig.name}}"> + <iron-input autocomplete="on" bind-value="{{_repoConfig.name}}"> + <input is="iron-input" id="repoNameInput" autocomplete="on" bind-value="{{_repoConfig.name}}"> </iron-input> </section> <section> <span class="title">Rights inherit from</span> <span class="value"> - <gr-autocomplete - id="rightsInheritFromInput" - text="{{_repoConfig.parent}}" - query="[[_query]]" - placeholder="Optional, defaults to 'All-Projects'"> + <gr-autocomplete id="rightsInheritFromInput" text="{{_repoConfig.parent}}" query="[[_query]]" placeholder="Optional, defaults to 'All-Projects'"> </gr-autocomplete> </span> </section> <section> <span class="title">Owner</span> <span class="value"> - <gr-autocomplete - id="ownerInput" - text="{{_repoOwner}}" - value="{{_repoOwnerId}}" - query="[[_queryGroups]]"> + <gr-autocomplete id="ownerInput" text="{{_repoOwner}}" value="{{_repoOwnerId}}" query="[[_queryGroups]]"> </gr-autocomplete> </span> </section> <section> <span class="title">Create initial empty commit</span> <span class="value"> - <gr-select - id="initialCommit" - bind-value="{{_repoConfig.create_empty_commit}}"> + <gr-select id="initialCommit" bind-value="{{_repoConfig.create_empty_commit}}"> <select> <option value="false">False</option> <option value="true">True</option> @@ -94,9 +68,7 @@ <section> <span class="title">Only serve as parent for other repositories</span> <span class="value"> - <gr-select - id="parentRepo" - bind-value="{{_repoConfig.permissions_only}}"> + <gr-select id="parentRepo" bind-value="{{_repoConfig.permissions_only}}"> <select> <option value="false">False</option> <option value="true">True</option> @@ -107,6 +79,4 @@ </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-create-repo-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html index 578c074..09bb63e 100644 --- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html +++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-repo-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-create-repo-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-create-repo-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-repo-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,74 +40,76 @@ </template> </test-fixture> -<script> - suite('gr-create-repo-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-repo-dialog.js'; +suite('gr-create-repo-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, }); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('default values are populated', () => { - assert.isTrue(element.$.initialCommit.bindValue); - assert.isFalse(element.$.parentRepo.bindValue); - }); + test('default values are populated', () => { + assert.isTrue(element.$.initialCommit.bindValue); + assert.isFalse(element.$.parentRepo.bindValue); + }); - test('repo created', done => { - const configInputObj = { - name: 'test-repo', - create_empty_commit: true, - parent: 'All-Project', - permissions_only: false, - owners: ['testId'], - }; + test('repo created', done => { + const configInputObj = { + name: 'test-repo', + create_empty_commit: true, + parent: 'All-Project', + permissions_only: false, + owners: ['testId'], + }; - const saveStub = sandbox.stub(element.$.restAPI, - 'createRepo', () => Promise.resolve({})); + const saveStub = sandbox.stub(element.$.restAPI, + 'createRepo', () => Promise.resolve({})); - assert.isFalse(element.hasNewRepoName); + assert.isFalse(element.hasNewRepoName); - element._repoConfig = { - name: 'test-repo', - create_empty_commit: true, - parent: 'All-Project', - permissions_only: false, - }; + element._repoConfig = { + name: 'test-repo', + create_empty_commit: true, + parent: 'All-Project', + permissions_only: false, + }; - element._repoOwner = 'test'; - element._repoOwnerId = 'testId'; + element._repoOwner = 'test'; + element._repoOwnerId = 'testId'; - element.$.repoNameInput.bindValue = configInputObj.name; - element.$.rightsInheritFromInput.bindValue = configInputObj.parent; - element.$.ownerInput.text = configInputObj.owners[0]; - element.$.initialCommit.bindValue = - configInputObj.create_empty_commit; - element.$.parentRepo.bindValue = - configInputObj.permissions_only; + element.$.repoNameInput.bindValue = configInputObj.name; + element.$.rightsInheritFromInput.bindValue = configInputObj.parent; + element.$.ownerInput.text = configInputObj.owners[0]; + element.$.initialCommit.bindValue = + configInputObj.create_empty_commit; + element.$.parentRepo.bindValue = + configInputObj.permissions_only; - assert.isTrue(element.hasNewRepoName); + assert.isTrue(element.hasNewRepoName); - assert.deepEqual(element._repoConfig, configInputObj); + assert.deepEqual(element._repoConfig, configInputObj); - element.handleCreateRepo().then(() => { - assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj)); - done(); - }); - }); - - test('testing observer of _repoOwner', () => { - element._repoOwnerId = 'test-5'; - assert.deepEqual(element._repoConfig.owners, ['test-5']); + element.handleCreateRepo().then(() => { + assert.isTrue(saveStub.lastCall.calledWithExactly(configInputObj)); + done(); }); }); + + test('testing observer of _repoOwner', () => { + element._repoOwnerId = 'test-5'; + assert.deepEqual(element._repoConfig.owners, ['test-5']); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js index 11517d6..81c9cde 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js +++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log.js
@@ -14,113 +14,127 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js'; - const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP']; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-account-link/gr-account-link.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-group-audit-log_html.js'; - /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.ListViewMixin - * @extends Polymer.Element - */ - class GrGroupAuditLog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.ListViewBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-group-audit-log'; } +const GROUP_EVENTS = ['ADD_GROUP', 'REMOVE_GROUP']; - static get properties() { - return { - groupId: String, - _auditLog: Array, - _loading: { - type: Boolean, - value: true, - }, - }; - } +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.ListViewMixin + * @extends Polymer.Element + */ +class GrGroupAuditLog extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.ListViewBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this.fire('title-change', {title: 'Audit Log'}); - } + static get is() { return 'gr-group-audit-log'; } - /** @override */ - ready() { - super.ready(); - this._getAuditLogs(); - } - - _getAuditLogs() { - if (!this.groupId) { return ''; } - - const errFn = response => { - this.fire('page-error', {response}); - }; - - return this.$.restAPI.getGroupAuditLog(this.groupId, errFn) - .then(auditLog => { - if (!auditLog) { - this._auditLog = []; - return; - } - this._auditLog = auditLog; - this._loading = false; - }); - } - - _status(item) { - return item.disabled ? 'Disabled' : 'Enabled'; - } - - itemType(type) { - let item; - switch (type) { - case 'ADD_GROUP': - case 'ADD_USER': - item = 'Added'; - break; - case 'REMOVE_GROUP': - case 'REMOVE_USER': - item = 'Removed'; - break; - default: - item = ''; - } - return item; - } - - _isGroupEvent(type) { - return GROUP_EVENTS.indexOf(type) !== -1; - } - - _computeGroupUrl(group) { - if (group && group.url && group.id) { - return Gerrit.Nav.getUrlForGroup(group.id); - } - - return ''; - } - - _getIdForUser(account) { - return account._account_id ? ' (' + account._account_id + ')' : ''; - } - - _getNameForGroup(group) { - if (group && group.name) { - return group.name; - } else if (group && group.id) { - // The URL encoded id of the member - return decodeURIComponent(group.id); - } - - return ''; - } + static get properties() { + return { + groupId: String, + _auditLog: Array, + _loading: { + type: Boolean, + value: true, + }, + }; } - customElements.define(GrGroupAuditLog.is, GrGroupAuditLog); -})(); + /** @override */ + attached() { + super.attached(); + this.fire('title-change', {title: 'Audit Log'}); + } + + /** @override */ + ready() { + super.ready(); + this._getAuditLogs(); + } + + _getAuditLogs() { + if (!this.groupId) { return ''; } + + const errFn = response => { + this.fire('page-error', {response}); + }; + + return this.$.restAPI.getGroupAuditLog(this.groupId, errFn) + .then(auditLog => { + if (!auditLog) { + this._auditLog = []; + return; + } + this._auditLog = auditLog; + this._loading = false; + }); + } + + _status(item) { + return item.disabled ? 'Disabled' : 'Enabled'; + } + + itemType(type) { + let item; + switch (type) { + case 'ADD_GROUP': + case 'ADD_USER': + item = 'Added'; + break; + case 'REMOVE_GROUP': + case 'REMOVE_USER': + item = 'Removed'; + break; + default: + item = ''; + } + return item; + } + + _isGroupEvent(type) { + return GROUP_EVENTS.indexOf(type) !== -1; + } + + _computeGroupUrl(group) { + if (group && group.url && group.id) { + return Gerrit.Nav.getUrlForGroup(group.id); + } + + return ''; + } + + _getIdForUser(account) { + return account._account_id ? ' (' + account._account_id + ')' : ''; + } + + _getNameForGroup(group) { + if (group && group.name) { + return group.name; + } else if (group && group.id) { + // The URL encoded id of the member + return decodeURIComponent(group.id); + } + + return ''; + } +} + +customElements.define(GrGroupAuditLog.is, GrGroupAuditLog);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js index 4ed751d..0958e7c 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js +++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_html.js
@@ -1,32 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> - -<dom-module id="gr-group-audit-log"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -38,28 +28,26 @@ } </style> <table id="list" class="genericList"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="date topHeader">Date</th> <th class="type topHeader">Type</th> <th class="member topHeader">Member</th> <th class="by-user topHeader">By User</th> </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]"> <td>Loading...</td> </tr> - <tbody class$="[[computeLoadingClass(_loading)]]"> + </tbody><tbody class\$="[[computeLoadingClass(_loading)]]"> <template is="dom-repeat" items="[[_auditLog]]"> <tr class="table"> <td class="date"> - <gr-date-formatter - has-tooltip - date-str="[[item.date]]"> + <gr-date-formatter has-tooltip="" date-str="[[item.date]]"> </gr-date-formatter> </td> <td class="type">[[itemType(item.type)]]</td> <td class="member"> <template is="dom-if" if="[[_isGroupEvent(item.type)]]"> - <a href$="[[_computeGroupUrl(item.member)]]"> + <a href\$="[[_computeGroupUrl(item.member)]]"> [[_getNameForGroup(item.member)]] </a> </template> @@ -77,6 +65,4 @@ </tbody> </table> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-group-audit-log.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html index 3a75611..d62874d 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html +++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group-audit-log</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-group-audit-log.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-group-audit-log.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group-audit-log.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,85 +40,87 @@ </template> </test-fixture> -<script> - suite('gr-group-audit-log tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group-audit-log.js'; +suite('gr-group-audit-log tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('members', () => { + test('test _getNameForGroup', () => { + let group = { + member: { + name: 'test-name', + }, + }; + assert.equal(element._getNameForGroup(group.member), 'test-name'); + + group = { + member: { + id: 'test-id', + }, + }; + assert.equal(element._getNameForGroup(group.member), 'test-id'); }); - teardown(() => { - sandbox.restore(); - }); + test('test _isGroupEvent', () => { + assert.isTrue(element._isGroupEvent('ADD_GROUP')); + assert.isTrue(element._isGroupEvent('REMOVE_GROUP')); - suite('members', () => { - test('test _getNameForGroup', () => { - let group = { - member: { - name: 'test-name', - }, - }; - assert.equal(element._getNameForGroup(group.member), 'test-name'); - - group = { - member: { - id: 'test-id', - }, - }; - assert.equal(element._getNameForGroup(group.member), 'test-id'); - }); - - test('test _isGroupEvent', () => { - assert.isTrue(element._isGroupEvent('ADD_GROUP')); - assert.isTrue(element._isGroupEvent('REMOVE_GROUP')); - - assert.isFalse(element._isGroupEvent('ADD_USER')); - assert.isFalse(element._isGroupEvent('REMOVE_USER')); - }); - }); - - suite('users', () => { - test('test _getIdForUser', () => { - const account = { - user: { - username: 'test-user', - _account_id: 12, - }, - }; - assert.equal(element._getIdForUser(account.user), ' (12)'); - }); - - test('test _account_id not present', () => { - const account = { - user: { - username: 'test-user', - }, - }; - assert.equal(element._getIdForUser(account.user), ''); - }); - }); - - suite('404', () => { - test('fires page-error', done => { - element.groupId = 1; - - const response = {status: 404}; - sandbox.stub( - element.$.restAPI, 'getGroupAuditLog', (group, errFn) => { - errFn(response); - }); - - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element._getAuditLogs(); - }); + assert.isFalse(element._isGroupEvent('ADD_USER')); + assert.isFalse(element._isGroupEvent('REMOVE_USER')); }); }); + + suite('users', () => { + test('test _getIdForUser', () => { + const account = { + user: { + username: 'test-user', + _account_id: 12, + }, + }; + assert.equal(element._getIdForUser(account.user), ' (12)'); + }); + + test('test _account_id not present', () => { + const account = { + user: { + username: 'test-user', + }, + }; + assert.equal(element._getIdForUser(account.user), ''); + }); + }); + + suite('404', () => { + test('fires page-error', done => { + element.groupId = 1; + + const response = {status: 404}; + sandbox.stub( + element.$.restAPI, 'getGroupAuditLog', (group, errFn) => { + errFn(response); + }); + + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + element._getAuditLogs(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js index 8c29f73..fc9e4a4 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members.js
@@ -14,280 +14,300 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const SUGGESTIONS_LIMIT = 15; - const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+ - 'permission to add it'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-subpage-styles.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-account-link/gr-account-link.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-group-members_html.js'; - const URL_REGEX = '^(?:[a-z]+:)?//'; +const SUGGESTIONS_LIMIT = 15; +const SAVING_ERROR_TEXT = 'Group may not exist, or you may not have '+ + 'permission to add it'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrGroupMembers extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-group-members'; } +const URL_REGEX = '^(?:[a-z]+:)?//'; - static get properties() { - return { - groupId: Number, - _groupMemberSearchId: String, - _groupMemberSearchName: String, - _includedGroupSearchId: String, - _includedGroupSearchName: String, - _loading: { - type: Boolean, - value: true, +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrGroupMembers extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-group-members'; } + + static get properties() { + return { + groupId: Number, + _groupMemberSearchId: String, + _groupMemberSearchName: String, + _includedGroupSearchId: String, + _includedGroupSearchName: String, + _loading: { + type: Boolean, + value: true, + }, + _groupName: String, + _groupMembers: Object, + _includedGroups: Object, + _itemName: String, + _itemType: String, + _queryMembers: { + type: Function, + value() { + return this._getAccountSuggestions.bind(this); }, - _groupName: String, - _groupMembers: Object, - _includedGroups: Object, - _itemName: String, - _itemType: String, - _queryMembers: { - type: Function, - value() { - return this._getAccountSuggestions.bind(this); - }, + }, + _queryIncludedGroup: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); }, - _queryIncludedGroup: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); - }, - }, - _groupOwner: { - type: Boolean, - value: false, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - }; - } + }, + _groupOwner: { + type: Boolean, + value: false, + }, + _isAdmin: { + type: Boolean, + value: false, + }, + }; + } - /** @override */ - attached() { - super.attached(); - this._loadGroupDetails(); + /** @override */ + attached() { + super.attached(); + this._loadGroupDetails(); - this.fire('title-change', {title: 'Members'}); - } + this.fire('title-change', {title: 'Members'}); + } - _loadGroupDetails() { - if (!this.groupId) { return; } + _loadGroupDetails() { + if (!this.groupId) { return; } - const promises = []; + const promises = []; - const errFn = response => { - this.fire('page-error', {response}); - }; + const errFn = response => { + this.fire('page-error', {response}); + }; - return this.$.restAPI.getGroupConfig(this.groupId, errFn) - .then(config => { - if (!config || !config.name) { return Promise.resolve(); } + return this.$.restAPI.getGroupConfig(this.groupId, errFn) + .then(config => { + if (!config || !config.name) { return Promise.resolve(); } - this._groupName = config.name; + this._groupName = config.name; - promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => { - this._isAdmin = isAdmin ? true : false; - })); + promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => { + this._isAdmin = isAdmin ? true : false; + })); - promises.push(this.$.restAPI.getIsGroupOwner(config.name) - .then(isOwner => { - this._groupOwner = isOwner ? true : false; - })); - - promises.push(this.$.restAPI.getGroupMembers(config.name).then( - members => { - this._groupMembers = members; - })); - - promises.push(this.$.restAPI.getIncludedGroup(config.name) - .then(includedGroup => { - this._includedGroups = includedGroup; - })); - - return Promise.all(promises).then(() => { - this._loading = false; - }); - }); - } - - _computeLoadingClass(loading) { - return loading ? 'loading' : ''; - } - - _isLoading() { - return this._loading || this._loading === undefined; - } - - _computeGroupUrl(url) { - if (!url) { return; } - - const r = new RegExp(URL_REGEX, 'i'); - if (r.test(url)) { - return url; - } - - // For GWT compatibility - if (url.startsWith('#')) { - return this.getBaseUrl() + url.slice(1); - } - return this.getBaseUrl() + url; - } - - _handleSavingGroupMember() { - return this.$.restAPI.saveGroupMembers(this._groupName, - this._groupMemberSearchId).then(config => { - if (!config) { - return; - } - this.$.restAPI.getGroupMembers(this._groupName).then(members => { - this._groupMembers = members; - }); - this._groupMemberSearchName = ''; - this._groupMemberSearchId = ''; - }); - } - - _handleDeleteConfirm() { - this.$.overlay.close(); - if (this._itemType === 'member') { - return this.$.restAPI.deleteGroupMembers(this._groupName, - this._itemId) - .then(itemDeleted => { - if (itemDeleted.status === 204) { - this.$.restAPI.getGroupMembers(this._groupName) - .then(members => { - this._groupMembers = members; - }); - } - }); - } else if (this._itemType === 'includedGroup') { - return this.$.restAPI.deleteIncludedGroup(this._groupName, - this._itemId) - .then(itemDeleted => { - if (itemDeleted.status === 204 || itemDeleted.status === 205) { - this.$.restAPI.getIncludedGroup(this._groupName) - .then(includedGroup => { - this._includedGroups = includedGroup; - }); - } - }); - } - } - - _handleConfirmDialogCancel() { - this.$.overlay.close(); - } - - _handleDeleteMember(e) { - const id = e.model.get('item._account_id'); - const name = e.model.get('item.name'); - const username = e.model.get('item.username'); - const email = e.model.get('item.email'); - const item = username || name || email || id; - if (!item) { - return ''; - } - this._itemName = item; - this._itemId = id; - this._itemType = 'member'; - this.$.overlay.open(); - } - - _handleSavingIncludedGroups() { - return this.$.restAPI.saveIncludedGroup(this._groupName, - this._includedGroupSearchId.replace(/\+/g, ' '), err => { - if (err.status === 404) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: SAVING_ERROR_TEXT}, - bubbles: true, - composed: true, + promises.push(this.$.restAPI.getIsGroupOwner(config.name) + .then(isOwner => { + this._groupOwner = isOwner ? true : false; })); - return err; - } - throw Error(err.statusText); - }) - .then(config => { - if (!config) { - return; - } - this.$.restAPI.getIncludedGroup(this._groupName) - .then(includedGroup => { - this._includedGroups = includedGroup; - }); - this._includedGroupSearchName = ''; - this._includedGroupSearchId = ''; + + promises.push(this.$.restAPI.getGroupMembers(config.name).then( + members => { + this._groupMembers = members; + })); + + promises.push(this.$.restAPI.getIncludedGroup(config.name) + .then(includedGroup => { + this._includedGroups = includedGroup; + })); + + return Promise.all(promises).then(() => { + this._loading = false; }); + }); + } + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + } + + _isLoading() { + return this._loading || this._loading === undefined; + } + + _computeGroupUrl(url) { + if (!url) { return; } + + const r = new RegExp(URL_REGEX, 'i'); + if (r.test(url)) { + return url; } - _handleDeleteIncludedGroup(e) { - const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' '); - const name = e.model.get('item.name'); - const item = name || id; - if (!item) { return ''; } - this._itemName = item; - this._itemId = id; - this._itemType = 'includedGroup'; - this.$.overlay.open(); + // For GWT compatibility + if (url.startsWith('#')) { + return this.getBaseUrl() + url.slice(1); } + return this.getBaseUrl() + url; + } - _getAccountSuggestions(input) { - if (input.length === 0) { return Promise.resolve([]); } - return this.$.restAPI.getSuggestedAccounts( - input, SUGGESTIONS_LIMIT).then(accounts => { - const accountSuggestions = []; - let nameAndEmail; - if (!accounts) { return []; } - for (const key in accounts) { - if (!accounts.hasOwnProperty(key)) { continue; } - if (accounts[key].email !== undefined) { - nameAndEmail = accounts[key].name + - ' <' + accounts[key].email + '>'; - } else { - nameAndEmail = accounts[key].name; - } - accountSuggestions.push({ - name: nameAndEmail, - value: accounts[key]._account_id, - }); - } - return accountSuggestions; + _handleSavingGroupMember() { + return this.$.restAPI.saveGroupMembers(this._groupName, + this._groupMemberSearchId).then(config => { + if (!config) { + return; + } + this.$.restAPI.getGroupMembers(this._groupName).then(members => { + this._groupMembers = members; }); - } + this._groupMemberSearchName = ''; + this._groupMemberSearchId = ''; + }); + } - _getGroupSuggestions(input) { - return this.$.restAPI.getSuggestedGroups(input) - .then(response => { - const groups = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - groups.push({ - name: key, - value: decodeURIComponent(response[key].id), - }); + _handleDeleteConfirm() { + this.$.overlay.close(); + if (this._itemType === 'member') { + return this.$.restAPI.deleteGroupMembers(this._groupName, + this._itemId) + .then(itemDeleted => { + if (itemDeleted.status === 204) { + this.$.restAPI.getGroupMembers(this._groupName) + .then(members => { + this._groupMembers = members; + }); } - return groups; }); - } - - _computeHideItemClass(owner, admin) { - return admin || owner ? '' : 'canModify'; + } else if (this._itemType === 'includedGroup') { + return this.$.restAPI.deleteIncludedGroup(this._groupName, + this._itemId) + .then(itemDeleted => { + if (itemDeleted.status === 204 || itemDeleted.status === 205) { + this.$.restAPI.getIncludedGroup(this._groupName) + .then(includedGroup => { + this._includedGroups = includedGroup; + }); + } + }); } } - customElements.define(GrGroupMembers.is, GrGroupMembers); -})(); + _handleConfirmDialogCancel() { + this.$.overlay.close(); + } + + _handleDeleteMember(e) { + const id = e.model.get('item._account_id'); + const name = e.model.get('item.name'); + const username = e.model.get('item.username'); + const email = e.model.get('item.email'); + const item = username || name || email || id; + if (!item) { + return ''; + } + this._itemName = item; + this._itemId = id; + this._itemType = 'member'; + this.$.overlay.open(); + } + + _handleSavingIncludedGroups() { + return this.$.restAPI.saveIncludedGroup(this._groupName, + this._includedGroupSearchId.replace(/\+/g, ' '), err => { + if (err.status === 404) { + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message: SAVING_ERROR_TEXT}, + bubbles: true, + composed: true, + })); + return err; + } + throw Error(err.statusText); + }) + .then(config => { + if (!config) { + return; + } + this.$.restAPI.getIncludedGroup(this._groupName) + .then(includedGroup => { + this._includedGroups = includedGroup; + }); + this._includedGroupSearchName = ''; + this._includedGroupSearchId = ''; + }); + } + + _handleDeleteIncludedGroup(e) { + const id = decodeURIComponent(e.model.get('item.id')).replace(/\+/g, ' '); + const name = e.model.get('item.name'); + const item = name || id; + if (!item) { return ''; } + this._itemName = item; + this._itemId = id; + this._itemType = 'includedGroup'; + this.$.overlay.open(); + } + + _getAccountSuggestions(input) { + if (input.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedAccounts( + input, SUGGESTIONS_LIMIT).then(accounts => { + const accountSuggestions = []; + let nameAndEmail; + if (!accounts) { return []; } + for (const key in accounts) { + if (!accounts.hasOwnProperty(key)) { continue; } + if (accounts[key].email !== undefined) { + nameAndEmail = accounts[key].name + + ' <' + accounts[key].email + '>'; + } else { + nameAndEmail = accounts[key].name; + } + accountSuggestions.push({ + name: nameAndEmail, + value: accounts[key]._account_id, + }); + } + return accountSuggestions; + }); + } + + _getGroupSuggestions(input) { + return this.$.restAPI.getSuggestedGroups(input) + .then(response => { + const groups = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + groups.push({ + name: key, + value: decodeURIComponent(response[key].id), + }); + } + return groups; + }); + } + + _computeHideItemClass(owner, admin) { + return admin || owner ? '' : 'canModify'; + } +} + +customElements.define(GrGroupMembers.is, GrGroupMembers);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js index cf24793..79a88fd 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_html.js
@@ -1,38 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-subpage-styles.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html"> - -<dom-module id="gr-group-members"> - <template> +export const htmlTemplate = html` <style include="gr-form-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -72,37 +56,29 @@ display: none; } </style> - <main class$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]"> - <div id="loading" class$="[[_computeLoadingClass(_loading)]]"> + <main class\$="gr-form-styles [[_computeHideItemClass(_groupOwner, _isAdmin)]]"> + <div id="loading" class\$="[[_computeLoadingClass(_loading)]]"> Loading... </div> - <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> + <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]"> <h1 id="Title">[[_groupName]]</h1> <div id="form"> <h3 id="members">Members</h3> <fieldset> <span class="value"> - <gr-autocomplete - id="groupMemberSearchInput" - text="{{_groupMemberSearchName}}" - value="{{_groupMemberSearchId}}" - query="[[_queryMembers]]" - placeholder="Name Or Email"> + <gr-autocomplete id="groupMemberSearchInput" text="{{_groupMemberSearchName}}" value="{{_groupMemberSearchId}}" query="[[_queryMembers]]" placeholder="Name Or Email"> </gr-autocomplete> </span> - <gr-button - id="saveGroupMember" - on-click="_handleSavingGroupMember" - disabled="[[!_groupMemberSearchId]]"> + <gr-button id="saveGroupMember" on-click="_handleSavingGroupMember" disabled="[[!_groupMemberSearchId]]"> Add </gr-button> <table id="groupMembers"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="nameHeader">Name</th> <th class="emailAddressHeader">Email Address</th> <th class="deleteHeader">Delete Member</th> </tr> - <tbody> + </tbody><tbody> <template is="dom-repeat" items="[[_groupMembers]]"> <tr> <td class="nameColumn"> @@ -110,9 +86,7 @@ </td> <td>[[item.email]]</td> <td class="deleteColumn"> - <gr-button - class="deleteMembersButton" - on-click="_handleDeleteMember"> + <gr-button class="deleteMembersButton" on-click="_handleDeleteMember"> Delete </gr-button> </td> @@ -124,35 +98,26 @@ <h3 id="includedGroups">Included Groups</h3> <fieldset> <span class="value"> - <gr-autocomplete - id="includedGroupSearchInput" - text="{{_includedGroupSearchName}}" - value="{{_includedGroupSearchId}}" - query="[[_queryIncludedGroup]]" - placeholder="Group Name"> + <gr-autocomplete id="includedGroupSearchInput" text="{{_includedGroupSearchName}}" value="{{_includedGroupSearchId}}" query="[[_queryIncludedGroup]]" placeholder="Group Name"> </gr-autocomplete> </span> - <gr-button - id="saveIncludedGroups" - on-click="_handleSavingIncludedGroups" - disabled="[[!_includedGroupSearchId]]"> + <gr-button id="saveIncludedGroups" on-click="_handleSavingIncludedGroups" disabled="[[!_includedGroupSearchId]]"> Add </gr-button> <table id="includedGroups"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="groupNameHeader">Group Name</th> <th class="descriptionHeader">Description</th> <th class="deleteIncludedHeader"> Delete Group </th> </tr> - <tbody> + </tbody><tbody> <template is="dom-repeat" items="[[_includedGroups]]"> <tr> <td class="nameColumn"> <template is="dom-if" if="[[item.url]]"> - <a href$="[[_computeGroupUrl(item.url)]]" - rel="noopener"> + <a href\$="[[_computeGroupUrl(item.url)]]" rel="noopener"> [[item.name]] </a> </template> @@ -162,9 +127,7 @@ </td> <td>[[item.description]]</td> <td class="deleteColumn"> - <gr-button - class="deleteIncludedGroupButton" - on-click="_handleDeleteIncludedGroup"> + <gr-button class="deleteIncludedGroupButton" on-click="_handleDeleteIncludedGroup"> Delete </gr-button> </td> @@ -176,15 +139,8 @@ </div> </div> </main> - <gr-overlay id="overlay" with-backdrop> - <gr-confirm-delete-item-dialog - class="confirmDialog" - on-confirm="_handleDeleteConfirm" - on-cancel="_handleConfirmDialogCancel" - item="[[_itemName]]" - item-type="[[_itemType]]"></gr-confirm-delete-item-dialog> + <gr-overlay id="overlay" with-backdrop=""> + <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_itemName]]" item-type="[[_itemType]]"></gr-confirm-delete-item-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-group-members.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html index ec9a80c..9380b86 100644 --- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html +++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group-members</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-group-members.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-group-members.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group-members.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,342 +40,345 @@ </template> </test-fixture> -<script> - suite('gr-group-members tests', async () => { - await readyToTest(); - let element; - let sandbox; - let groups; - let groupMembers; - let includedGroups; - let groupStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group-members.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-group-members tests', () => { + let element; + let sandbox; + let groups; + let groupMembers; + let includedGroups; + let groupStub; - setup(() => { - sandbox = sinon.sandbox.create(); + setup(() => { + sandbox = sinon.sandbox.create(); - groups = { - name: 'Administrators', - owner: 'Administrators', - group_id: 1, - }; + groups = { + name: 'Administrators', + owner: 'Administrators', + group_id: 1, + }; - groupMembers = [ - { - _account_id: 1000097, - name: 'Jane Roe', - email: 'jane.roe@example.com', - username: 'jane', - }, - { - _account_id: 1000096, - name: 'Test User', - email: 'john.doe@example.com', - }, - { - _account_id: 1000095, - name: 'Gerrit', - }, - { - _account_id: 1000098, - }, - ]; - - includedGroups = [{ - url: 'https://group/url', - options: {}, - id: 'testId', - name: 'testName', + groupMembers = [ + { + _account_id: 1000097, + name: 'Jane Roe', + email: 'jane.roe@example.com', + username: 'jane', }, { - url: '/group/url', - options: {}, - id: 'testId2', - name: 'testName2', + _account_id: 1000096, + name: 'Test User', + email: 'john.doe@example.com', }, { - url: '#/group/url', - options: {}, - id: 'testId3', - name: 'testName3', + _account_id: 1000095, + name: 'Gerrit', }, - ]; + { + _account_id: 1000098, + }, + ]; - stub('gr-rest-api-interface', { - getSuggestedAccounts(input) { - if (input.startsWith('test')) { - return Promise.resolve([ - { - _account_id: 1000096, - name: 'test-account', - email: 'test.account@example.com', - username: 'test123', - }, - { - _account_id: 1001439, - name: 'test-admin', - email: 'test.admin@example.com', - username: 'test_admin', - }, - { - _account_id: 1001439, - name: 'test-git', - username: 'test_git', - }, - ]); - } else { - return Promise.resolve({}); - } - }, - getSuggestedGroups(input) { - if (input.startsWith('test')) { - return Promise.resolve({ - 'test-admin': { - id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a', - }, - 'test/Administrator (admin)': { - id: 'test%3Aadmin', - }, - }); - } else { - return Promise.resolve({}); - } - }, - getLoggedIn() { return Promise.resolve(true); }, - getConfig() { - return Promise.resolve(); - }, - getGroupMembers() { - return Promise.resolve(groupMembers); - }, - getIsGroupOwner() { - return Promise.resolve(true); - }, - getIncludedGroup() { - return Promise.resolve(includedGroups); - }, - getAccountCapabilities() { - return Promise.resolve(); - }, - }); - element = fixture('basic'); - sandbox.stub(element, 'getBaseUrl').returns('https://test/site'); - element.groupId = 1; - groupStub = sandbox.stub( - element.$.restAPI, - 'getGroupConfig', - () => Promise.resolve(groups)); - return element._loadGroupDetails(); + includedGroups = [{ + url: 'https://group/url', + options: {}, + id: 'testId', + name: 'testName', + }, + { + url: '/group/url', + options: {}, + id: 'testId2', + name: 'testName2', + }, + { + url: '#/group/url', + options: {}, + id: 'testId3', + name: 'testName3', + }, + ]; + + stub('gr-rest-api-interface', { + getSuggestedAccounts(input) { + if (input.startsWith('test')) { + return Promise.resolve([ + { + _account_id: 1000096, + name: 'test-account', + email: 'test.account@example.com', + username: 'test123', + }, + { + _account_id: 1001439, + name: 'test-admin', + email: 'test.admin@example.com', + username: 'test_admin', + }, + { + _account_id: 1001439, + name: 'test-git', + username: 'test_git', + }, + ]); + } else { + return Promise.resolve({}); + } + }, + getSuggestedGroups(input) { + if (input.startsWith('test')) { + return Promise.resolve({ + 'test-admin': { + id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a', + }, + 'test/Administrator (admin)': { + id: 'test%3Aadmin', + }, + }); + } else { + return Promise.resolve({}); + } + }, + getLoggedIn() { return Promise.resolve(true); }, + getConfig() { + return Promise.resolve(); + }, + getGroupMembers() { + return Promise.resolve(groupMembers); + }, + getIsGroupOwner() { + return Promise.resolve(true); + }, + getIncludedGroup() { + return Promise.resolve(includedGroups); + }, + getAccountCapabilities() { + return Promise.resolve(); + }, }); + element = fixture('basic'); + sandbox.stub(element, 'getBaseUrl').returns('https://test/site'); + element.groupId = 1; + groupStub = sandbox.stub( + element.$.restAPI, + 'getGroupConfig', + () => Promise.resolve(groups)); + return element._loadGroupDetails(); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('_includedGroups', () => { - assert.equal(element._includedGroups.length, 3); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('.nameColumn a')[1].href, - 'https://test/site/group/url'); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('.nameColumn a')[2].href, - 'https://test/site/group/url'); - }); + test('_includedGroups', () => { + assert.equal(element._includedGroups.length, 3); + assert.equal(dom(element.root) + .querySelectorAll('.nameColumn a')[0].href, includedGroups[0].url); + assert.equal(dom(element.root) + .querySelectorAll('.nameColumn a')[1].href, + 'https://test/site/group/url'); + assert.equal(dom(element.root) + .querySelectorAll('.nameColumn a')[2].href, + 'https://test/site/group/url'); + }); - test('save members correctly', () => { - element._groupOwner = true; + test('save members correctly', () => { + element._groupOwner = true; - const memberName = 'test-admin'; + const memberName = 'test-admin'; - const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers', - () => Promise.resolve({})); + const saveStub = sandbox.stub(element.$.restAPI, 'saveGroupMembers', + () => Promise.resolve({})); - const button = element.$.saveGroupMember; + const button = element.$.saveGroupMember; + assert.isTrue(button.hasAttribute('disabled')); + + element.$.groupMemberSearchInput.text = memberName; + element.$.groupMemberSearchInput.value = 1234; + + assert.isFalse(button.hasAttribute('disabled')); + + return element._handleSavingGroupMember().then(() => { assert.isTrue(button.hasAttribute('disabled')); - - element.$.groupMemberSearchInput.text = memberName; - element.$.groupMemberSearchInput.value = 1234; - - assert.isFalse(button.hasAttribute('disabled')); - - return element._handleSavingGroupMember().then(() => { - assert.isTrue(button.hasAttribute('disabled')); - assert.isFalse(element.$.Title.classList.contains('edited')); - assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators', - 1234)); - }); - }); - - test('save included groups correctly', () => { - element._groupOwner = true; - - const includedGroupName = 'testName'; - - const saveIncludedGroupStub = sandbox.stub( - element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({})); - - const button = element.$.saveIncludedGroups; - - assert.isTrue(button.hasAttribute('disabled')); - - element.$.includedGroupSearchInput.text = includedGroupName; - element.$.includedGroupSearchInput.value = 'testId'; - - assert.isFalse(button.hasAttribute('disabled')); - - return element._handleSavingIncludedGroups().then(() => { - assert.isTrue(button.hasAttribute('disabled')); - assert.isFalse(element.$.Title.classList.contains('edited')); - assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators'); - assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId'); - }); - }); - - test('add included group 404 shows helpful error text', () => { - element._groupOwner = true; - - const memberName = 'bad-name'; - const alertStub = sandbox.stub(); - element.addEventListener('show-alert', alertStub); - const error = new Error('error'); - error.status = 404; - sandbox.stub(element.$.restAPI, 'saveGroupMembers', - () => Promise.reject(error)); - - element.$.groupMemberSearchInput.text = memberName; - element.$.groupMemberSearchInput.value = 1234; - - return element._handleSavingIncludedGroups().then(() => { - assert.isTrue(alertStub.called); - }); - }); - - test('_getAccountSuggestions empty', done => { - element - ._getAccountSuggestions('nonexistent').then(accounts => { - assert.equal(accounts.length, 0); - done(); - }); - }); - - test('_getAccountSuggestions non-empty', done => { - element - ._getAccountSuggestions('test-').then(accounts => { - assert.equal(accounts.length, 3); - assert.equal(accounts[0].name, - 'test-account <test.account@example.com>'); - assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>'); - assert.equal(accounts[2].name, 'test-git'); - done(); - }); - }); - - test('_getGroupSuggestions empty', done => { - element - ._getGroupSuggestions('nonexistent').then(groups => { - assert.equal(groups.length, 0); - done(); - }); - }); - - test('_getGroupSuggestions non-empty', done => { - element - ._getGroupSuggestions('test').then(groups => { - assert.equal(groups.length, 2); - assert.equal(groups[0].name, 'test-admin'); - assert.equal(groups[1].name, 'test/Administrator (admin)'); - done(); - }); - }); - - test('_computeHideItemClass returns string for admin', () => { - const admin = true; - const owner = false; - assert.equal(element._computeHideItemClass(owner, admin), ''); - }); - - test('_computeHideItemClass returns hideItem for admin and owner', () => { - const admin = false; - const owner = false; - assert.equal(element._computeHideItemClass(owner, admin), 'canModify'); - }); - - test('_computeHideItemClass returns string for owner', () => { - const admin = false; - const owner = true; - assert.equal(element._computeHideItemClass(owner, admin), ''); - }); - - test('delete member', () => { - const deletelBtns = Polymer.dom(element.root) - .querySelectorAll('.deleteMembersButton'); - MockInteractions.tap(deletelBtns[0]); - assert.equal(element._itemId, '1000097'); - assert.equal(element._itemName, 'jane'); - MockInteractions.tap(deletelBtns[1]); - assert.equal(element._itemId, '1000096'); - assert.equal(element._itemName, 'Test User'); - MockInteractions.tap(deletelBtns[2]); - assert.equal(element._itemId, '1000095'); - assert.equal(element._itemName, 'Gerrit'); - MockInteractions.tap(deletelBtns[3]); - assert.equal(element._itemId, '1000098'); - assert.equal(element._itemName, '1000098'); - }); - - test('delete included groups', () => { - const deletelBtns = Polymer.dom(element.root) - .querySelectorAll('.deleteIncludedGroupButton'); - MockInteractions.tap(deletelBtns[0]); - assert.equal(element._itemId, 'testId'); - assert.equal(element._itemName, 'testName'); - MockInteractions.tap(deletelBtns[1]); - assert.equal(element._itemId, 'testId2'); - assert.equal(element._itemName, 'testName2'); - MockInteractions.tap(deletelBtns[2]); - assert.equal(element._itemId, 'testId3'); - assert.equal(element._itemName, 'testName3'); - }); - - test('_computeLoadingClass', () => { - assert.equal(element._computeLoadingClass(true), 'loading'); - - assert.equal(element._computeLoadingClass(false), ''); - }); - - test('_computeGroupUrl', () => { - assert.isUndefined(element._computeGroupUrl(undefined)); - - assert.isUndefined(element._computeGroupUrl(false)); - - let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'; - assert.equal(element._computeGroupUrl(url), - 'https://test/site/admin/groups/' + - 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'); - - url = 'https://gerrit.local/admin/groups/' + - 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'; - assert.equal(element._computeGroupUrl(url), url); - }); - - test('fires page-error', done => { - groupStub.restore(); - - element.groupId = 1; - - const response = {status: 404}; - sandbox.stub( - element.$.restAPI, 'getGroupConfig', (group, errFn) => { - errFn(response); - }); - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element._loadGroupDetails(); + assert.isFalse(element.$.Title.classList.contains('edited')); + assert.isTrue(saveStub.lastCall.calledWithExactly('Administrators', + 1234)); }); }); + + test('save included groups correctly', () => { + element._groupOwner = true; + + const includedGroupName = 'testName'; + + const saveIncludedGroupStub = sandbox.stub( + element.$.restAPI, 'saveIncludedGroup', () => Promise.resolve({})); + + const button = element.$.saveIncludedGroups; + + assert.isTrue(button.hasAttribute('disabled')); + + element.$.includedGroupSearchInput.text = includedGroupName; + element.$.includedGroupSearchInput.value = 'testId'; + + assert.isFalse(button.hasAttribute('disabled')); + + return element._handleSavingIncludedGroups().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + assert.equal(saveIncludedGroupStub.lastCall.args[0], 'Administrators'); + assert.equal(saveIncludedGroupStub.lastCall.args[1], 'testId'); + }); + }); + + test('add included group 404 shows helpful error text', () => { + element._groupOwner = true; + + const memberName = 'bad-name'; + const alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); + const error = new Error('error'); + error.status = 404; + sandbox.stub(element.$.restAPI, 'saveGroupMembers', + () => Promise.reject(error)); + + element.$.groupMemberSearchInput.text = memberName; + element.$.groupMemberSearchInput.value = 1234; + + return element._handleSavingIncludedGroups().then(() => { + assert.isTrue(alertStub.called); + }); + }); + + test('_getAccountSuggestions empty', done => { + element + ._getAccountSuggestions('nonexistent').then(accounts => { + assert.equal(accounts.length, 0); + done(); + }); + }); + + test('_getAccountSuggestions non-empty', done => { + element + ._getAccountSuggestions('test-').then(accounts => { + assert.equal(accounts.length, 3); + assert.equal(accounts[0].name, + 'test-account <test.account@example.com>'); + assert.equal(accounts[1].name, 'test-admin <test.admin@example.com>'); + assert.equal(accounts[2].name, 'test-git'); + done(); + }); + }); + + test('_getGroupSuggestions empty', done => { + element + ._getGroupSuggestions('nonexistent').then(groups => { + assert.equal(groups.length, 0); + done(); + }); + }); + + test('_getGroupSuggestions non-empty', done => { + element + ._getGroupSuggestions('test').then(groups => { + assert.equal(groups.length, 2); + assert.equal(groups[0].name, 'test-admin'); + assert.equal(groups[1].name, 'test/Administrator (admin)'); + done(); + }); + }); + + test('_computeHideItemClass returns string for admin', () => { + const admin = true; + const owner = false; + assert.equal(element._computeHideItemClass(owner, admin), ''); + }); + + test('_computeHideItemClass returns hideItem for admin and owner', () => { + const admin = false; + const owner = false; + assert.equal(element._computeHideItemClass(owner, admin), 'canModify'); + }); + + test('_computeHideItemClass returns string for owner', () => { + const admin = false; + const owner = true; + assert.equal(element._computeHideItemClass(owner, admin), ''); + }); + + test('delete member', () => { + const deletelBtns = dom(element.root) + .querySelectorAll('.deleteMembersButton'); + MockInteractions.tap(deletelBtns[0]); + assert.equal(element._itemId, '1000097'); + assert.equal(element._itemName, 'jane'); + MockInteractions.tap(deletelBtns[1]); + assert.equal(element._itemId, '1000096'); + assert.equal(element._itemName, 'Test User'); + MockInteractions.tap(deletelBtns[2]); + assert.equal(element._itemId, '1000095'); + assert.equal(element._itemName, 'Gerrit'); + MockInteractions.tap(deletelBtns[3]); + assert.equal(element._itemId, '1000098'); + assert.equal(element._itemName, '1000098'); + }); + + test('delete included groups', () => { + const deletelBtns = dom(element.root) + .querySelectorAll('.deleteIncludedGroupButton'); + MockInteractions.tap(deletelBtns[0]); + assert.equal(element._itemId, 'testId'); + assert.equal(element._itemName, 'testName'); + MockInteractions.tap(deletelBtns[1]); + assert.equal(element._itemId, 'testId2'); + assert.equal(element._itemName, 'testName2'); + MockInteractions.tap(deletelBtns[2]); + assert.equal(element._itemId, 'testId3'); + assert.equal(element._itemName, 'testName3'); + }); + + test('_computeLoadingClass', () => { + assert.equal(element._computeLoadingClass(true), 'loading'); + + assert.equal(element._computeLoadingClass(false), ''); + }); + + test('_computeGroupUrl', () => { + assert.isUndefined(element._computeGroupUrl(undefined)); + + assert.isUndefined(element._computeGroupUrl(false)); + + let url = '#/admin/groups/uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'; + assert.equal(element._computeGroupUrl(url), + 'https://test/site/admin/groups/' + + 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'); + + url = 'https://gerrit.local/admin/groups/' + + 'uuid-529b3c2605bb1029c8146f9de4a91c776fe64498'; + assert.equal(element._computeGroupUrl(url), url); + }); + + test('fires page-error', done => { + groupStub.restore(); + + element.groupId = 1; + + const response = {status: 404}; + sandbox.stub( + element.$.restAPI, 'getGroupConfig', (group, errFn) => { + errFn(response); + }); + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + element._loadGroupDetails(); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js index 42846f4..5127733 100644 --- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.js +++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.js
@@ -14,238 +14,253 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; - const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-subpage-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-group_html.js'; - const OPTIONS = { - submitFalse: { - value: false, - label: 'False', - }, - submitTrue: { - value: true, - label: 'True', - }, - }; +const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/; +const OPTIONS = { + submitFalse: { + value: false, + label: 'False', + }, + submitTrue: { + value: true, + label: 'True', + }, +}; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrGroup extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-group'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the group name changes. + * + * @event name-changed */ - class GrGroup extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-group'; } - /** - * Fired when the group name changes. - * - * @event name-changed - */ - static get properties() { - return { - groupId: Number, - _rename: { - type: Boolean, - value: false, + static get properties() { + return { + groupId: Number, + _rename: { + type: Boolean, + value: false, + }, + _groupIsInternal: Boolean, + _description: { + type: Boolean, + value: false, + }, + _owner: { + type: Boolean, + value: false, + }, + _options: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + /** @type {?} */ + _groupConfig: Object, + _groupConfigOwner: String, + _groupName: Object, + _groupOwner: { + type: Boolean, + value: false, + }, + _submitTypes: { + type: Array, + value() { + return Object.values(OPTIONS); }, - _groupIsInternal: Boolean, - _description: { - type: Boolean, - value: false, + }, + _query: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); }, - _owner: { - type: Boolean, - value: false, - }, - _options: { - type: Boolean, - value: false, - }, - _loading: { - type: Boolean, - value: true, - }, - /** @type {?} */ - _groupConfig: Object, - _groupConfigOwner: String, - _groupName: Object, - _groupOwner: { - type: Boolean, - value: false, - }, - _submitTypes: { - type: Array, - value() { - return Object.values(OPTIONS); - }, - }, - _query: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); - }, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - }; - } - - static get observers() { - return [ - '_handleConfigName(_groupConfig.name)', - '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)', - '_handleConfigDescription(_groupConfig.description)', - '_handleConfigOptions(_groupConfig.options.visible_to_all)', - ]; - } - - /** @override */ - attached() { - super.attached(); - this._loadGroup(); - } - - _loadGroup() { - if (!this.groupId) { return; } - - const promises = []; - - const errFn = response => { - this.fire('page-error', {response}); - }; - - return this.$.restAPI.getGroupConfig(this.groupId, errFn) - .then(config => { - if (!config || !config.name) { return Promise.resolve(); } - - this._groupName = config.name; - this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX); - - promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => { - this._isAdmin = isAdmin ? true : false; - })); - - promises.push(this.$.restAPI.getIsGroupOwner(config.name) - .then(isOwner => { - this._groupOwner = isOwner ? true : false; - })); - - // If visible to all is undefined, set to false. If it is defined - // as false, setting to false is fine. If any optional values - // are added with a default of true, then this would need to be an - // undefined check and not a truthy/falsy check. - if (!config.options.visible_to_all) { - config.options.visible_to_all = false; - } - this._groupConfig = config; - - this.fire('title-change', {title: config.name}); - - return Promise.all(promises).then(() => { - this._loading = false; - }); - }); - } - - _computeLoadingClass(loading) { - return loading ? 'loading' : ''; - } - - _isLoading() { - return this._loading || this._loading === undefined; - } - - _handleSaveName() { - return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name) - .then(config => { - if (config.status === 200) { - this._groupName = this._groupConfig.name; - this.fire('name-changed', {name: this._groupConfig.name, - external: this._groupIsExtenral}); - this._rename = false; - } - }); - } - - _handleSaveOwner() { - let owner = this._groupConfig.owner; - if (this._groupConfigOwner) { - owner = decodeURIComponent(this._groupConfigOwner); - } - return this.$.restAPI.saveGroupOwner(this.groupId, - owner).then(config => { - this._owner = false; - }); - } - - _handleSaveDescription() { - return this.$.restAPI.saveGroupDescription(this.groupId, - this._groupConfig.description).then(config => { - this._description = false; - }); - } - - _handleSaveOptions() { - const visible = this._groupConfig.options.visible_to_all; - - const options = {visible_to_all: visible}; - - return this.$.restAPI.saveGroupOptions(this.groupId, - options).then(config => { - this._options = false; - }); - } - - _handleConfigName() { - if (this._isLoading()) { return; } - this._rename = true; - } - - _handleConfigOwner() { - if (this._isLoading()) { return; } - this._owner = true; - } - - _handleConfigDescription() { - if (this._isLoading()) { return; } - this._description = true; - } - - _handleConfigOptions() { - if (this._isLoading()) { return; } - this._options = true; - } - - _computeHeaderClass(configChanged) { - return configChanged ? 'edited' : ''; - } - - _getGroupSuggestions(input) { - return this.$.restAPI.getSuggestedGroups(input) - .then(response => { - const groups = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - groups.push({ - name: key, - value: decodeURIComponent(response[key].id), - }); - } - return groups; - }); - } - - _computeGroupDisabled(owner, admin, groupIsInternal) { - return groupIsInternal && (admin || owner) ? false : true; - } + }, + _isAdmin: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrGroup.is, GrGroup); -})(); + static get observers() { + return [ + '_handleConfigName(_groupConfig.name)', + '_handleConfigOwner(_groupConfig.owner, _groupConfigOwner)', + '_handleConfigDescription(_groupConfig.description)', + '_handleConfigOptions(_groupConfig.options.visible_to_all)', + ]; + } + + /** @override */ + attached() { + super.attached(); + this._loadGroup(); + } + + _loadGroup() { + if (!this.groupId) { return; } + + const promises = []; + + const errFn = response => { + this.fire('page-error', {response}); + }; + + return this.$.restAPI.getGroupConfig(this.groupId, errFn) + .then(config => { + if (!config || !config.name) { return Promise.resolve(); } + + this._groupName = config.name; + this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX); + + promises.push(this.$.restAPI.getIsAdmin().then(isAdmin => { + this._isAdmin = isAdmin ? true : false; + })); + + promises.push(this.$.restAPI.getIsGroupOwner(config.name) + .then(isOwner => { + this._groupOwner = isOwner ? true : false; + })); + + // If visible to all is undefined, set to false. If it is defined + // as false, setting to false is fine. If any optional values + // are added with a default of true, then this would need to be an + // undefined check and not a truthy/falsy check. + if (!config.options.visible_to_all) { + config.options.visible_to_all = false; + } + this._groupConfig = config; + + this.fire('title-change', {title: config.name}); + + return Promise.all(promises).then(() => { + this._loading = false; + }); + }); + } + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + } + + _isLoading() { + return this._loading || this._loading === undefined; + } + + _handleSaveName() { + return this.$.restAPI.saveGroupName(this.groupId, this._groupConfig.name) + .then(config => { + if (config.status === 200) { + this._groupName = this._groupConfig.name; + this.fire('name-changed', {name: this._groupConfig.name, + external: this._groupIsExtenral}); + this._rename = false; + } + }); + } + + _handleSaveOwner() { + let owner = this._groupConfig.owner; + if (this._groupConfigOwner) { + owner = decodeURIComponent(this._groupConfigOwner); + } + return this.$.restAPI.saveGroupOwner(this.groupId, + owner).then(config => { + this._owner = false; + }); + } + + _handleSaveDescription() { + return this.$.restAPI.saveGroupDescription(this.groupId, + this._groupConfig.description).then(config => { + this._description = false; + }); + } + + _handleSaveOptions() { + const visible = this._groupConfig.options.visible_to_all; + + const options = {visible_to_all: visible}; + + return this.$.restAPI.saveGroupOptions(this.groupId, + options).then(config => { + this._options = false; + }); + } + + _handleConfigName() { + if (this._isLoading()) { return; } + this._rename = true; + } + + _handleConfigOwner() { + if (this._isLoading()) { return; } + this._owner = true; + } + + _handleConfigDescription() { + if (this._isLoading()) { return; } + this._description = true; + } + + _handleConfigOptions() { + if (this._isLoading()) { return; } + this._options = true; + } + + _computeHeaderClass(configChanged) { + return configChanged ? 'edited' : ''; + } + + _getGroupSuggestions(input) { + return this.$.restAPI.getSuggestedGroups(input) + .then(response => { + const groups = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + groups.push({ + name: key, + value: decodeURIComponent(response[key].id), + }); + } + return groups; + }); + } + + _computeGroupDisabled(owner, admin, groupIsInternal) { + return groupIsInternal && (admin || owner) ? false : true; + } +} + +customElements.define(GrGroup.is, GrGroup);
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js index faabe84..dc80235 100644 --- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js +++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.js
@@ -1,33 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-subpage-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<dom-module id="gr-group"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -44,77 +33,57 @@ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <main class="gr-form-styles read-only"> - <div id="loading" class$="[[_computeLoadingClass(_loading)]]"> + <div id="loading" class\$="[[_computeLoadingClass(_loading)]]"> Loading... </div> - <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> + <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]"> <h1 id="Title">[[_groupName]]</h1> <h2 id="configurations">General</h2> <div id="form"> <fieldset> <h3 id="groupUUID">Group UUID</h3> <fieldset> - <gr-copy-clipboard - text="[[groupId]]"></gr-copy-clipboard> + <gr-copy-clipboard text="[[groupId]]"></gr-copy-clipboard> </fieldset> - <h3 id="groupName" class$="[[_computeHeaderClass(_rename)]]"> + <h3 id="groupName" class\$="[[_computeHeaderClass(_rename)]]"> Group Name </h3> <fieldset> <span class="value"> - <gr-autocomplete - id="groupNameInput" - text="{{_groupConfig.name}}" - disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete> + <gr-autocomplete id="groupNameInput" text="{{_groupConfig.name}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></gr-autocomplete> </span> - <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> - <gr-button - id="inputUpdateNameBtn" - on-click="_handleSaveName" - disabled="[[!_rename]]"> + <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> + <gr-button id="inputUpdateNameBtn" on-click="_handleSaveName" disabled="[[!_rename]]"> Rename Group</gr-button> </span> </fieldset> - <h3 class$="[[_computeHeaderClass(_owner)]]"> + <h3 class\$="[[_computeHeaderClass(_owner)]]"> Owners </h3> <fieldset> <span class="value"> - <gr-autocomplete - id="groupOwnerInput" - text="{{_groupConfig.owner}}" - value="{{_groupConfigOwner}}" - query="[[_query]]" - disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> + <gr-autocomplete id="groupOwnerInput" text="{{_groupConfig.owner}}" value="{{_groupConfigOwner}}" query="[[_query]]" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> </gr-autocomplete> </span> - <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> - <gr-button - on-click="_handleSaveOwner" - disabled="[[!_owner]]"> + <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> + <gr-button on-click="_handleSaveOwner" disabled="[[!_owner]]"> Change Owners</gr-button> </span> </fieldset> - <h3 class$="[[_computeHeaderClass(_description)]]"> + <h3 class\$="[[_computeHeaderClass(_description)]]"> Description </h3> <fieldset> <div> - <iron-autogrow-textarea - class="description" - autocomplete="on" - bind-value="{{_groupConfig.description}}" - disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea> + <iron-autogrow-textarea class="description" autocomplete="on" bind-value="{{_groupConfig.description}}" disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"></iron-autogrow-textarea> </div> - <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> - <gr-button - on-click="_handleSaveDescription" - disabled="[[!_description]]"> + <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> + <gr-button on-click="_handleSaveDescription" disabled="[[!_description]]"> Save Description </gr-button> </span> </fieldset> - <h3 id="options" class$="[[_computeHeaderClass(_options)]]"> + <h3 id="options" class\$="[[_computeHeaderClass(_options)]]"> Group Options </h3> <fieldset id="visableToAll"> @@ -123,10 +92,8 @@ Make group visible to all registered users </span> <span class="value"> - <gr-select - id="visibleToAll" - bind-value="{{_groupConfig.options.visible_to_all}}"> - <select disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> + <gr-select id="visibleToAll" bind-value="{{_groupConfig.options.visible_to_all}}"> + <select disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> <template is="dom-repeat" items="[[_submitTypes]]"> <option value="[[item.value]]">[[item.label]]</option> </template> @@ -134,10 +101,8 @@ </gr-select> </span> </section> - <span class="value" disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> - <gr-button - on-click="_handleSaveOptions" - disabled="[[!_options]]"> + <span class="value" disabled\$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"> + <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]"> Save Group Options </gr-button> </span> @@ -147,6 +112,4 @@ </div> </main> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-group.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html index a6aebbf..9f278c4 100644 --- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html +++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-group</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-group.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-group.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,222 +40,224 @@ </template> </test-fixture> -<script> - suite('gr-group tests', async () => { - await readyToTest(); - let element; - let sandbox; - let groupStub; - const group = { - id: '6a1e70e1a88782771a91808c8af9bbb7a9871389', - url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389', - options: {}, - description: 'Gerrit Site Administrators', - group_id: 1, - owner: 'Administrators', - owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389', - name: 'Administrators', - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group.js'; +suite('gr-group tests', () => { + let element; + let sandbox; + let groupStub; + const group = { + id: '6a1e70e1a88782771a91808c8af9bbb7a9871389', + url: '#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389', + options: {}, + description: 'Gerrit Site Administrators', + group_id: 1, + owner: 'Administrators', + owner_id: '6a1e70e1a88782771a91808c8af9bbb7a9871389', + name: 'Administrators', + }; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - }); - element = fixture('basic'); - groupStub = sandbox.stub( - element.$.restAPI, - 'getGroupConfig', - () => Promise.resolve(group) - ); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, }); + element = fixture('basic'); + groupStub = sandbox.stub( + element.$.restAPI, + 'getGroupConfig', + () => Promise.resolve(group) + ); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('loading displays before group config is loaded', () => { - assert.isTrue(element.$.loading.classList.contains('loading')); - assert.isFalse(getComputedStyle(element.$.loading).display === 'none'); - assert.isTrue(element.$.loadedContent.classList.contains('loading')); - assert.isTrue(getComputedStyle(element.$.loadedContent) - .display === 'none'); - }); + test('loading displays before group config is loaded', () => { + assert.isTrue(element.$.loading.classList.contains('loading')); + assert.isFalse(getComputedStyle(element.$.loading).display === 'none'); + assert.isTrue(element.$.loadedContent.classList.contains('loading')); + assert.isTrue(getComputedStyle(element.$.loadedContent) + .display === 'none'); + }); - test('default values are populated with internal group', done => { - sandbox.stub( - element.$.restAPI, - 'getIsGroupOwner', - () => Promise.resolve(true)); - element.groupId = 1; - element._loadGroup().then(() => { - assert.isTrue(element._groupIsInternal); - assert.isFalse(element.$.visibleToAll.bindValue); - done(); - }); - }); - - test('default values with external group', done => { - const groupExternal = Object.assign({}, group); - groupExternal.id = 'external-group-id'; - groupStub.restore(); - groupStub = sandbox.stub( - element.$.restAPI, - 'getGroupConfig', - () => Promise.resolve(groupExternal)); - sandbox.stub( - element.$.restAPI, - 'getIsGroupOwner', - () => Promise.resolve(true)); - element.groupId = 1; - element._loadGroup().then(() => { - assert.isFalse(element._groupIsInternal); - assert.isFalse(element.$.visibleToAll.bindValue); - done(); - }); - }); - - test('rename group', done => { - const groupName = 'test-group'; - const groupName2 = 'test-group2'; - element.groupId = 1; - element._groupConfig = { - name: groupName, - }; - element._groupConfigOwner = 'testId'; - element._groupName = groupName; - element._groupOwner = true; - - sandbox.stub( - element.$.restAPI, - 'getIsGroupOwner', - () => Promise.resolve(true)); - - sandbox.stub( - element.$.restAPI, - 'saveGroupName', - () => Promise.resolve({status: 200})); - - const button = element.$.inputUpdateNameBtn; - - element._loadGroup().then(() => { - assert.isTrue(button.hasAttribute('disabled')); - assert.isFalse(element.$.Title.classList.contains('edited')); - - element.$.groupNameInput.text = groupName2; - - element.$.groupOwnerInput.text = 'testId2'; - - assert.isFalse(button.hasAttribute('disabled')); - assert.isTrue(element.$.groupName.classList.contains('edited')); - - element._handleSaveName().then(() => { - assert.isTrue(button.hasAttribute('disabled')); - assert.isFalse(element.$.Title.classList.contains('edited')); - assert.equal(element._groupName, groupName2); - done(); - }); - - element._handleSaveOwner().then(() => { - assert.isTrue(button.hasAttribute('disabled')); - assert.isFalse(element.$.Title.classList.contains('edited')); - assert.equal(element._groupConfigOwner, 'testId2'); - done(); - }); - }); - }); - - test('test for undefined group name', done => { - groupStub.restore(); - - sandbox.stub( - element.$.restAPI, - 'getGroupConfig', - () => Promise.resolve({})); - - assert.isUndefined(element.groupId); - - element.groupId = 1; - - assert.isDefined(element.groupId); - - // Test that loading shows instead of filling - // in group details - element._loadGroup().then(() => { - assert.isTrue(element.$.loading.classList.contains('loading')); - - assert.isTrue(element._loading); - - done(); - }); - }); - - test('test fire event', done => { - element._groupConfig = { - name: 'test-group', - }; - - sandbox.stub(element.$.restAPI, 'saveGroupName') - .returns(Promise.resolve({status: 200})); - - const showStub = sandbox.stub(element, 'fire'); - element._handleSaveName() - .then(() => { - assert.isTrue(showStub.called); - done(); - }); - }); - - test('_computeGroupDisabled', () => { - let admin = true; - let owner = false; - let groupIsInternal = true; - assert.equal(element._computeGroupDisabled(owner, admin, - groupIsInternal), false); - - admin = false; - assert.equal(element._computeGroupDisabled(owner, admin, - groupIsInternal), true); - - owner = true; - assert.equal(element._computeGroupDisabled(owner, admin, - groupIsInternal), false); - - owner = false; - assert.equal(element._computeGroupDisabled(owner, admin, - groupIsInternal), true); - - groupIsInternal = false; - assert.equal(element._computeGroupDisabled(owner, admin, - groupIsInternal), true); - - admin = true; - assert.equal(element._computeGroupDisabled(owner, admin, - groupIsInternal), true); - }); - - test('_computeLoadingClass', () => { - assert.equal(element._computeLoadingClass(true), 'loading'); - assert.equal(element._computeLoadingClass(false), ''); - }); - - test('fires page-error', done => { - groupStub.restore(); - - element.groupId = 1; - - const response = {status: 404}; - sandbox.stub( - element.$.restAPI, 'getGroupConfig', (group, errFn) => { - errFn(response); - }); - - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element._loadGroup(); + test('default values are populated with internal group', done => { + sandbox.stub( + element.$.restAPI, + 'getIsGroupOwner', + () => Promise.resolve(true)); + element.groupId = 1; + element._loadGroup().then(() => { + assert.isTrue(element._groupIsInternal); + assert.isFalse(element.$.visibleToAll.bindValue); + done(); }); }); + + test('default values with external group', done => { + const groupExternal = Object.assign({}, group); + groupExternal.id = 'external-group-id'; + groupStub.restore(); + groupStub = sandbox.stub( + element.$.restAPI, + 'getGroupConfig', + () => Promise.resolve(groupExternal)); + sandbox.stub( + element.$.restAPI, + 'getIsGroupOwner', + () => Promise.resolve(true)); + element.groupId = 1; + element._loadGroup().then(() => { + assert.isFalse(element._groupIsInternal); + assert.isFalse(element.$.visibleToAll.bindValue); + done(); + }); + }); + + test('rename group', done => { + const groupName = 'test-group'; + const groupName2 = 'test-group2'; + element.groupId = 1; + element._groupConfig = { + name: groupName, + }; + element._groupConfigOwner = 'testId'; + element._groupName = groupName; + element._groupOwner = true; + + sandbox.stub( + element.$.restAPI, + 'getIsGroupOwner', + () => Promise.resolve(true)); + + sandbox.stub( + element.$.restAPI, + 'saveGroupName', + () => Promise.resolve({status: 200})); + + const button = element.$.inputUpdateNameBtn; + + element._loadGroup().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + + element.$.groupNameInput.text = groupName2; + + element.$.groupOwnerInput.text = 'testId2'; + + assert.isFalse(button.hasAttribute('disabled')); + assert.isTrue(element.$.groupName.classList.contains('edited')); + + element._handleSaveName().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + assert.equal(element._groupName, groupName2); + done(); + }); + + element._handleSaveOwner().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + assert.equal(element._groupConfigOwner, 'testId2'); + done(); + }); + }); + }); + + test('test for undefined group name', done => { + groupStub.restore(); + + sandbox.stub( + element.$.restAPI, + 'getGroupConfig', + () => Promise.resolve({})); + + assert.isUndefined(element.groupId); + + element.groupId = 1; + + assert.isDefined(element.groupId); + + // Test that loading shows instead of filling + // in group details + element._loadGroup().then(() => { + assert.isTrue(element.$.loading.classList.contains('loading')); + + assert.isTrue(element._loading); + + done(); + }); + }); + + test('test fire event', done => { + element._groupConfig = { + name: 'test-group', + }; + + sandbox.stub(element.$.restAPI, 'saveGroupName') + .returns(Promise.resolve({status: 200})); + + const showStub = sandbox.stub(element, 'fire'); + element._handleSaveName() + .then(() => { + assert.isTrue(showStub.called); + done(); + }); + }); + + test('_computeGroupDisabled', () => { + let admin = true; + let owner = false; + let groupIsInternal = true; + assert.equal(element._computeGroupDisabled(owner, admin, + groupIsInternal), false); + + admin = false; + assert.equal(element._computeGroupDisabled(owner, admin, + groupIsInternal), true); + + owner = true; + assert.equal(element._computeGroupDisabled(owner, admin, + groupIsInternal), false); + + owner = false; + assert.equal(element._computeGroupDisabled(owner, admin, + groupIsInternal), true); + + groupIsInternal = false; + assert.equal(element._computeGroupDisabled(owner, admin, + groupIsInternal), true); + + admin = true; + assert.equal(element._computeGroupDisabled(owner, admin, + groupIsInternal), true); + }); + + test('_computeLoadingClass', () => { + assert.equal(element._computeLoadingClass(true), 'loading'); + assert.equal(element._computeLoadingClass(false), ''); + }); + + test('fires page-error', done => { + groupStub.restore(); + + element.groupId = 1; + + const response = {status: 404}; + sandbox.stub( + element.$.restAPI, 'getGroupConfig', (group, errFn) => { + errFn(response); + }); + + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + element._loadGroup(); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js index 508c3a2..4c5bbb8 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.js
@@ -14,301 +14,318 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const MAX_AUTOCOMPLETE_RESULTS = 20; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-access-behavior/gr-access-behavior.js'; +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-menu-page-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-rule-editor/gr-rule-editor.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-permission_html.js'; - const RANGE_NAMES = [ - 'QUERY LIMIT', - 'BATCH CHANGES LIMIT', - ]; +const MAX_AUTOCOMPLETE_RESULTS = 20; +const RANGE_NAMES = [ + 'QUERY LIMIT', + 'BATCH CHANGES LIMIT', +]; + +/** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.FireMixin + */ +/** + * Fired when the permission has been modified or removed. + * + * @event access-modified + */ +/** + * Fired when a permission that was previously added was removed. + * + * @event added-permission-removed + * @extends Polymer.Element + */ +class GrPermission extends mixinBehaviors( [ + Gerrit.AccessBehavior, /** - * @appliesMixin Gerrit.AccessMixin - * @appliesMixin Gerrit.FireMixin + * Unused in this element, but called by other elements in tests + * e.g gr-access-section_test. */ - /** - * Fired when the permission has been modified or removed. - * - * @event access-modified - */ - /** - * Fired when a permission that was previously added was removed. - * - * @event added-permission-removed - * @extends Polymer.Element - */ - class GrPermission extends Polymer.mixinBehaviors( [ - Gerrit.AccessBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-access-section_test. - */ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-permission'; } + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - static get properties() { - return { - labels: Object, - name: String, - /** @type {?} */ - permission: { - type: Object, - observer: '_sortPermission', - notify: true, + static get is() { return 'gr-permission'; } + + static get properties() { + return { + labels: Object, + name: String, + /** @type {?} */ + permission: { + type: Object, + observer: '_sortPermission', + notify: true, + }, + groups: Object, + section: String, + editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + _label: { + type: Object, + computed: '_computeLabel(permission, labels)', + }, + _groupFilter: String, + _query: { + type: Function, + value() { + return this._getGroupSuggestions.bind(this); }, - groups: Object, - section: String, - editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - _label: { - type: Object, - computed: '_computeLabel(permission, labels)', - }, - _groupFilter: String, - _query: { - type: Function, - value() { - return this._getGroupSuggestions.bind(this); - }, - }, - _rules: Array, - _groupsWithRules: Object, - _deleted: { - type: Boolean, - value: false, - }, - _originalExclusiveValue: Boolean, - }; - } + }, + _rules: Array, + _groupsWithRules: Object, + _deleted: { + type: Boolean, + value: false, + }, + _originalExclusiveValue: Boolean, + }; + } - static get observers() { - return [ - '_handleRulesChanged(_rules.splices)', - ]; - } + static get observers() { + return [ + '_handleRulesChanged(_rules.splices)', + ]; + } - /** @override */ - created() { - super.created(); - this.addEventListener('access-saved', - () => this._handleAccessSaved()); - } + /** @override */ + created() { + super.created(); + this.addEventListener('access-saved', + () => this._handleAccessSaved()); + } - /** @override */ - ready() { - super.ready(); - this._setupValues(); - } + /** @override */ + ready() { + super.ready(); + this._setupValues(); + } - _setupValues() { - if (!this.permission) { return; } - this._originalExclusiveValue = !!this.permission.value.exclusive; - Polymer.dom.flush(); - } + _setupValues() { + if (!this.permission) { return; } + this._originalExclusiveValue = !!this.permission.value.exclusive; + flush(); + } - _handleAccessSaved() { - // Set a new 'original' value to keep track of after the value has been - // saved. - this._setupValues(); - } + _handleAccessSaved() { + // Set a new 'original' value to keep track of after the value has been + // saved. + this._setupValues(); + } - _permissionIsOwnerOrGlobal(permissionId, section) { - return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES'; - } + _permissionIsOwnerOrGlobal(permissionId, section) { + return permissionId === 'owner' || section === 'GLOBAL_CAPABILITIES'; + } - _handleEditingChanged(editing, editingOld) { - // Ignore when editing gets set initially. - if (!editingOld) { return; } - // Restore original values if no longer editing. - if (!editing) { - this._deleted = false; - delete this.permission.value.deleted; - this._groupFilter = ''; - this._rules = this._rules.filter(rule => !rule.value.added); - for (const key of Object.keys(this.permission.value.rules)) { - if (this.permission.value.rules[key].added) { - delete this.permission.value.rules[key]; - } - } - - // Restore exclusive bit to original. - this.set(['permission', 'value', 'exclusive'], - this._originalExclusiveValue); - } - } - - _handleAddedRuleRemoved(e) { - const index = e.model.index; - this._rules = this._rules.slice(0, index) - .concat(this._rules.slice(index + 1, this._rules.length)); - } - - _handleValueChange() { - this.permission.value.modified = true; - // Allows overall access page to know a change has been made. - this.dispatchEvent( - new CustomEvent('access-modified', {bubbles: true, composed: true})); - } - - _handleRemovePermission() { - if (this.permission.value.added) { - this.dispatchEvent(new CustomEvent( - 'added-permission-removed', {bubbles: true, composed: true})); - } - this._deleted = true; - this.permission.value.deleted = true; - this.dispatchEvent( - new CustomEvent('access-modified', {bubbles: true, composed: true})); - } - - _handleRulesChanged(changeRecord) { - // Update the groups to exclude in the autocomplete. - this._groupsWithRules = this._computeGroupsWithRules(this._rules); - } - - _sortPermission(permission) { - this._rules = this.toSortedArray(permission.value.rules); - } - - _computeSectionClass(editing, deleted) { - const classList = []; - if (editing) { - classList.push('editing'); - } - if (deleted) { - classList.push('deleted'); - } - return classList.join(' '); - } - - _handleUndoRemove() { + _handleEditingChanged(editing, editingOld) { + // Ignore when editing gets set initially. + if (!editingOld) { return; } + // Restore original values if no longer editing. + if (!editing) { this._deleted = false; delete this.permission.value.deleted; - } - - _computeLabel(permission, labels) { - if (!labels || !permission || - !permission.value || !permission.value.label) { return; } - - const labelName = permission.value.label; - - // It is possible to have a label name that is not included in the - // 'labels' object. In this case, treat it like anything else. - if (!labels[labelName]) { return; } - const label = { - name: labelName, - values: this._computeLabelValues(labels[labelName].values), - }; - return label; - } - - _computeLabelValues(values) { - const valuesArr = []; - const keys = Object.keys(values) - .sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); - - for (const key of keys) { - let text = values[key]; - if (!text) { text = ''; } - // The value from the server being used to choose which item is - // selected is in integer form, so this must be converted. - valuesArr.push({value: parseInt(key, 10), text}); - } - return valuesArr; - } - - /** - * @param {!Array} rules - * @return {!Object} Object with groups with rues as keys, and true as - * value. - */ - _computeGroupsWithRules(rules) { - const groups = {}; - for (const rule of rules) { - groups[rule.id] = true; - } - return groups; - } - - _computeGroupName(groups, groupId) { - return groups && groups[groupId] && groups[groupId].name ? - groups[groupId].name : groupId; - } - - _getGroupSuggestions() { - return this.$.restAPI.getSuggestedGroups( - this._groupFilter, - MAX_AUTOCOMPLETE_RESULTS) - .then(response => { - const groups = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - groups.push({ - name: key, - value: response[key], - }); - } - // Does not return groups in which we already have rules for. - return groups - .filter(group => !this._groupsWithRules[group.value.id]); - }); - } - - /** - * Handles adding a skeleton item to the dom-repeat. - * gr-rule-editor handles setting the default values. - */ - _handleAddRuleItem(e) { - // The group id is encoded, but have to decode in order for the access - // API to work as expected. - const groupId = decodeURIComponent(e.detail.value.id) - .replace(/\+/g, ' '); - // We cannot use "this.set(...)" here, because groupId may contain dots, - // and dots in property path names are totally unsupported by Polymer. - // Apparently Polymer picks up this change anyway, otherwise we should - // have looked at using MutableData: - // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data - this.permission.value.rules[groupId] = {}; - - // Purposely don't recompute sorted array so that the newly added rule - // is the last item of the array. - this.push('_rules', { - id: groupId, - }); - - // Add the new group name to the groups object so the name renders - // correctly. - if (this.groups && !this.groups[groupId]) { - this.groups[groupId] = {name: this.$.groupAutocomplete.text}; + this._groupFilter = ''; + this._rules = this._rules.filter(rule => !rule.value.added); + for (const key of Object.keys(this.permission.value.rules)) { + if (this.permission.value.rules[key].added) { + delete this.permission.value.rules[key]; + } } - // Wait for new rule to get value populated via gr-rule-editor, and then - // add to permission values as well, so that the change gets propogated - // back to the section. Since the rule is inside a dom-repeat, a flush - // is needed. - Polymer.dom.flush(); - const value = this._rules[this._rules.length - 1].value; - value.added = true; - // See comment above for why we cannot use "this.set(...)" here. - this.permission.value.rules[groupId] = value; - this.dispatchEvent( - new CustomEvent('access-modified', {bubbles: true, composed: true})); - } - - _computeHasRange(name) { - if (!name) { return false; } - - return RANGE_NAMES.includes(name.toUpperCase()); + // Restore exclusive bit to original. + this.set(['permission', 'value', 'exclusive'], + this._originalExclusiveValue); } } - customElements.define(GrPermission.is, GrPermission); -})(); + _handleAddedRuleRemoved(e) { + const index = e.model.index; + this._rules = this._rules.slice(0, index) + .concat(this._rules.slice(index + 1, this._rules.length)); + } + + _handleValueChange() { + this.permission.value.modified = true; + // Allows overall access page to know a change has been made. + this.dispatchEvent( + new CustomEvent('access-modified', {bubbles: true, composed: true})); + } + + _handleRemovePermission() { + if (this.permission.value.added) { + this.dispatchEvent(new CustomEvent( + 'added-permission-removed', {bubbles: true, composed: true})); + } + this._deleted = true; + this.permission.value.deleted = true; + this.dispatchEvent( + new CustomEvent('access-modified', {bubbles: true, composed: true})); + } + + _handleRulesChanged(changeRecord) { + // Update the groups to exclude in the autocomplete. + this._groupsWithRules = this._computeGroupsWithRules(this._rules); + } + + _sortPermission(permission) { + this._rules = this.toSortedArray(permission.value.rules); + } + + _computeSectionClass(editing, deleted) { + const classList = []; + if (editing) { + classList.push('editing'); + } + if (deleted) { + classList.push('deleted'); + } + return classList.join(' '); + } + + _handleUndoRemove() { + this._deleted = false; + delete this.permission.value.deleted; + } + + _computeLabel(permission, labels) { + if (!labels || !permission || + !permission.value || !permission.value.label) { return; } + + const labelName = permission.value.label; + + // It is possible to have a label name that is not included in the + // 'labels' object. In this case, treat it like anything else. + if (!labels[labelName]) { return; } + const label = { + name: labelName, + values: this._computeLabelValues(labels[labelName].values), + }; + return label; + } + + _computeLabelValues(values) { + const valuesArr = []; + const keys = Object.keys(values) + .sort((a, b) => parseInt(a, 10) - parseInt(b, 10)); + + for (const key of keys) { + let text = values[key]; + if (!text) { text = ''; } + // The value from the server being used to choose which item is + // selected is in integer form, so this must be converted. + valuesArr.push({value: parseInt(key, 10), text}); + } + return valuesArr; + } + + /** + * @param {!Array} rules + * @return {!Object} Object with groups with rues as keys, and true as + * value. + */ + _computeGroupsWithRules(rules) { + const groups = {}; + for (const rule of rules) { + groups[rule.id] = true; + } + return groups; + } + + _computeGroupName(groups, groupId) { + return groups && groups[groupId] && groups[groupId].name ? + groups[groupId].name : groupId; + } + + _getGroupSuggestions() { + return this.$.restAPI.getSuggestedGroups( + this._groupFilter, + MAX_AUTOCOMPLETE_RESULTS) + .then(response => { + const groups = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + groups.push({ + name: key, + value: response[key], + }); + } + // Does not return groups in which we already have rules for. + return groups + .filter(group => !this._groupsWithRules[group.value.id]); + }); + } + + /** + * Handles adding a skeleton item to the dom-repeat. + * gr-rule-editor handles setting the default values. + */ + _handleAddRuleItem(e) { + // The group id is encoded, but have to decode in order for the access + // API to work as expected. + const groupId = decodeURIComponent(e.detail.value.id) + .replace(/\+/g, ' '); + // We cannot use "this.set(...)" here, because groupId may contain dots, + // and dots in property path names are totally unsupported by Polymer. + // Apparently Polymer picks up this change anyway, otherwise we should + // have looked at using MutableData: + // https://polymer-library.polymer-project.org/2.0/docs/devguide/data-system#mutable-data + this.permission.value.rules[groupId] = {}; + + // Purposely don't recompute sorted array so that the newly added rule + // is the last item of the array. + this.push('_rules', { + id: groupId, + }); + + // Add the new group name to the groups object so the name renders + // correctly. + if (this.groups && !this.groups[groupId]) { + this.groups[groupId] = {name: this.$.groupAutocomplete.text}; + } + + // Wait for new rule to get value populated via gr-rule-editor, and then + // add to permission values as well, so that the change gets propogated + // back to the section. Since the rule is inside a dom-repeat, a flush + // is needed. + flush(); + const value = this._rules[this._rules.length - 1].value; + value.added = true; + // See comment above for why we cannot use "this.set(...)" here. + this.permission.value.rules[groupId] = value; + this.dispatchEvent( + new CustomEvent('access-modified', {bubbles: true, composed: true})); + } + + _computeHasRange(name) { + if (!name) { return false; } + + return RANGE_NAMES.includes(name.toUpperCase()); + } +} + +customElements.define(GrPermission.is, GrPermission);
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js index e07f911..1b57336 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html"> -<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-menu-page-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-rule-editor/gr-rule-editor.html"> - -<dom-module id="gr-permission"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -88,49 +76,23 @@ <style include="gr-menu-page-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <section - id="permission" - class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"> + <section id="permission" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"> <div id="mainContainer"> <div class="header"> <span class="title">[[name]]</span> <div class="right"> - <template is=dom-if if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]"> - <paper-toggle-button - id="exclusiveToggle" - checked="{{permission.value.exclusive}}" - on-change="_handleValueChange" - disabled$="[[!editing]]"></paper-toggle-button>Exclusive + <template is="dom-if" if="[[!_permissionIsOwnerOrGlobal(permission.id, section)]]"> + <paper-toggle-button id="exclusiveToggle" checked="{{permission.value.exclusive}}" on-change="_handleValueChange" disabled\$="[[!editing]]"></paper-toggle-button>Exclusive </template> - <gr-button - link - id="removeBtn" - on-click="_handleRemovePermission">Remove</gr-button> + <gr-button link="" id="removeBtn" on-click="_handleRemovePermission">Remove</gr-button> </div> </div><!-- end header --> <div class="rules"> - <template - is="dom-repeat" - items="{{_rules}}" - as="rule"> - <gr-rule-editor - has-range="[[_computeHasRange(name)]]" - label="[[_label]]" - editing="[[editing]]" - group-id="[[rule.id]]" - group-name="[[_computeGroupName(groups, rule.id)]]" - permission="[[permission.id]]" - rule="{{rule}}" - section="[[section]]" - on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor> + <template is="dom-repeat" items="{{_rules}}" as="rule"> + <gr-rule-editor has-range="[[_computeHasRange(name)]]" label="[[_label]]" editing="[[editing]]" group-id="[[rule.id]]" group-name="[[_computeGroupName(groups, rule.id)]]" permission="[[permission.id]]" rule="{{rule}}" section="[[section]]" on-added-rule-removed="_handleAddedRuleRemoved"></gr-rule-editor> </template> <div id="addRule"> - <gr-autocomplete - id="groupAutocomplete" - text="{{_groupFilter}}" - query="[[_query]]" - placeholder="Add group" - on-commit="_handleAddRuleItem"> + <gr-autocomplete id="groupAutocomplete" text="{{_groupFilter}}" query="[[_query]]" placeholder="Add group" on-commit="_handleAddRuleItem"> </gr-autocomplete> </div> <!-- end addRule --> @@ -138,13 +100,8 @@ </div><!-- end mainContainer --> <div id="deletedContainer"> <span>[[name]] was deleted</span> - <gr-button - link - id="undoRemoveBtn" - on-click="_handleUndoRemove">Undo</gr-button> + <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button> </div><!-- end deletedContainer --> </section> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-permission.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html index f3c1e4f..6f05029 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-permission</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-permission.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-permission.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-permission.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,399 +41,401 @@ </template> </test-fixture> -<script> - suite('gr-permission tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-permission.js'; +suite('gr-permission tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns( - Promise.resolve({ - 'Administrators': { - id: '4c97682e6ce61b7247f3381b6f1789356666de7f', + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + sandbox.stub(element.$.restAPI, 'getSuggestedGroups').returns( + Promise.resolve({ + 'Administrators': { + id: '4c97682e6ce61b7247f3381b6f1789356666de7f', + }, + 'Anonymous Users': { + id: 'global%3AAnonymous-Users', + }, + })); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('unit tests', () => { + test('_sortPermission', () => { + const permission = { + id: 'submit', + value: { + rules: { + 'global:Project-Owners': { + action: 'ALLOW', + force: false, }, - 'Anonymous Users': { - id: 'global%3AAnonymous-Users', + '4c97682e6ce6b7247f3381b6f1789356666de7f': { + action: 'ALLOW', + force: false, }, - })); + }, + }, + }; + + const expectedRules = [ + { + id: '4c97682e6ce6b7247f3381b6f1789356666de7f', + value: {action: 'ALLOW', force: false}, + }, + { + id: 'global:Project-Owners', + value: {action: 'ALLOW', force: false}, + }, + ]; + + element._sortPermission(permission); + assert.deepEqual(element._rules, expectedRules); }); - teardown(() => { - sandbox.restore(); + test('_computeLabel and _computeLabelValues', () => { + const labels = { + 'Code-Review': { + default_value: 0, + values: { + ' 0': 'No score', + '-1': 'I would prefer this is not merged as is', + '-2': 'This shall not be merged', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + }, + }; + let permission = { + id: 'label-Code-Review', + value: { + label: 'Code-Review', + rules: { + 'global:Project-Owners': { + action: 'ALLOW', + force: false, + min: -2, + max: 2, + }, + '4c97682e6ce6b7247f3381b6f1789356666de7f': { + action: 'ALLOW', + force: false, + min: -2, + max: 2, + }, + }, + }, + }; + + const expectedLabelValues = [ + {value: -2, text: 'This shall not be merged'}, + {value: -1, text: 'I would prefer this is not merged as is'}, + {value: 0, text: 'No score'}, + {value: 1, text: 'Looks good to me, but someone else must approve'}, + {value: 2, text: 'Looks good to me, approved'}, + ]; + + const expectedLabel = { + name: 'Code-Review', + values: expectedLabelValues, + }; + + assert.deepEqual(element._computeLabelValues( + labels['Code-Review'].values), expectedLabelValues); + + assert.deepEqual(element._computeLabel(permission, labels), + expectedLabel); + + permission = { + id: 'label-reviewDB', + value: { + label: 'reviewDB', + rules: { + 'global:Project-Owners': { + action: 'ALLOW', + force: false, + }, + '4c97682e6ce6b7247f3381b6f1789356666de7f': { + action: 'ALLOW', + force: false, + }, + }, + }, + }; + + assert.isNotOk(element._computeLabel(permission, labels)); }); - suite('unit tests', () => { - test('_sortPermission', () => { - const permission = { - id: 'submit', - value: { - rules: { - 'global:Project-Owners': { - action: 'ALLOW', - force: false, - }, - '4c97682e6ce6b7247f3381b6f1789356666de7f': { - action: 'ALLOW', - force: false, - }, - }, - }, - }; + test('_computeSectionClass', () => { + let deleted = true; + let editing = false; + assert.equal(element._computeSectionClass(editing, deleted), 'deleted'); - const expectedRules = [ + deleted = false; + assert.equal(element._computeSectionClass(editing, deleted), ''); + + editing = true; + assert.equal(element._computeSectionClass(editing, deleted), 'editing'); + + deleted = true; + assert.equal(element._computeSectionClass(editing, deleted), + 'editing deleted'); + }); + + test('_computeGroupName', () => { + const groups = { + abc123: {name: 'test group'}, + bcd234: {}, + }; + assert.equal(element._computeGroupName(groups, 'abc123'), 'test group'); + assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234'); + }); + + test('_computeGroupsWithRules', () => { + const rules = [ + { + id: '4c97682e6ce6b7247f3381b6f1789356666de7f', + value: {action: 'ALLOW', force: false}, + }, + { + id: 'global:Project-Owners', + value: {action: 'ALLOW', force: false}, + }, + ]; + const groupsWithRules = { + '4c97682e6ce6b7247f3381b6f1789356666de7f': true, + 'global:Project-Owners': true, + }; + assert.deepEqual(element._computeGroupsWithRules(rules), + groupsWithRules); + }); + + test('_getGroupSuggestions without existing rules', done => { + element._groupsWithRules = {}; + + element._getGroupSuggestions().then(groups => { + assert.deepEqual(groups, [ { - id: '4c97682e6ce6b7247f3381b6f1789356666de7f', - value: {action: 'ALLOW', force: false}, - }, - { - id: 'global:Project-Owners', - value: {action: 'ALLOW', force: false}, - }, - ]; - - element._sortPermission(permission); - assert.deepEqual(element._rules, expectedRules); - }); - - test('_computeLabel and _computeLabelValues', () => { - const labels = { - 'Code-Review': { - default_value: 0, - values: { - ' 0': 'No score', - '-1': 'I would prefer this is not merged as is', - '-2': 'This shall not be merged', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', - }, - }, - }; - let permission = { - id: 'label-Code-Review', - value: { - label: 'Code-Review', - rules: { - 'global:Project-Owners': { - action: 'ALLOW', - force: false, - min: -2, - max: 2, - }, - '4c97682e6ce6b7247f3381b6f1789356666de7f': { - action: 'ALLOW', - force: false, - min: -2, - max: 2, - }, - }, - }, - }; - - const expectedLabelValues = [ - {value: -2, text: 'This shall not be merged'}, - {value: -1, text: 'I would prefer this is not merged as is'}, - {value: 0, text: 'No score'}, - {value: 1, text: 'Looks good to me, but someone else must approve'}, - {value: 2, text: 'Looks good to me, approved'}, - ]; - - const expectedLabel = { - name: 'Code-Review', - values: expectedLabelValues, - }; - - assert.deepEqual(element._computeLabelValues( - labels['Code-Review'].values), expectedLabelValues); - - assert.deepEqual(element._computeLabel(permission, labels), - expectedLabel); - - permission = { - id: 'label-reviewDB', - value: { - label: 'reviewDB', - rules: { - 'global:Project-Owners': { - action: 'ALLOW', - force: false, - }, - '4c97682e6ce6b7247f3381b6f1789356666de7f': { - action: 'ALLOW', - force: false, - }, - }, - }, - }; - - assert.isNotOk(element._computeLabel(permission, labels)); - }); - - test('_computeSectionClass', () => { - let deleted = true; - let editing = false; - assert.equal(element._computeSectionClass(editing, deleted), 'deleted'); - - deleted = false; - assert.equal(element._computeSectionClass(editing, deleted), ''); - - editing = true; - assert.equal(element._computeSectionClass(editing, deleted), 'editing'); - - deleted = true; - assert.equal(element._computeSectionClass(editing, deleted), - 'editing deleted'); - }); - - test('_computeGroupName', () => { - const groups = { - abc123: {name: 'test group'}, - bcd234: {}, - }; - assert.equal(element._computeGroupName(groups, 'abc123'), 'test group'); - assert.equal(element._computeGroupName(groups, 'bcd234'), 'bcd234'); - }); - - test('_computeGroupsWithRules', () => { - const rules = [ - { - id: '4c97682e6ce6b7247f3381b6f1789356666de7f', - value: {action: 'ALLOW', force: false}, - }, - { - id: 'global:Project-Owners', - value: {action: 'ALLOW', force: false}, - }, - ]; - const groupsWithRules = { - '4c97682e6ce6b7247f3381b6f1789356666de7f': true, - 'global:Project-Owners': true, - }; - assert.deepEqual(element._computeGroupsWithRules(rules), - groupsWithRules); - }); - - test('_getGroupSuggestions without existing rules', done => { - element._groupsWithRules = {}; - - element._getGroupSuggestions().then(groups => { - assert.deepEqual(groups, [ - { - name: 'Administrators', - value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'}, - }, { - name: 'Anonymous Users', - value: {id: 'global%3AAnonymous-Users'}, - }, - ]); - done(); - }); - }); - - test('_getGroupSuggestions with existing rules filters them', done => { - element._groupsWithRules = { - '4c97682e6ce61b7247f3381b6f1789356666de7f': true, - }; - - element._getGroupSuggestions().then(groups => { - assert.deepEqual(groups, [{ + name: 'Administrators', + value: {id: '4c97682e6ce61b7247f3381b6f1789356666de7f'}, + }, { name: 'Anonymous Users', value: {id: 'global%3AAnonymous-Users'}, - }]); - done(); - }); - }); - - test('_handleRemovePermission', () => { - element.editing = true; - element.permission = {value: {rules: {}}}; - element._handleRemovePermission(); - assert.isTrue(element._deleted); - assert.isTrue(element.permission.value.deleted); - - element.editing = false; - assert.isFalse(element._deleted); - assert.isNotOk(element.permission.value.deleted); - }); - - test('_handleUndoRemove', () => { - element.permission = {value: {deleted: true, rules: {}}}; - element._handleUndoRemove(); - assert.isFalse(element._deleted); - assert.isNotOk(element.permission.value.deleted); - }); - - test('_computeHasRange', () => { - assert.isTrue(element._computeHasRange('Query Limit')); - - assert.isTrue(element._computeHasRange('Batch Changes Limit')); - - assert.isFalse(element._computeHasRange('test')); + }, + ]); + done(); }); }); - suite('interactions', () => { - setup(() => { - sandbox.spy(element, '_computeLabel'); - element.name = 'Priority'; - element.section = 'refs/*'; - element.labels = { - 'Code-Review': { - values: { - ' 0': 'No score', - '-1': 'I would prefer this is not merged as is', - '-2': 'This shall not be merged', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', - }, - default_value: 0, - }, - }; - element.permission = { - id: 'label-Code-Review', - value: { - label: 'Code-Review', - rules: { - 'global:Project-Owners': { - action: 'ALLOW', - force: false, - min: -2, - max: 2, - }, - '4c97682e6ce6b7247f3381b6f1789356666de7f': { - action: 'ALLOW', - force: false, - min: -2, - max: 2, - }, - }, - }, - }; - element._setupValues(); - flushAsynchronousOperations(); + test('_getGroupSuggestions with existing rules filters them', done => { + element._groupsWithRules = { + '4c97682e6ce61b7247f3381b6f1789356666de7f': true, + }; + + element._getGroupSuggestions().then(groups => { + assert.deepEqual(groups, [{ + name: 'Anonymous Users', + value: {id: 'global%3AAnonymous-Users'}, + }]); + done(); }); + }); - test('adding a rule', () => { - element.name = 'Priority'; - element.section = 'refs/*'; - element.groups = {}; - element.$.groupAutocomplete.text = 'ldap/tests te.st'; - const e = { - detail: { - value: { - id: 'ldap:CN=test+te.st', - }, - }, - }; - element.editing = true; - assert.equal(element._rules.length, 2); - assert.equal(Object.keys(element._groupsWithRules).length, 2); - element._handleAddRuleItem(e); - flushAsynchronousOperations(); - assert.deepEqual(element.groups, {'ldap:CN=test te.st': { - name: 'ldap/tests te.st'}}); - assert.equal(element._rules.length, 3); - assert.equal(Object.keys(element._groupsWithRules).length, 3); - assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'], - {action: 'ALLOW', min: -2, max: 2, added: true}); - // New rule should be removed if cancel from editing. - element.editing = false; - assert.equal(element._rules.length, 2); - assert.equal(Object.keys(element.permission.value.rules).length, 2); - }); + test('_handleRemovePermission', () => { + element.editing = true; + element.permission = {value: {rules: {}}}; + element._handleRemovePermission(); + assert.isTrue(element._deleted); + assert.isTrue(element.permission.value.deleted); - test('removing an added rule', () => { - element.name = 'Priority'; - element.section = 'refs/*'; - element.groups = {}; - element.$.groupAutocomplete.text = 'new group name'; - assert.equal(element._rules.length, 2); - element.shadowRoot - .querySelector('gr-rule-editor').fire('added-rule-removed'); - flushAsynchronousOperations(); - assert.equal(element._rules.length, 1); - }); + element.editing = false; + assert.isFalse(element._deleted); + assert.isNotOk(element.permission.value.deleted); + }); - test('removing an added permission', () => { - const removeStub = sandbox.stub(); - element.addEventListener('added-permission-removed', removeStub); - element.editing = true; - element.name = 'Priority'; - element.section = 'refs/*'; - element.permission.value.added = true; - MockInteractions.tap(element.$.removeBtn); - assert.isTrue(removeStub.called); - }); + test('_handleUndoRemove', () => { + element.permission = {value: {deleted: true, rules: {}}}; + element._handleUndoRemove(); + assert.isFalse(element._deleted); + assert.isNotOk(element.permission.value.deleted); + }); - test('removing the permission', () => { - element.editing = true; - element.name = 'Priority'; - element.section = 'refs/*'; + test('_computeHasRange', () => { + assert.isTrue(element._computeHasRange('Query Limit')); - const removeStub = sandbox.stub(); - element.addEventListener('added-permission-removed', removeStub); + assert.isTrue(element._computeHasRange('Batch Changes Limit')); - assert.isFalse(element.$.permission.classList.contains('deleted')); - assert.isFalse(element._deleted); - MockInteractions.tap(element.$.removeBtn); - assert.isTrue(element.$.permission.classList.contains('deleted')); - assert.isTrue(element._deleted); - MockInteractions.tap(element.$.undoRemoveBtn); - assert.isFalse(element.$.permission.classList.contains('deleted')); - assert.isFalse(element._deleted); - assert.isFalse(removeStub.called); - }); - - test('modify a permission', () => { - element.editing = true; - element.name = 'Priority'; - element.section = 'refs/*'; - - assert.isFalse(element._originalExclusiveValue); - assert.isNotOk(element.permission.value.modified); - MockInteractions.tap(element.shadowRoot - .querySelector('#exclusiveToggle')); - flushAsynchronousOperations(); - assert.isTrue(element.permission.value.exclusive); - assert.isTrue(element.permission.value.modified); - assert.isFalse(element._originalExclusiveValue); - element.editing = false; - assert.isFalse(element.permission.value.exclusive); - }); - - test('_handleValueChange', () => { - const modifiedHandler = sandbox.stub(); - element.permission = {value: {rules: {}}}; - element.addEventListener('access-modified', modifiedHandler); - assert.isNotOk(element.permission.value.modified); - element._handleValueChange(); - assert.isTrue(element.permission.value.modified); - assert.isTrue(modifiedHandler.called); - }); - - test('Exclusive hidden for owner permission', () => { - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('#exclusiveToggle')).display, - 'flex'); - element.set(['permission', 'id'], 'owner'); - flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('#exclusiveToggle')).display, - 'none'); - }); - - test('Exclusive hidden for any global permissions', () => { - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('#exclusiveToggle')).display, - 'flex'); - element.section = 'GLOBAL_CAPABILITIES'; - flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('#exclusiveToggle')).display, - 'none'); - }); + assert.isFalse(element._computeHasRange('test')); }); }); + + suite('interactions', () => { + setup(() => { + sandbox.spy(element, '_computeLabel'); + element.name = 'Priority'; + element.section = 'refs/*'; + element.labels = { + 'Code-Review': { + values: { + ' 0': 'No score', + '-1': 'I would prefer this is not merged as is', + '-2': 'This shall not be merged', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + default_value: 0, + }, + }; + element.permission = { + id: 'label-Code-Review', + value: { + label: 'Code-Review', + rules: { + 'global:Project-Owners': { + action: 'ALLOW', + force: false, + min: -2, + max: 2, + }, + '4c97682e6ce6b7247f3381b6f1789356666de7f': { + action: 'ALLOW', + force: false, + min: -2, + max: 2, + }, + }, + }, + }; + element._setupValues(); + flushAsynchronousOperations(); + }); + + test('adding a rule', () => { + element.name = 'Priority'; + element.section = 'refs/*'; + element.groups = {}; + element.$.groupAutocomplete.text = 'ldap/tests te.st'; + const e = { + detail: { + value: { + id: 'ldap:CN=test+te.st', + }, + }, + }; + element.editing = true; + assert.equal(element._rules.length, 2); + assert.equal(Object.keys(element._groupsWithRules).length, 2); + element._handleAddRuleItem(e); + flushAsynchronousOperations(); + assert.deepEqual(element.groups, {'ldap:CN=test te.st': { + name: 'ldap/tests te.st'}}); + assert.equal(element._rules.length, 3); + assert.equal(Object.keys(element._groupsWithRules).length, 3); + assert.deepEqual(element.permission.value.rules['ldap:CN=test te.st'], + {action: 'ALLOW', min: -2, max: 2, added: true}); + // New rule should be removed if cancel from editing. + element.editing = false; + assert.equal(element._rules.length, 2); + assert.equal(Object.keys(element.permission.value.rules).length, 2); + }); + + test('removing an added rule', () => { + element.name = 'Priority'; + element.section = 'refs/*'; + element.groups = {}; + element.$.groupAutocomplete.text = 'new group name'; + assert.equal(element._rules.length, 2); + element.shadowRoot + .querySelector('gr-rule-editor').fire('added-rule-removed'); + flushAsynchronousOperations(); + assert.equal(element._rules.length, 1); + }); + + test('removing an added permission', () => { + const removeStub = sandbox.stub(); + element.addEventListener('added-permission-removed', removeStub); + element.editing = true; + element.name = 'Priority'; + element.section = 'refs/*'; + element.permission.value.added = true; + MockInteractions.tap(element.$.removeBtn); + assert.isTrue(removeStub.called); + }); + + test('removing the permission', () => { + element.editing = true; + element.name = 'Priority'; + element.section = 'refs/*'; + + const removeStub = sandbox.stub(); + element.addEventListener('added-permission-removed', removeStub); + + assert.isFalse(element.$.permission.classList.contains('deleted')); + assert.isFalse(element._deleted); + MockInteractions.tap(element.$.removeBtn); + assert.isTrue(element.$.permission.classList.contains('deleted')); + assert.isTrue(element._deleted); + MockInteractions.tap(element.$.undoRemoveBtn); + assert.isFalse(element.$.permission.classList.contains('deleted')); + assert.isFalse(element._deleted); + assert.isFalse(removeStub.called); + }); + + test('modify a permission', () => { + element.editing = true; + element.name = 'Priority'; + element.section = 'refs/*'; + + assert.isFalse(element._originalExclusiveValue); + assert.isNotOk(element.permission.value.modified); + MockInteractions.tap(element.shadowRoot + .querySelector('#exclusiveToggle')); + flushAsynchronousOperations(); + assert.isTrue(element.permission.value.exclusive); + assert.isTrue(element.permission.value.modified); + assert.isFalse(element._originalExclusiveValue); + element.editing = false; + assert.isFalse(element.permission.value.exclusive); + }); + + test('_handleValueChange', () => { + const modifiedHandler = sandbox.stub(); + element.permission = {value: {rules: {}}}; + element.addEventListener('access-modified', modifiedHandler); + assert.isNotOk(element.permission.value.modified); + element._handleValueChange(); + assert.isTrue(element.permission.value.modified); + assert.isTrue(modifiedHandler.called); + }); + + test('Exclusive hidden for owner permission', () => { + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'flex'); + element.set(['permission', 'id'], 'owner'); + flushAsynchronousOperations(); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'none'); + }); + + test('Exclusive hidden for any global permissions', () => { + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'flex'); + element.section = 'GLOBAL_CAPABILITIES'; + flushAsynchronousOperations(); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#exclusiveToggle')).display, + 'none'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js index 92a8655..318c2c3 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js +++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor.js
@@ -14,84 +14,95 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrPluginConfigArrayEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-plugin-config-array-editor'; } - /** - * Fired when the plugin config option changes. - * - * @event plugin-config-option-changed - */ +import '@polymer/iron-input/iron-input.js'; +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-plugin-config-array-editor_html.js'; - static get properties() { - return { +/** @extends Polymer.Element */ +class GrPluginConfigArrayEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-plugin-config-array-editor'; } + /** + * Fired when the plugin config option changes. + * + * @event plugin-config-option-changed + */ + + static get properties() { + return { + /** @type {?} */ + pluginOption: Object, + /** @type {boolean} */ + disabled: { + type: Boolean, + computed: '_computeDisabled(pluginOption.*)', + }, /** @type {?} */ - pluginOption: Object, - /** @type {boolean} */ - disabled: { - type: Boolean, - computed: '_computeDisabled(pluginOption.*)', - }, - /** @type {?} */ - _newValue: { - type: String, - value: '', - }, - }; - } + _newValue: { + type: String, + value: '', + }, + }; + } - _computeDisabled(record) { - return !(record && record.base && record.base.info && - record.base.info.editable); - } + _computeDisabled(record) { + return !(record && record.base && record.base.info && + record.base.info.editable); + } - _handleAddTap(e) { + _handleAddTap(e) { + e.preventDefault(); + this._handleAdd(); + } + + _handleInputKeydown(e) { + // Enter. + if (e.keyCode === 13) { e.preventDefault(); this._handleAdd(); } - - _handleInputKeydown(e) { - // Enter. - if (e.keyCode === 13) { - e.preventDefault(); - this._handleAdd(); - } - } - - _handleAdd() { - if (!this._newValue.length) { return; } - this._dispatchChanged( - this.pluginOption.info.values.concat([this._newValue])); - this._newValue = ''; - } - - _handleDelete(e) { - const value = Polymer.dom(e).localTarget.dataset.item; - this._dispatchChanged( - this.pluginOption.info.values.filter(str => str !== value)); - } - - _dispatchChanged(values) { - const {_key, info} = this.pluginOption; - const detail = { - _key, - info: Object.assign(info, {values}, {}), - notifyPath: `${_key}.values`, - }; - this.dispatchEvent( - new CustomEvent('plugin-config-option-changed', {detail})); - } - - _computeShowInputRow(disabled) { - return disabled ? 'hide' : ''; - } } - customElements.define(GrPluginConfigArrayEditor.is, - GrPluginConfigArrayEditor); -})(); + _handleAdd() { + if (!this._newValue.length) { return; } + this._dispatchChanged( + this.pluginOption.info.values.concat([this._newValue])); + this._newValue = ''; + } + + _handleDelete(e) { + const value = dom(e).localTarget.dataset.item; + this._dispatchChanged( + this.pluginOption.info.values.filter(str => str !== value)); + } + + _dispatchChanged(values) { + const {_key, info} = this.pluginOption; + const detail = { + _key, + info: Object.assign(info, {values}, {}), + notifyPath: `${_key}.values`, + }; + this.dispatchEvent( + new CustomEvent('plugin-config-option-changed', {detail})); + } + + _computeShowInputRow(disabled) { + return disabled ? 'hide' : ''; + } +} + +customElements.define(GrPluginConfigArrayEditor.is, + GrPluginConfigArrayEditor);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js index f6c744b..d97e2b37 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js +++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> - -<dom-module id="gr-plugin-config-array-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -72,11 +64,7 @@ <template is="dom-repeat" items="[[pluginOption.info.values]]"> <div class="row"> <span>[[item]]</span> - <gr-button - link - disabled$="[[disabled]]" - data-item$="[[item]]" - on-click="_handleDelete">Delete</gr-button> + <gr-button link="" disabled\$="[[disabled]]" data-item\$="[[item]]" on-click="_handleDelete">Delete</gr-button> </div> </template> </div> @@ -84,23 +72,11 @@ <template is="dom-if" if="[[!pluginOption.info.values.length]]"> <div class="row placeholder">None configured.</div> </template> - <div class$="row [[_computeShowInputRow(disabled)]]"> - <iron-input - on-keydown="_handleInputKeydown" - bind-value="{{_newValue}}"> - <input - is="iron-input" - id="input" - on-keydown="_handleInputKeydown" - bind-value="{{_newValue}}"> + <div class\$="row [[_computeShowInputRow(disabled)]]"> + <iron-input on-keydown="_handleInputKeydown" bind-value="{{_newValue}}"> + <input is="iron-input" id="input" on-keydown="_handleInputKeydown" bind-value="{{_newValue}}"> </iron-input> - <gr-button - id="addButton" - disabled$="[[!_newValue.length]]" - link - on-click="_handleAddTap">Add</gr-button> + <gr-button id="addButton" disabled\$="[[!_newValue.length]]" link="" on-click="_handleAddTap">Add</gr-button> </div> </div> - </template> - <script src="gr-plugin-config-array-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html index 3342967..f66346e 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html +++ b/polygerrit-ui/app/elements/admin/gr-plugin-config-array-editor/gr-plugin-config-array-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-config-array-editor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-plugin-config-array-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-plugin-config-array-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-config-array-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,112 +40,115 @@ </template> </test-fixture> -<script> - suite('gr-plugin-config-array-editor tests', async () => { - await readyToTest(); - let element; - let sandbox; - let dispatchStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-config-array-editor.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-plugin-config-array-editor tests', () => { + let element; + let sandbox; + let dispatchStub; - const getAll = str => Polymer.dom(element.root).querySelectorAll(str); + const getAll = str => dom(element.root).querySelectorAll(str); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.pluginOption = { + _key: 'test-key', + info: { + values: [], + }, + }; + }); + + teardown(() => sandbox.restore()); + + test('_computeShowInputRow', () => { + assert.equal(element._computeShowInputRow(true), 'hide'); + assert.equal(element._computeShowInputRow(false), ''); + }); + + test('_computeDisabled', () => { + assert.isTrue(element._computeDisabled({})); + assert.isTrue(element._computeDisabled({base: {}})); + assert.isTrue(element._computeDisabled({base: {info: {}}})); + assert.isTrue( + element._computeDisabled({base: {info: {editable: false}}})); + assert.isFalse( + element._computeDisabled({base: {info: {editable: true}}})); + }); + + suite('adding', () => { setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.pluginOption = { - _key: 'test-key', - info: { - values: [], - }, - }; - }); - - teardown(() => sandbox.restore()); - - test('_computeShowInputRow', () => { - assert.equal(element._computeShowInputRow(true), 'hide'); - assert.equal(element._computeShowInputRow(false), ''); - }); - - test('_computeDisabled', () => { - assert.isTrue(element._computeDisabled({})); - assert.isTrue(element._computeDisabled({base: {}})); - assert.isTrue(element._computeDisabled({base: {info: {}}})); - assert.isTrue( - element._computeDisabled({base: {info: {editable: false}}})); - assert.isFalse( - element._computeDisabled({base: {info: {editable: true}}})); - }); - - suite('adding', () => { - setup(() => { - dispatchStub = sandbox.stub(element, '_dispatchChanged'); - }); - - test('with enter', () => { - element._newValue = ''; - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter - flushAsynchronousOperations(); - - assert.isFalse(dispatchStub.called); - element._newValue = 'test'; - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter - flushAsynchronousOperations(); - - assert.isTrue(dispatchStub.called); - assert.equal(dispatchStub.lastCall.args[0], 'test'); - assert.equal(element._newValue, ''); - }); - - test('with add btn', () => { - element._newValue = ''; - MockInteractions.tap(element.$.addButton); - flushAsynchronousOperations(); - - assert.isFalse(dispatchStub.called); - element._newValue = 'test'; - MockInteractions.tap(element.$.addButton); - flushAsynchronousOperations(); - - assert.isTrue(dispatchStub.called); - assert.equal(dispatchStub.lastCall.args[0], 'test'); - assert.equal(element._newValue, ''); - }); - }); - - test('deleting', () => { dispatchStub = sandbox.stub(element, '_dispatchChanged'); - element.pluginOption = {info: {values: ['test', 'test2']}}; - flushAsynchronousOperations(); + }); - const rows = getAll('.existingItems .row'); - assert.equal(rows.length, 2); - const button = rows[0].querySelector('gr-button'); - - MockInteractions.tap(button); + test('with enter', () => { + element._newValue = ''; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter flushAsynchronousOperations(); assert.isFalse(dispatchStub.called); - element.pluginOption.info.editable = true; - element.notifyPath('pluginOption.info.editable'); - flushAsynchronousOperations(); - - MockInteractions.tap(button); + element._newValue = 'test'; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13); // Enter flushAsynchronousOperations(); assert.isTrue(dispatchStub.called); - assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']); + assert.equal(dispatchStub.lastCall.args[0], 'test'); + assert.equal(element._newValue, ''); }); - test('_dispatchChanged', () => { - const eventStub = sandbox.stub(element, 'dispatchEvent'); - element._dispatchChanged(['new-test-value']); + test('with add btn', () => { + element._newValue = ''; + MockInteractions.tap(element.$.addButton); + flushAsynchronousOperations(); - assert.isTrue(eventStub.called); - const {detail} = eventStub.lastCall.args[0]; - assert.equal(detail._key, 'test-key'); - assert.deepEqual(detail.info, {values: ['new-test-value']}); - assert.equal(detail.notifyPath, 'test-key.values'); + assert.isFalse(dispatchStub.called); + element._newValue = 'test'; + MockInteractions.tap(element.$.addButton); + flushAsynchronousOperations(); + + assert.isTrue(dispatchStub.called); + assert.equal(dispatchStub.lastCall.args[0], 'test'); + assert.equal(element._newValue, ''); }); }); + + test('deleting', () => { + dispatchStub = sandbox.stub(element, '_dispatchChanged'); + element.pluginOption = {info: {values: ['test', 'test2']}}; + flushAsynchronousOperations(); + + const rows = getAll('.existingItems .row'); + assert.equal(rows.length, 2); + const button = rows[0].querySelector('gr-button'); + + MockInteractions.tap(button); + flushAsynchronousOperations(); + + assert.isFalse(dispatchStub.called); + element.pluginOption.info.editable = true; + element.notifyPath('pluginOption.info.editable'); + flushAsynchronousOperations(); + + MockInteractions.tap(button); + flushAsynchronousOperations(); + + assert.isTrue(dispatchStub.called); + assert.deepEqual(dispatchStub.lastCall.args[0], ['test2']); + }); + + test('_dispatchChanged', () => { + const eventStub = sandbox.stub(element, 'dispatchEvent'); + element._dispatchChanged(['new-test-value']); + + assert.isTrue(eventStub.called); + const {detail} = eventStub.lastCall.args[0]; + assert.equal(detail._key, 'test-key'); + assert.deepEqual(detail.info, {values: ['new-test-value']}); + assert.equal(detail.notifyPath, 'test-key.values'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js index 5dd6ec2..d5a4e08 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js +++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -14,110 +14,122 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.ListViewMixin - * @extends Polymer.Element - */ - class GrPluginList extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.ListViewBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-plugin-list'; } +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-list-view/gr-list-view.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-plugin-list_html.js'; - static get properties() { - return { +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.ListViewMixin + * @extends Polymer.Element + */ +class GrPluginList extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.ListViewBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-plugin-list'; } + + static get properties() { + return { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, /** - * URL params passed from the router. + * Offset of currently visible query results. */ - params: { - type: Object, - observer: '_paramsChanged', - }, - /** - * Offset of currently visible query results. - */ - _offset: { - type: Number, - value: 0, - }, - _path: { - type: String, - readOnly: true, - value: '/admin/plugins', - }, - _plugins: Array, - /** - * Because we request one more than the pluginsPerPage, _shownPlugins - * maybe one less than _plugins. - * */ - _shownPlugins: { - type: Array, - computed: 'computeShownItems(_plugins)', - }, - _pluginsPerPage: { - type: Number, - value: 25, - }, - _loading: { - type: Boolean, - value: true, - }, - _filter: { - type: String, - value: '', - }, - }; - } - - /** @override */ - attached() { - super.attached(); - this.fire('title-change', {title: 'Plugins'}); - } - - _paramsChanged(params) { - this._loading = true; - this._filter = this.getFilterValue(params); - this._offset = this.getOffsetValue(params); - - return this._getPlugins(this._filter, this._pluginsPerPage, - this._offset); - } - - _getPlugins(filter, pluginsPerPage, offset) { - const errFn = response => { - this.fire('page-error', {response}); - }; - return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn) - .then(plugins => { - if (!plugins) { - this._plugins = []; - return; - } - this._plugins = Object.keys(plugins) - .map(key => { - const plugin = plugins[key]; - plugin.name = key; - return plugin; - }); - this._loading = false; - }); - } - - _status(item) { - return item.disabled === true ? 'Disabled' : 'Enabled'; - } - - _computePluginUrl(id) { - return this.getUrl('/', id); - } + _offset: { + type: Number, + value: 0, + }, + _path: { + type: String, + readOnly: true, + value: '/admin/plugins', + }, + _plugins: Array, + /** + * Because we request one more than the pluginsPerPage, _shownPlugins + * maybe one less than _plugins. + * */ + _shownPlugins: { + type: Array, + computed: 'computeShownItems(_plugins)', + }, + _pluginsPerPage: { + type: Number, + value: 25, + }, + _loading: { + type: Boolean, + value: true, + }, + _filter: { + type: String, + value: '', + }, + }; } - customElements.define(GrPluginList.is, GrPluginList); -})(); + /** @override */ + attached() { + super.attached(); + this.fire('title-change', {title: 'Plugins'}); + } + + _paramsChanged(params) { + this._loading = true; + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getPlugins(this._filter, this._pluginsPerPage, + this._offset); + } + + _getPlugins(filter, pluginsPerPage, offset) { + const errFn = response => { + this.fire('page-error', {response}); + }; + return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset, errFn) + .then(plugins => { + if (!plugins) { + this._plugins = []; + return; + } + this._plugins = Object.keys(plugins) + .map(key => { + const plugin = plugins[key]; + plugin.name = key; + return plugin; + }); + this._loading = false; + }); + } + + _status(item) { + return item.disabled === true ? 'Disabled' : 'Enabled'; + } + + _computePluginUrl(id) { + return this.getUrl('/', id); + } +} + +customElements.define(GrPluginList.is, GrPluginList);
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js index b056f92..90192c4 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js +++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_html.js
@@ -1,58 +1,44 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-plugin-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <style include="gr-table-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <gr-list-view - filter="[[_filter]]" - items-per-page="[[_pluginsPerPage]]" - items="[[_plugins]]" - loading="[[_loading]]" - offset="[[_offset]]" - path="[[_path]]"> + <gr-list-view filter="[[_filter]]" items-per-page="[[_pluginsPerPage]]" items="[[_plugins]]" loading="[[_loading]]" offset="[[_offset]]" path="[[_path]]"> <table id="list" class="genericList"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="name topHeader">Plugin Name</th> <th class="version topHeader">Version</th> <th class="status topHeader">Status</th> </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]"> <td>Loading...</td> </tr> - <tbody class$="[[computeLoadingClass(_loading)]]"> + </tbody><tbody class\$="[[computeLoadingClass(_loading)]]"> <template is="dom-repeat" items="[[_shownPlugins]]"> <tr class="table"> <td class="name"> <template is="dom-if" if="[[item.index_url]]"> - <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a> + <a href\$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a> </template> <template is="dom-if" if="[[!item.index_url]]"> [[item.id]] @@ -66,6 +52,4 @@ </table> </gr-list-view> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-plugin-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html index 67a6c3f..f89eb8e 100644 --- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-plugin-list.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-plugin-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,151 +41,154 @@ </template> </test-fixture> -<script> - let counter; - const pluginGenerator = () => { - const plugin = { - id: `test${++counter}`, - version: '3.0-SNAPSHOT', - disabled: false, - }; - - if (counter !== 2) { - plugin.index_url = `plugins/test${counter}/`; - } - return plugin; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +let counter; +const pluginGenerator = () => { + const plugin = { + id: `test${++counter}`, + version: '3.0-SNAPSHOT', + disabled: false, }; - suite('gr-plugin-list tests', async () => { - await readyToTest(); - let element; - let plugins; - let sandbox; - let value; + if (counter !== 2) { + plugin.index_url = `plugins/test${counter}/`; + } + return plugin; +}; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - counter = 0; +suite('gr-plugin-list tests', () => { + let element; + let plugins; + let sandbox; + let value; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + counter = 0; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list with plugins', () => { + setup(done => { + plugins = _.times(26, pluginGenerator); + + stub('gr-rest-api-interface', { + getPlugins(num, offset) { + return Promise.resolve(plugins); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); }); - teardown(() => { - sandbox.restore(); - }); - - suite('list with plugins', () => { - setup(done => { - plugins = _.times(26, pluginGenerator); - - stub('gr-rest-api-interface', { - getPlugins(num, offset) { - return Promise.resolve(plugins); - }, - }); - - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('plugin in the list is formatted correctly', done => { - flush(() => { - assert.equal(element._plugins[2].id, 'test3'); - assert.equal(element._plugins[2].index_url, 'plugins/test3/'); - assert.equal(element._plugins[2].version, '3.0-SNAPSHOT'); - assert.equal(element._plugins[2].disabled, false); - done(); - }); - }); - - test('with and without urls', done => { - flush(() => { - const names = Polymer.dom(element.root).querySelectorAll('.name'); - assert.isOk(names[1].querySelector('a')); - assert.equal(names[1].querySelector('a').innerText, 'test1'); - assert.isNotOk(names[2].querySelector('a')); - assert.equal(names[2].innerText, 'test2'); - done(); - }); - }); - - test('_shownPlugins', () => { - assert.equal(element._shownPlugins.length, 25); + test('plugin in the list is formatted correctly', done => { + flush(() => { + assert.equal(element._plugins[2].id, 'test3'); + assert.equal(element._plugins[2].index_url, 'plugins/test3/'); + assert.equal(element._plugins[2].version, '3.0-SNAPSHOT'); + assert.equal(element._plugins[2].disabled, false); + done(); }); }); - suite('list with less then 26 plugins', () => { - setup(done => { - plugins = _.times(25, pluginGenerator); - - stub('gr-rest-api-interface', { - getPlugins(num, offset) { - return Promise.resolve(plugins); - }, - }); - - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('_shownPlugins', () => { - assert.equal(element._shownPlugins.length, 25); + test('with and without urls', done => { + flush(() => { + const names = dom(element.root).querySelectorAll('.name'); + assert.isOk(names[1].querySelector('a')); + assert.equal(names[1].querySelector('a').innerText, 'test1'); + assert.isNotOk(names[2].querySelector('a')); + assert.equal(names[2].innerText, 'test2'); + done(); }); }); - suite('filter', () => { - test('_paramsChanged', done => { - sandbox.stub( - element.$.restAPI, - 'getPlugins', - () => Promise.resolve(plugins)); - const value = { - filter: 'test', - offset: 25, - }; - element._paramsChanged(value).then(() => { - assert.equal(element.$.restAPI.getPlugins.lastCall.args[0], - 'test'); - assert.equal(element.$.restAPI.getPlugins.lastCall.args[1], - 25); - assert.equal(element.$.restAPI.getPlugins.lastCall.args[2], - 25); - done(); - }); + test('_shownPlugins', () => { + assert.equal(element._shownPlugins.length, 25); + }); + }); + + suite('list with less then 26 plugins', () => { + setup(done => { + plugins = _.times(25, pluginGenerator); + + stub('gr-rest-api-interface', { + getPlugins(num, offset) { + return Promise.resolve(plugins); + }, }); + + element._paramsChanged(value).then(() => { flush(done); }); }); - suite('loading', () => { - test('correct contents are displayed', () => { - assert.isTrue(element._loading); - assert.equal(element.computeLoadingClass(element._loading), 'loading'); - assert.equal(getComputedStyle(element.$.loading).display, 'block'); - - element._loading = false; - element._plugins = _.times(25, pluginGenerator); - - flushAsynchronousOperations(); - assert.equal(element.computeLoadingClass(element._loading), ''); - assert.equal(getComputedStyle(element.$.loading).display, 'none'); - }); + test('_shownPlugins', () => { + assert.equal(element._shownPlugins.length, 25); }); + }); - suite('404', () => { - test('fires page-error', done => { - const response = {status: 404}; - sandbox.stub(element.$.restAPI, 'getPlugins', - (filter, pluginsPerPage, opt_offset, errFn) => { - errFn(response); - }); - - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - const value = { - filter: 'test', - offset: 25, - }; - element._paramsChanged(value); + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub( + element.$.restAPI, + 'getPlugins', + () => Promise.resolve(plugins)); + const value = { + filter: 'test', + offset: 25, + }; + element._paramsChanged(value).then(() => { + assert.equal(element.$.restAPI.getPlugins.lastCall.args[0], + 'test'); + assert.equal(element.$.restAPI.getPlugins.lastCall.args[1], + 25); + assert.equal(element.$.restAPI.getPlugins.lastCall.args[2], + 25); + done(); }); }); }); + + suite('loading', () => { + test('correct contents are displayed', () => { + assert.isTrue(element._loading); + assert.equal(element.computeLoadingClass(element._loading), 'loading'); + assert.equal(getComputedStyle(element.$.loading).display, 'block'); + + element._loading = false; + element._plugins = _.times(25, pluginGenerator); + + flushAsynchronousOperations(); + assert.equal(element.computeLoadingClass(element._loading), ''); + assert.equal(getComputedStyle(element.$.loading).display, 'none'); + }); + }); + + suite('404', () => { + test('fires page-error', done => { + const response = {status: 404}; + sandbox.stub(element.$.restAPI, 'getPlugins', + (filter, pluginsPerPage, opt_offset, errFn) => { + errFn(response); + }); + + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + const value = { + filter: 'test', + offset: 25, + }; + element._paramsChanged(value); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js index 02b62e0..6cfa7ff 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.js
@@ -14,497 +14,515 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const Defs = {}; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-access-behavior/gr-access-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../styles/gr-menu-page-styles.js'; +import '../../../styles/gr-subpage-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-access-section/gr-access-section.js'; +import '../../../scripts/util.js'; +import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-access_html.js'; - const NOTHING_TO_SAVE = 'No changes to save.'; +const Defs = {}; - const MAX_AUTOCOMPLETE_RESULTS = 50; +const NOTHING_TO_SAVE = 'No changes to save.'; - /** - * Fired when save is a no-op - * - * @event show-alert - */ +const MAX_AUTOCOMPLETE_RESULTS = 50; - /** - * @typedef {{ - * value: !Object, - * }} - */ - Defs.rule; +/** + * Fired when save is a no-op + * + * @event show-alert + */ - /** - * @typedef {{ - * rules: !Object<string, Defs.rule> - * }} - */ - Defs.permission; +/** + * @typedef {{ + * value: !Object, + * }} + */ +Defs.rule; - /** - * Can be an empty object or consist of permissions. - * - * @typedef {{ - * permissions: !Object<string, Defs.permission> - * }} - */ - Defs.permissions; +/** + * @typedef {{ + * rules: !Object<string, Defs.rule> + * }} + */ +Defs.permission; - /** - * Can be an empty object or consist of permissions. - * - * @typedef {!Object<string, Defs.permissions>} - */ - Defs.sections; +/** + * Can be an empty object or consist of permissions. + * + * @typedef {{ + * permissions: !Object<string, Defs.permission> + * }} + */ +Defs.permissions; - /** - * @typedef {{ - * remove: !Defs.sections, - * add: !Defs.sections, - * }} - */ - Defs.projectAccessInput; +/** + * Can be an empty object or consist of permissions. + * + * @typedef {!Object<string, Defs.permissions>} + */ +Defs.sections; - /** - * @appliesMixin Gerrit.AccessMixin - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrRepoAccess extends Polymer.mixinBehaviors( [ - Gerrit.AccessBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-access'; } +/** + * @typedef {{ + * remove: !Defs.sections, + * add: !Defs.sections, + * }} + */ +Defs.projectAccessInput; - static get properties() { - return { - repo: { - type: String, - observer: '_repoChanged', +/** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrRepoAccess extends mixinBehaviors( [ + Gerrit.AccessBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-repo-access'; } + + static get properties() { + return { + repo: { + type: String, + observer: '_repoChanged', + }, + // The current path + path: String, + + _canUpload: { + type: Boolean, + value: false, + }, + _inheritFromFilter: String, + _query: { + type: Function, + value() { + return this._getInheritFromSuggestions.bind(this); }, - // The current path - path: String, + }, + _ownerOf: Array, + _capabilities: Object, + _groups: Object, + /** @type {?} */ + _inheritsFrom: Object, + _labels: Object, + _local: Object, + _editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + _modified: { + type: Boolean, + value: false, + }, + _sections: Array, + _weblinks: Array, + _loading: { + type: Boolean, + value: true, + }, + }; + } - _canUpload: { - type: Boolean, - value: false, - }, - _inheritFromFilter: String, - _query: { - type: Function, - value() { - return this._getInheritFromSuggestions.bind(this); - }, - }, - _ownerOf: Array, - _capabilities: Object, - _groups: Object, - /** @type {?} */ - _inheritsFrom: Object, - _labels: Object, - _local: Object, - _editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - _modified: { - type: Boolean, - value: false, - }, - _sections: Array, - _weblinks: Array, - _loading: { - type: Boolean, - value: true, - }, - }; - } + /** @override */ + created() { + super.created(); + this.addEventListener('access-modified', + () => + this._handleAccessModified()); + } - /** @override */ - created() { - super.created(); - this.addEventListener('access-modified', - () => - this._handleAccessModified()); - } + _handleAccessModified() { + this._modified = true; + } - _handleAccessModified() { - this._modified = true; - } + /** + * @param {string} repo + * @return {!Promise} + */ + _repoChanged(repo) { + this._loading = true; - /** - * @param {string} repo - * @return {!Promise} - */ - _repoChanged(repo) { - this._loading = true; + if (!repo) { return Promise.resolve(); } - if (!repo) { return Promise.resolve(); } + return this._reload(repo); + } - return this._reload(repo); - } + _reload(repo) { + const promises = []; - _reload(repo) { - const promises = []; + const errFn = response => { + this.fire('page-error', {response}); + }; - const errFn = response => { - this.fire('page-error', {response}); - }; + this._editing = false; - this._editing = false; + // Always reset sections when a project changes. + this._sections = []; + promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn) + .then(res => { + if (!res) { return Promise.resolve(); } - // Always reset sections when a project changes. - this._sections = []; - promises.push(this.$.restAPI.getRepoAccessRights(repo, errFn) - .then(res => { - if (!res) { return Promise.resolve(); } - - // Keep a copy of the original inherit from values separate from - // the ones data bound to gr-autocomplete, so the original value - // can be restored if the user cancels. - this._inheritsFrom = res.inherits_from ? Object.assign({}, - res.inherits_from) : null; - this._originalInheritsFrom = res.inherits_from ? Object.assign({}, - res.inherits_from) : null; - // Initialize the filter value so when the user clicks edit, the - // current value appears. If there is no parent repo, it is - // initialized as an empty string. - this._inheritFromFilter = res.inherits_from ? - this._inheritsFrom.name : ''; - this._local = res.local; - this._groups = res.groups; - this._weblinks = res.config_web_links || []; - this._canUpload = res.can_upload; - this._ownerOf = res.owner_of || []; - return this.toSortedArray(this._local); - })); - - promises.push(this.$.restAPI.getCapabilities(errFn) - .then(res => { - if (!res) { return Promise.resolve(); } - - return res; - })); - - promises.push(this.$.restAPI.getRepo(repo, errFn) - .then(res => { - if (!res) { return Promise.resolve(); } - - return res.labels; - })); - - return Promise.all(promises).then(([sections, capabilities, labels]) => { - this._capabilities = capabilities; - this._labels = labels; - this._sections = sections; - this._loading = false; - }); - } - - _handleUpdateInheritFrom(e) { - if (!this._inheritsFrom) { - this._inheritsFrom = {}; - } - this._inheritsFrom.id = e.detail.value; - this._inheritsFrom.name = this._inheritFromFilter; - this._handleAccessModified(); - } - - _getInheritFromSuggestions() { - return this.$.restAPI.getRepos( - this._inheritFromFilter, - MAX_AUTOCOMPLETE_RESULTS) - .then(response => { - const projects = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - projects.push({ - name: response[key].name, - value: response[key].id, - }); - } - return projects; - }); - } - - _computeLoadingClass(loading) { - return loading ? 'loading' : ''; - } - - _handleEdit() { - this._editing = !this._editing; - } - - _editOrCancel(editing) { - return editing ? 'Cancel' : 'Edit'; - } - - _computeWebLinkClass(weblinks) { - return weblinks && weblinks.length ? 'show' : ''; - } - - _computeShowInherit(inheritsFrom) { - return inheritsFrom ? 'show' : ''; - } - - _handleAddedSectionRemoved(e) { - const index = e.model.index; - this._sections = this._sections.slice(0, index) - .concat(this._sections.slice(index + 1, this._sections.length)); - } - - _handleEditingChanged(editing, editingOld) { - // Ignore when editing gets set initially. - if (!editingOld || editing) { return; } - // Remove any unsaved but added refs. - if (this._sections) { - this._sections = this._sections.filter(p => !p.value.added); - } - // Restore inheritFrom. - if (this._inheritsFrom) { - this._inheritsFrom = Object.assign({}, this._originalInheritsFrom); - this._inheritFromFilter = this._inheritsFrom.name; - } - for (const key of Object.keys(this._local)) { - if (this._local[key].added) { - delete this._local[key]; - } - } - } - - /** - * @param {!Defs.projectAccessInput} addRemoveObj - * @param {!Array} path - * @param {string} type add or remove - * @param {!Object=} opt_value value to add if the type is 'add' - * @return {!Defs.projectAccessInput} - */ - _updateAddRemoveObj(addRemoveObj, path, type, opt_value) { - let curPos = addRemoveObj[type]; - for (const item of path) { - if (!curPos[item]) { - if (item === path[path.length - 1] && type === 'remove') { - if (path[path.length - 2] === 'permissions') { - curPos[item] = {rules: {}}; - } else if (path.length === 1) { - curPos[item] = {permissions: {}}; - } else { - curPos[item] = {}; - } - } else if (item === path[path.length - 1] && type === 'add') { - curPos[item] = opt_value; - } else { - curPos[item] = {}; - } - } - curPos = curPos[item]; - } - return addRemoveObj; - } - - /** - * Used to recursively remove any objects with a 'deleted' bit. - */ - _recursivelyRemoveDeleted(obj) { - for (const k in obj) { - if (!obj.hasOwnProperty(k)) { continue; } - - if (typeof obj[k] == 'object') { - if (obj[k].deleted) { - delete obj[k]; - return; - } - this._recursivelyRemoveDeleted(obj[k]); - } - } - } - - _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) { - for (const k in obj) { - if (!obj.hasOwnProperty(k)) { continue; } - if (typeof obj[k] == 'object') { - const updatedId = obj[k].updatedId; - const ref = updatedId ? updatedId : k; - if (obj[k].deleted) { - this._updateAddRemoveObj(addRemoveObj, - path.concat(k), 'remove'); - continue; - } else if (obj[k].modified) { - this._updateAddRemoveObj(addRemoveObj, - path.concat(k), 'remove'); - this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add', - obj[k]); - /* Special case for ref changes because they need to be added and - removed in a different way. The new ref needs to include all - changes but also the initial state. To do this, instead of - continuing with the same recursion, just remove anything that is - deleted in the current state. */ - if (updatedId && updatedId !== k) { - this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]); - } - continue; - } else if (obj[k].added) { - this._updateAddRemoveObj(addRemoveObj, - path.concat(ref), 'add', obj[k]); - /** - * As add / delete both can happen in the new section, - * so here to make sure it will remove the deleted ones. - * - * @see Issue 11339 - */ - this._recursivelyRemoveDeleted(addRemoveObj.add[k]); - continue; - } - this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj, - path.concat(k)); - } - } - } - - /** - * Returns an object formatted for saving or submitting access changes for - * review - * - * @return {!Defs.projectAccessInput} - */ - _computeAddAndRemove() { - const addRemoveObj = { - add: {}, - remove: {}, - }; - - const originalInheritsFromId = this._originalInheritsFrom ? - this.singleDecodeURL(this._originalInheritsFrom.id) : - null; - const inheritsFromId = this._inheritsFrom ? - this.singleDecodeURL(this._inheritsFrom.id) : - null; - - const inheritFromChanged = - // Inherit from changed - (originalInheritsFromId && - originalInheritsFromId !== inheritsFromId) || - // Inherit from added (did not have one initially); - (!originalInheritsFromId && inheritsFromId); - - this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj); - - if (inheritFromChanged) { - addRemoveObj.parent = inheritsFromId; - } - return addRemoveObj; - } - - _handleCreateSection() { - let newRef = 'refs/for/*'; - // Avoid using an already used key for the placeholder, since it - // immediately gets added to an object. - while (this._local[newRef]) { - newRef = `${newRef}*`; - } - const section = {permissions: {}, added: true}; - this.push('_sections', {id: newRef, value: section}); - this.set(['_local', newRef], section); - Polymer.dom.flush(); - Polymer.dom(this.root).querySelector('gr-access-section:last-of-type') - .editReference(); - } - - _getObjforSave() { - const addRemoveObj = this._computeAddAndRemove(); - // If there are no changes, don't actually save. - if (!Object.keys(addRemoveObj.add).length && - !Object.keys(addRemoveObj.remove).length && - !addRemoveObj.parent) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: NOTHING_TO_SAVE}, - bubbles: true, - composed: true, + // Keep a copy of the original inherit from values separate from + // the ones data bound to gr-autocomplete, so the original value + // can be restored if the user cancels. + this._inheritsFrom = res.inherits_from ? Object.assign({}, + res.inherits_from) : null; + this._originalInheritsFrom = res.inherits_from ? Object.assign({}, + res.inherits_from) : null; + // Initialize the filter value so when the user clicks edit, the + // current value appears. If there is no parent repo, it is + // initialized as an empty string. + this._inheritFromFilter = res.inherits_from ? + this._inheritsFrom.name : ''; + this._local = res.local; + this._groups = res.groups; + this._weblinks = res.config_web_links || []; + this._canUpload = res.can_upload; + this._ownerOf = res.owner_of || []; + return this.toSortedArray(this._local); })); - return; - } - const obj = { - add: addRemoveObj.add, - remove: addRemoveObj.remove, - }; - if (addRemoveObj.parent) { - obj.parent = addRemoveObj.parent; - } - return obj; - } - _handleSave(e) { - const obj = this._getObjforSave(); - if (!obj) { return; } - const button = e && e.target; - if (button) { - button.loading = true; + promises.push(this.$.restAPI.getCapabilities(errFn) + .then(res => { + if (!res) { return Promise.resolve(); } + + return res; + })); + + promises.push(this.$.restAPI.getRepo(repo, errFn) + .then(res => { + if (!res) { return Promise.resolve(); } + + return res.labels; + })); + + return Promise.all(promises).then(([sections, capabilities, labels]) => { + this._capabilities = capabilities; + this._labels = labels; + this._sections = sections; + this._loading = false; + }); + } + + _handleUpdateInheritFrom(e) { + if (!this._inheritsFrom) { + this._inheritsFrom = {}; + } + this._inheritsFrom.id = e.detail.value; + this._inheritsFrom.name = this._inheritFromFilter; + this._handleAccessModified(); + } + + _getInheritFromSuggestions() { + return this.$.restAPI.getRepos( + this._inheritFromFilter, + MAX_AUTOCOMPLETE_RESULTS) + .then(response => { + const projects = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + projects.push({ + name: response[key].name, + value: response[key].id, + }); + } + return projects; + }); + } + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + } + + _handleEdit() { + this._editing = !this._editing; + } + + _editOrCancel(editing) { + return editing ? 'Cancel' : 'Edit'; + } + + _computeWebLinkClass(weblinks) { + return weblinks && weblinks.length ? 'show' : ''; + } + + _computeShowInherit(inheritsFrom) { + return inheritsFrom ? 'show' : ''; + } + + _handleAddedSectionRemoved(e) { + const index = e.model.index; + this._sections = this._sections.slice(0, index) + .concat(this._sections.slice(index + 1, this._sections.length)); + } + + _handleEditingChanged(editing, editingOld) { + // Ignore when editing gets set initially. + if (!editingOld || editing) { return; } + // Remove any unsaved but added refs. + if (this._sections) { + this._sections = this._sections.filter(p => !p.value.added); + } + // Restore inheritFrom. + if (this._inheritsFrom) { + this._inheritsFrom = Object.assign({}, this._originalInheritsFrom); + this._inheritFromFilter = this._inheritsFrom.name; + } + for (const key of Object.keys(this._local)) { + if (this._local[key].added) { + delete this._local[key]; } - return this.$.restAPI.setRepoAccessRights(this.repo, obj) - .then(() => { - this._reload(this.repo); - }) - .finally(() => { - this._modified = false; - if (button) { - button.loading = false; - } - }); - } - - _handleSaveForReview(e) { - const obj = this._getObjforSave(); - if (!obj) { return; } - const button = e && e.target; - if (button) { - button.loading = true; - } - return this.$.restAPI - .setRepoAccessRightsForReview(this.repo, obj) - .then(change => { - Gerrit.Nav.navigateToChange(change); - }) - .finally(() => { - this._modified = false; - if (button) { - button.loading = false; - } - }); - } - - _computeSaveReviewBtnClass(canUpload) { - return !canUpload ? 'invisible' : ''; - } - - _computeSaveBtnClass(ownerOf) { - return ownerOf && ownerOf.length === 0 ? 'invisible' : ''; - } - - _computeMainClass(ownerOf, canUpload, editing) { - const classList = []; - if (ownerOf && ownerOf.length > 0 || canUpload) { - classList.push('admin'); - } - if (editing) { - classList.push('editing'); - } - return classList.join(' '); - } - - _computeParentHref(repoName) { - return this.getBaseUrl() + - `/admin/repos/${this.encodeURL(repoName, true)},access`; } } - customElements.define(GrRepoAccess.is, GrRepoAccess); -})(); + /** + * @param {!Defs.projectAccessInput} addRemoveObj + * @param {!Array} path + * @param {string} type add or remove + * @param {!Object=} opt_value value to add if the type is 'add' + * @return {!Defs.projectAccessInput} + */ + _updateAddRemoveObj(addRemoveObj, path, type, opt_value) { + let curPos = addRemoveObj[type]; + for (const item of path) { + if (!curPos[item]) { + if (item === path[path.length - 1] && type === 'remove') { + if (path[path.length - 2] === 'permissions') { + curPos[item] = {rules: {}}; + } else if (path.length === 1) { + curPos[item] = {permissions: {}}; + } else { + curPos[item] = {}; + } + } else if (item === path[path.length - 1] && type === 'add') { + curPos[item] = opt_value; + } else { + curPos[item] = {}; + } + } + curPos = curPos[item]; + } + return addRemoveObj; + } + + /** + * Used to recursively remove any objects with a 'deleted' bit. + */ + _recursivelyRemoveDeleted(obj) { + for (const k in obj) { + if (!obj.hasOwnProperty(k)) { continue; } + + if (typeof obj[k] == 'object') { + if (obj[k].deleted) { + delete obj[k]; + return; + } + this._recursivelyRemoveDeleted(obj[k]); + } + } + } + + _recursivelyUpdateAddRemoveObj(obj, addRemoveObj, path = []) { + for (const k in obj) { + if (!obj.hasOwnProperty(k)) { continue; } + if (typeof obj[k] == 'object') { + const updatedId = obj[k].updatedId; + const ref = updatedId ? updatedId : k; + if (obj[k].deleted) { + this._updateAddRemoveObj(addRemoveObj, + path.concat(k), 'remove'); + continue; + } else if (obj[k].modified) { + this._updateAddRemoveObj(addRemoveObj, + path.concat(k), 'remove'); + this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add', + obj[k]); + /* Special case for ref changes because they need to be added and + removed in a different way. The new ref needs to include all + changes but also the initial state. To do this, instead of + continuing with the same recursion, just remove anything that is + deleted in the current state. */ + if (updatedId && updatedId !== k) { + this._recursivelyRemoveDeleted(addRemoveObj.add[updatedId]); + } + continue; + } else if (obj[k].added) { + this._updateAddRemoveObj(addRemoveObj, + path.concat(ref), 'add', obj[k]); + /** + * As add / delete both can happen in the new section, + * so here to make sure it will remove the deleted ones. + * + * @see Issue 11339 + */ + this._recursivelyRemoveDeleted(addRemoveObj.add[k]); + continue; + } + this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj, + path.concat(k)); + } + } + } + + /** + * Returns an object formatted for saving or submitting access changes for + * review + * + * @return {!Defs.projectAccessInput} + */ + _computeAddAndRemove() { + const addRemoveObj = { + add: {}, + remove: {}, + }; + + const originalInheritsFromId = this._originalInheritsFrom ? + this.singleDecodeURL(this._originalInheritsFrom.id) : + null; + const inheritsFromId = this._inheritsFrom ? + this.singleDecodeURL(this._inheritsFrom.id) : + null; + + const inheritFromChanged = + // Inherit from changed + (originalInheritsFromId && + originalInheritsFromId !== inheritsFromId) || + // Inherit from added (did not have one initially); + (!originalInheritsFromId && inheritsFromId); + + this._recursivelyUpdateAddRemoveObj(this._local, addRemoveObj); + + if (inheritFromChanged) { + addRemoveObj.parent = inheritsFromId; + } + return addRemoveObj; + } + + _handleCreateSection() { + let newRef = 'refs/for/*'; + // Avoid using an already used key for the placeholder, since it + // immediately gets added to an object. + while (this._local[newRef]) { + newRef = `${newRef}*`; + } + const section = {permissions: {}, added: true}; + this.push('_sections', {id: newRef, value: section}); + this.set(['_local', newRef], section); + flush(); + dom(this.root).querySelector('gr-access-section:last-of-type') + .editReference(); + } + + _getObjforSave() { + const addRemoveObj = this._computeAddAndRemove(); + // If there are no changes, don't actually save. + if (!Object.keys(addRemoveObj.add).length && + !Object.keys(addRemoveObj.remove).length && + !addRemoveObj.parent) { + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message: NOTHING_TO_SAVE}, + bubbles: true, + composed: true, + })); + return; + } + const obj = { + add: addRemoveObj.add, + remove: addRemoveObj.remove, + }; + if (addRemoveObj.parent) { + obj.parent = addRemoveObj.parent; + } + return obj; + } + + _handleSave(e) { + const obj = this._getObjforSave(); + if (!obj) { return; } + const button = e && e.target; + if (button) { + button.loading = true; + } + return this.$.restAPI.setRepoAccessRights(this.repo, obj) + .then(() => { + this._reload(this.repo); + }) + .finally(() => { + this._modified = false; + if (button) { + button.loading = false; + } + }); + } + + _handleSaveForReview(e) { + const obj = this._getObjforSave(); + if (!obj) { return; } + const button = e && e.target; + if (button) { + button.loading = true; + } + return this.$.restAPI + .setRepoAccessRightsForReview(this.repo, obj) + .then(change => { + Gerrit.Nav.navigateToChange(change); + }) + .finally(() => { + this._modified = false; + if (button) { + button.loading = false; + } + }); + } + + _computeSaveReviewBtnClass(canUpload) { + return !canUpload ? 'invisible' : ''; + } + + _computeSaveBtnClass(ownerOf) { + return ownerOf && ownerOf.length === 0 ? 'invisible' : ''; + } + + _computeMainClass(ownerOf, canUpload, editing) { + const classList = []; + if (ownerOf && ownerOf.length > 0 || canUpload) { + classList.push('admin'); + } + if (editing) { + classList.push('editing'); + } + return classList.join(' '); + } + + _computeParentHref(repoName) { + return this.getBaseUrl() + + `/admin/repos/${this.encodeURL(repoName, true)},access`; + } +} + +customElements.define(GrRepoAccess.is, GrRepoAccess);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js index 54006b5..9a27371 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.js
@@ -1,38 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> - -<link rel="import" href="../../../styles/gr-menu-page-styles.html"> -<link rel="import" href="../../../styles/gr-subpage-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-access-section/gr-access-section.html"> - -<script src="../../../scripts/util.js"></script> - -<dom-module id="gr-repo-access"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -73,25 +57,18 @@ <style include="gr-menu-page-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <main class$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]"> - <div id="loading" class$="[[_computeLoadingClass(_loading)]]"> + <main class\$="[[_computeMainClass(_ownerOf, _canUpload, _editing)]]"> + <div id="loading" class\$="[[_computeLoadingClass(_loading)]]"> Loading... </div> - <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> - <h3 id="inheritsFrom" class$="[[_computeShowInherit(_inheritsFrom)]]"> + <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]"> + <h3 id="inheritsFrom" class\$="[[_computeShowInherit(_inheritsFrom)]]"> <span class="rightsText">Rights Inherit From</span> - <a - href$="[[_computeParentHref(_inheritsFrom.name)]]" - rel="noopener" - id="inheritFromName"> + <a href\$="[[_computeParentHref(_inheritsFrom.name)]]" rel="noopener" id="inheritFromName"> [[_inheritsFrom.name]]</a> - <gr-autocomplete - id="editInheritFromInput" - text="{{_inheritFromFilter}}" - query="[[_query]]" - on-commit="_handleUpdateInheritFrom"></gr-autocomplete> + <gr-autocomplete id="editInheritFromInput" text="{{_inheritFromFilter}}" query="[[_query]]" on-commit="_handleUpdateInheritFrom"></gr-autocomplete> </h3> - <div class$="weblinks [[_computeWebLinkClass(_weblinks)]]"> + <div class\$="weblinks [[_computeWebLinkClass(_weblinks)]]"> History: <template is="dom-repeat" items="[[_weblinks]]" as="link"> <a href="[[link.url]]" class="weblink" rel="noopener" target="[[link.target]]"> @@ -99,41 +76,16 @@ </a> </template> </div> - <gr-button id="editBtn" - on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button> - <gr-button id="saveBtn" - primary - class$="[[_computeSaveBtnClass(_ownerOf)]]" - on-click="_handleSave" - disabled="[[!_modified]]">Save</gr-button> - <gr-button id="saveReviewBtn" - primary - class$="[[_computeSaveReviewBtnClass(_canUpload)]]" - on-click="_handleSaveForReview" - disabled="[[!_modified]]">Save for review</gr-button> - <template - is="dom-repeat" - items="{{_sections}}" - initial-count="5" - target-framerate="60" - as="section"> - <gr-access-section - capabilities="[[_capabilities]]" - section="{{section}}" - labels="[[_labels]]" - can-upload="[[_canUpload]]" - editing="[[_editing]]" - owner-of="[[_ownerOf]]" - groups="[[_groups]]" - on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section> + <gr-button id="editBtn" on-click="_handleEdit">[[_editOrCancel(_editing)]]</gr-button> + <gr-button id="saveBtn" primary="" class\$="[[_computeSaveBtnClass(_ownerOf)]]" on-click="_handleSave" disabled="[[!_modified]]">Save</gr-button> + <gr-button id="saveReviewBtn" primary="" class\$="[[_computeSaveReviewBtnClass(_canUpload)]]" on-click="_handleSaveForReview" disabled="[[!_modified]]">Save for review</gr-button> + <template is="dom-repeat" items="{{_sections}}" initial-count="5" target-framerate="60" as="section"> + <gr-access-section capabilities="[[_capabilities]]" section="{{section}}" labels="[[_labels]]" can-upload="[[_canUpload]]" editing="[[_editing]]" owner-of="[[_ownerOf]]" groups="[[_groups]]" on-added-section-removed="_handleAddedSectionRemoved"></gr-access-section> </template> <div class="referenceContainer"> - <gr-button id="addReferenceBtn" - on-click="_handleCreateSection">Add Reference</gr-button> + <gr-button id="addReferenceBtn" on-click="_handleCreateSection">Add Reference</gr-button> </div> </div> </main> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-access.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html index d89b5df..4a482db 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-access</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-access.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-access.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-access.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,395 +41,432 @@ </template> </test-fixture> -<script> - suite('gr-repo-access tests', async () => { - await readyToTest(); - let element; - let sandbox; - let repoStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-access.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-repo-access tests', () => { + let element; + let sandbox; + let repoStub; - const accessRes = { - local: { - 'refs/*': { - permissions: { - owner: { - rules: { - 234: {action: 'ALLOW'}, - 123: {action: 'DENY'}, - }, + const accessRes = { + local: { + 'refs/*': { + permissions: { + owner: { + rules: { + 234: {action: 'ALLOW'}, + 123: {action: 'DENY'}, }, - read: { - rules: { - 234: {action: 'ALLOW'}, + }, + read: { + rules: { + 234: {action: 'ALLOW'}, + }, + }, + }, + }, + }, + groups: { + Administrators: { + name: 'Administrators', + }, + Maintainers: { + name: 'Maintainers', + }, + }, + config_web_links: [{ + name: 'gitiles', + target: '_blank', + url: 'https://my/site/+log/123/project.config', + }], + can_upload: true, + }; + const accessRes2 = { + local: { + GLOBAL_CAPABILITIES: { + permissions: { + accessDatabase: { + rules: { + group1: { + action: 'ALLOW', }, }, }, }, }, - groups: { - Administrators: { - name: 'Administrators', - }, - Maintainers: { - name: 'Maintainers', + }, + }; + const repoRes = { + labels: { + 'Code-Review': { + values: { + ' 0': 'No score', + '-1': 'I would prefer this is not merged as is', + '-2': 'This shall not be merged', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', }, }, - config_web_links: [{ - name: 'gitiles', - target: '_blank', - url: 'https://my/site/+log/123/project.config', - }], - can_upload: true, - }; - const accessRes2 = { - local: { - GLOBAL_CAPABILITIES: { - permissions: { - accessDatabase: { - rules: { - group1: { - action: 'ALLOW', - }, - }, - }, - }, - }, - }, - }; - const repoRes = { - labels: { - 'Code-Review': { - values: { - ' 0': 'No score', - '-1': 'I would prefer this is not merged as is', - '-2': 'This shall not be merged', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', - }, - }, - }, - }; + }, + }; + const capabilitiesRes = { + accessDatabase: { + id: 'accessDatabase', + name: 'Access Database', + }, + createAccount: { + id: 'createAccount', + name: 'Create Account', + }, + }; + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + }); + repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns( + Promise.resolve(repoRes)); + element._loading = false; + element._ownerOf = []; + element._canUpload = false; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_repoChanged called when repo name changes', () => { + sandbox.stub(element, '_repoChanged'); + element.repo = 'New Repo'; + assert.isTrue(element._repoChanged.called); + }); + + test('_repoChanged', done => { + const accessStub = sandbox.stub(element.$.restAPI, + 'getRepoAccessRights'); + + accessStub.withArgs('New Repo').returns( + Promise.resolve(JSON.parse(JSON.stringify(accessRes)))); + accessStub.withArgs('Another New Repo') + .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2)))); + const capabilitiesStub = sandbox.stub(element.$.restAPI, + 'getCapabilities'); + capabilitiesStub.returns(Promise.resolve(capabilitiesRes)); + + element._repoChanged('New Repo').then(() => { + assert.isTrue(accessStub.called); + assert.isTrue(capabilitiesStub.called); + assert.isTrue(repoStub.called); + assert.isNotOk(element._inheritsFrom); + assert.deepEqual(element._local, accessRes.local); + assert.deepEqual(element._sections, + element.toSortedArray(accessRes.local)); + assert.deepEqual(element._labels, repoRes.labels); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.weblinks')).display, + 'block'); + return element._repoChanged('Another New Repo'); + }) + .then(() => { + assert.deepEqual(element._sections, + element.toSortedArray(accessRes2.local)); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.weblinks')).display, + 'none'); + done(); + }); + }); + + test('_repoChanged when repo changes to undefined returns', done => { const capabilitiesRes = { accessDatabase: { id: 'accessDatabase', name: 'Access Database', }, - createAccount: { - id: 'createAccount', - name: 'Create Account', - }, }; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(null); }, - }); - repoStub = sandbox.stub(element.$.restAPI, 'getRepo').returns( - Promise.resolve(repoRes)); - element._loading = false; - element._ownerOf = []; - element._canUpload = false; + const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights') + .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2)))); + const capabilitiesStub = sandbox.stub(element.$.restAPI, + 'getCapabilities').returns(Promise.resolve(capabilitiesRes)); + + element._repoChanged().then(() => { + assert.isFalse(accessStub.called); + assert.isFalse(capabilitiesStub.called); + assert.isFalse(repoStub.called); + done(); + }); + }); + + test('_computeParentHref', () => { + const repoName = 'test-repo'; + assert.equal(element._computeParentHref(repoName), + '/admin/repos/test-repo,access'); + }); + + test('_computeMainClass', () => { + let ownerOf = ['refs/*']; + const editing = true; + const canUpload = false; + assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin'); + assert.equal(element._computeMainClass(ownerOf, canUpload, editing), + 'admin editing'); + ownerOf = []; + assert.equal(element._computeMainClass(ownerOf, canUpload), ''); + assert.equal(element._computeMainClass(ownerOf, canUpload, editing), + 'editing'); + }); + + test('inherit section', () => { + element._local = {}; + element._ownerOf = []; + sandbox.stub(element, '_computeParentHref'); + // Nothing should appear when no inherit from and not in edit mode. + assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none'); + // The autocomplete should be hidden, and the link should be displayed. + assert.isFalse(element._computeParentHref.called); + // When it edit mode, the autocomplete should appear. + element._editing = true; + // When editing, the autocomplete should still not be shown. + assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none'); + element._editing = false; + element._inheritsFrom = { + name: 'another-repo', + }; + // When there is a parent project, the link should be displayed. + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none'); + assert.notEqual(getComputedStyle(element.$.inheritFromName).display, + 'none'); + assert.equal(getComputedStyle(element.$.editInheritFromInput).display, + 'none'); + assert.isTrue(element._computeParentHref.called); + element._editing = true; + // When editing, the autocomplete should be shown. + assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none'); + assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none'); + assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display, + 'none'); + }); + + test('_handleUpdateInheritFrom', () => { + element._inheritFromFilter = 'foo bar baz'; + element._handleUpdateInheritFrom({detail: {value: 'abc+123'}}); + assert.isOk(element._inheritsFrom); + assert.equal(element._inheritsFrom.id, 'abc+123'); + assert.equal(element._inheritsFrom.name, 'foo bar baz'); + }); + + test('_computeLoadingClass', () => { + assert.equal(element._computeLoadingClass(true), 'loading'); + assert.equal(element._computeLoadingClass(false), ''); + }); + + test('fires page-error', done => { + const response = {status: 404}; + + sandbox.stub( + element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => { + errFn(response); + }); + + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); }); - teardown(() => { - sandbox.restore(); - }); + element.repo = 'test'; + }); - test('_repoChanged called when repo name changes', () => { - sandbox.stub(element, '_repoChanged'); - element.repo = 'New Repo'; - assert.isTrue(element._repoChanged.called); - }); - - test('_repoChanged', done => { - const accessStub = sandbox.stub(element.$.restAPI, - 'getRepoAccessRights'); - - accessStub.withArgs('New Repo').returns( - Promise.resolve(JSON.parse(JSON.stringify(accessRes)))); - accessStub.withArgs('Another New Repo') - .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2)))); - const capabilitiesStub = sandbox.stub(element.$.restAPI, - 'getCapabilities'); - capabilitiesStub.returns(Promise.resolve(capabilitiesRes)); - - element._repoChanged('New Repo').then(() => { - assert.isTrue(accessStub.called); - assert.isTrue(capabilitiesStub.called); - assert.isTrue(repoStub.called); - assert.isNotOk(element._inheritsFrom); - assert.deepEqual(element._local, accessRes.local); - assert.deepEqual(element._sections, - element.toSortedArray(accessRes.local)); - assert.deepEqual(element._labels, repoRes.labels); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.weblinks')).display, - 'block'); - return element._repoChanged('Another New Repo'); - }) - .then(() => { - assert.deepEqual(element._sections, - element.toSortedArray(accessRes2.local)); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.weblinks')).display, - 'none'); - done(); - }); - }); - - test('_repoChanged when repo changes to undefined returns', done => { - const capabilitiesRes = { - accessDatabase: { - id: 'accessDatabase', - name: 'Access Database', - }, - }; - const accessStub = sandbox.stub(element.$.restAPI, 'getRepoAccessRights') - .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2)))); - const capabilitiesStub = sandbox.stub(element.$.restAPI, - 'getCapabilities').returns(Promise.resolve(capabilitiesRes)); - - element._repoChanged().then(() => { - assert.isFalse(accessStub.called); - assert.isFalse(capabilitiesStub.called); - assert.isFalse(repoStub.called); - done(); - }); - }); - - test('_computeParentHref', () => { - const repoName = 'test-repo'; - assert.equal(element._computeParentHref(repoName), - '/admin/repos/test-repo,access'); - }); - - test('_computeMainClass', () => { - let ownerOf = ['refs/*']; - const editing = true; - const canUpload = false; - assert.equal(element._computeMainClass(ownerOf, canUpload), 'admin'); - assert.equal(element._computeMainClass(ownerOf, canUpload, editing), - 'admin editing'); - ownerOf = []; - assert.equal(element._computeMainClass(ownerOf, canUpload), ''); - assert.equal(element._computeMainClass(ownerOf, canUpload, editing), - 'editing'); - }); - - test('inherit section', () => { - element._local = {}; - element._ownerOf = []; - sandbox.stub(element, '_computeParentHref'); - // Nothing should appear when no inherit from and not in edit mode. - assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none'); - // The autocomplete should be hidden, and the link should be displayed. - assert.isFalse(element._computeParentHref.called); - // When it edit mode, the autocomplete should appear. - element._editing = true; - // When editing, the autocomplete should still not be shown. - assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none'); - element._editing = false; - element._inheritsFrom = { - name: 'another-repo', - }; - // When there is a parent project, the link should be displayed. - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none'); - assert.notEqual(getComputedStyle(element.$.inheritFromName).display, - 'none'); + suite('with defined sections', () => { + const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => { + // Edit button is visible and Save button is hidden. + assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none'); + assert.equal(getComputedStyle(element.$.saveBtn).display, 'none'); + assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none'); + assert.equal(element.$.editBtn.innerText, 'EDIT'); assert.equal(getComputedStyle(element.$.editInheritFromInput).display, 'none'); - assert.isTrue(element._computeParentHref.called); - element._editing = true; - // When editing, the autocomplete should be shown. - assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none'); - assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none'); - assert.notEqual(getComputedStyle(element.$.editInheritFromInput).display, - 'none'); - }); + element._inheritsFrom = { + id: 'test-project', + }; + flushAsynchronousOperations(); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('#editInheritFromInput')) + .display, 'none'); - test('_handleUpdateInheritFrom', () => { - element._inheritFromFilter = 'foo bar baz'; - element._handleUpdateInheritFrom({detail: {value: 'abc+123'}}); - assert.isOk(element._inheritsFrom); - assert.equal(element._inheritsFrom.id, 'abc+123'); - assert.equal(element._inheritsFrom.name, 'foo bar baz'); - }); + MockInteractions.tap(element.$.editBtn); + flushAsynchronousOperations(); - test('_computeLoadingClass', () => { - assert.equal(element._computeLoadingClass(true), 'loading'); - assert.equal(element._computeLoadingClass(false), ''); - }); - - test('fires page-error', done => { - const response = {status: 404}; - - sandbox.stub( - element.$.restAPI, 'getRepoAccessRights', (repoName, errFn) => { - errFn(response); - }); - - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element.repo = 'test'; - }); - - suite('with defined sections', () => { - const testEditSaveCancelBtns = (shouldShowSave, shouldShowSaveReview) => { - // Edit button is visible and Save button is hidden. - assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none'); - assert.equal(getComputedStyle(element.$.saveBtn).display, 'none'); - assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none'); - assert.equal(element.$.editBtn.innerText, 'EDIT'); - assert.equal(getComputedStyle(element.$.editInheritFromInput).display, + // Edit button changes to Cancel button, and Save button is visible but + // disabled. + assert.equal(element.$.editBtn.innerText, 'CANCEL'); + if (shouldShowSaveReview) { + assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display, 'none'); - element._inheritsFrom = { - id: 'test-project', - }; - flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('#editInheritFromInput')) - .display, 'none'); + assert.isTrue(element.$.saveReviewBtn.disabled); + } + if (shouldShowSave) { + assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none'); + assert.isTrue(element.$.saveBtn.disabled); + } + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('#editInheritFromInput')) + .display, 'none'); - MockInteractions.tap(element.$.editBtn); - flushAsynchronousOperations(); + // Save button should be enabled after access is modified + element.fire('access-modified'); + if (shouldShowSaveReview) { + assert.isFalse(element.$.saveReviewBtn.disabled); + } + if (shouldShowSave) { + assert.isFalse(element.$.saveBtn.disabled); + } + }; - // Edit button changes to Cancel button, and Save button is visible but - // disabled. - assert.equal(element.$.editBtn.innerText, 'CANCEL'); - if (shouldShowSaveReview) { - assert.notEqual(getComputedStyle(element.$.saveReviewBtn).display, - 'none'); - assert.isTrue(element.$.saveReviewBtn.disabled); - } - if (shouldShowSave) { - assert.notEqual(getComputedStyle(element.$.saveBtn).display, 'none'); - assert.isTrue(element.$.saveBtn.disabled); - } - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('#editInheritFromInput')) - .display, 'none'); + setup(() => { + // Create deep copies of these objects so the originals are not modified + // by any tests. + element._local = JSON.parse(JSON.stringify(accessRes.local)); + element._ownerOf = []; + element._sections = element.toSortedArray(element._local); + element._groups = JSON.parse(JSON.stringify(accessRes.groups)); + element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes)); + element._labels = JSON.parse(JSON.stringify(repoRes.labels)); + flushAsynchronousOperations(); + }); - // Save button should be enabled after access is modified - element.fire('access-modified'); - if (shouldShowSaveReview) { - assert.isFalse(element.$.saveReviewBtn.disabled); - } - if (shouldShowSave) { - assert.isFalse(element.$.saveBtn.disabled); - } + test('removing an added section', () => { + element.editing = true; + assert.equal(element._sections.length, 1); + element.shadowRoot + .querySelector('gr-access-section').fire('added-section-removed'); + flushAsynchronousOperations(); + assert.equal(element._sections.length, 0); + }); + + test('button visibility for non ref owner', () => { + assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none'); + assert.equal(getComputedStyle(element.$.editBtn).display, 'none'); + }); + + test('button visibility for non ref owner with upload privilege', () => { + element._canUpload = true; + testEditSaveCancelBtns(false, true); + }); + + test('button visibility for ref owner', () => { + element._ownerOf = ['refs/for/*']; + testEditSaveCancelBtns(true, false); + }); + + test('button visibility for ref owner and upload', () => { + element._ownerOf = ['refs/for/*']; + element._canUpload = true; + testEditSaveCancelBtns(true, false); + }); + + test('_handleAccessModified called with event fired', () => { + sandbox.spy(element, '_handleAccessModified'); + element.fire('access-modified'); + assert.isTrue(element._handleAccessModified.called); + }); + + test('_handleAccessModified called when parent changes', () => { + element._inheritsFrom = { + id: 'test-project', + }; + flushAsynchronousOperations(); + element.shadowRoot.querySelector('#editInheritFromInput').fire('commit'); + sandbox.spy(element, '_handleAccessModified'); + element.fire('access-modified'); + assert.isTrue(element._handleAccessModified.called); + }); + + test('_handleSaveForReview', () => { + const saveStub = + sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview'); + sandbox.stub(element, '_computeAddAndRemove').returns({ + add: {}, + remove: {}, + }); + element._handleSaveForReview(); + assert.isFalse(saveStub.called); + }); + + test('_recursivelyRemoveDeleted', () => { + const obj = { + 'refs/*': { + permissions: { + owner: { + rules: { + 234: {action: 'ALLOW'}, + 123: {action: 'DENY', deleted: true}, + }, + }, + read: { + deleted: true, + rules: { + 234: {action: 'ALLOW'}, + }, + }, + }, + }, + }; + const expectedResult = { + 'refs/*': { + permissions: { + owner: { + rules: { + 234: {action: 'ALLOW'}, + }, + }, + }, + }, + }; + element._recursivelyRemoveDeleted(obj); + assert.deepEqual(obj, expectedResult); + }); + + test('_recursivelyUpdateAddRemoveObj on new added section', () => { + const obj = { + 'refs/for/*': { + permissions: { + 'label-Code-Review': { + rules: { + e798fed07afbc9173a587f876ef8760c78d240c1: { + min: -2, + max: 2, + action: 'ALLOW', + added: true, + }, + }, + added: true, + label: 'Code-Review', + }, + 'labelAs-Code-Review': { + rules: { + 'ldap:gerritcodereview-eng': { + min: -2, + max: 2, + action: 'ALLOW', + added: true, + deleted: true, + }, + }, + added: true, + label: 'Code-Review', + }, + }, + added: true, + }, }; - setup(() => { - // Create deep copies of these objects so the originals are not modified - // by any tests. - element._local = JSON.parse(JSON.stringify(accessRes.local)); - element._ownerOf = []; - element._sections = element.toSortedArray(element._local); - element._groups = JSON.parse(JSON.stringify(accessRes.groups)); - element._capabilities = JSON.parse(JSON.stringify(capabilitiesRes)); - element._labels = JSON.parse(JSON.stringify(repoRes.labels)); - flushAsynchronousOperations(); - }); - - test('removing an added section', () => { - element.editing = true; - assert.equal(element._sections.length, 1); - element.shadowRoot - .querySelector('gr-access-section').fire('added-section-removed'); - flushAsynchronousOperations(); - assert.equal(element._sections.length, 0); - }); - - test('button visibility for non ref owner', () => { - assert.equal(getComputedStyle(element.$.saveReviewBtn).display, 'none'); - assert.equal(getComputedStyle(element.$.editBtn).display, 'none'); - }); - - test('button visibility for non ref owner with upload privilege', () => { - element._canUpload = true; - testEditSaveCancelBtns(false, true); - }); - - test('button visibility for ref owner', () => { - element._ownerOf = ['refs/for/*']; - testEditSaveCancelBtns(true, false); - }); - - test('button visibility for ref owner and upload', () => { - element._ownerOf = ['refs/for/*']; - element._canUpload = true; - testEditSaveCancelBtns(true, false); - }); - - test('_handleAccessModified called with event fired', () => { - sandbox.spy(element, '_handleAccessModified'); - element.fire('access-modified'); - assert.isTrue(element._handleAccessModified.called); - }); - - test('_handleAccessModified called when parent changes', () => { - element._inheritsFrom = { - id: 'test-project', - }; - flushAsynchronousOperations(); - element.shadowRoot.querySelector('#editInheritFromInput').fire('commit'); - sandbox.spy(element, '_handleAccessModified'); - element.fire('access-modified'); - assert.isTrue(element._handleAccessModified.called); - }); - - test('_handleSaveForReview', () => { - const saveStub = - sandbox.stub(element.$.restAPI, 'setRepoAccessRightsForReview'); - sandbox.stub(element, '_computeAddAndRemove').returns({ - add: {}, - remove: {}, - }); - element._handleSaveForReview(); - assert.isFalse(saveStub.called); - }); - - test('_recursivelyRemoveDeleted', () => { - const obj = { - 'refs/*': { - permissions: { - owner: { - rules: { - 234: {action: 'ALLOW'}, - 123: {action: 'DENY', deleted: true}, - }, - }, - read: { - deleted: true, - rules: { - 234: {action: 'ALLOW'}, - }, - }, - }, - }, - }; - const expectedResult = { - 'refs/*': { - permissions: { - owner: { - rules: { - 234: {action: 'ALLOW'}, - }, - }, - }, - }, - }; - element._recursivelyRemoveDeleted(obj); - assert.deepEqual(obj, expectedResult); - }); - - test('_recursivelyUpdateAddRemoveObj on new added section', () => { - const obj = { + const expectedResult = { + add: { 'refs/for/*': { permissions: { 'label-Code-Review': { @@ -440,798 +482,764 @@ label: 'Code-Review', }, 'labelAs-Code-Review': { - rules: { - 'ldap:gerritcodereview-eng': { - min: -2, - max: 2, - action: 'ALLOW', - added: true, - deleted: true, - }, - }, + rules: {}, added: true, label: 'Code-Review', }, }, added: true, }, - }; + }, + remove: {}, + }; + const updateObj = {add: {}, remove: {}}; + element._recursivelyUpdateAddRemoveObj(obj, updateObj); + assert.deepEqual(updateObj, expectedResult); + }); - const expectedResult = { - add: { - 'refs/for/*': { - permissions: { - 'label-Code-Review': { - rules: { - e798fed07afbc9173a587f876ef8760c78d240c1: { - min: -2, - max: 2, - action: 'ALLOW', - added: true, - }, - }, - added: true, - label: 'Code-Review', - }, - 'labelAs-Code-Review': { - rules: {}, - added: true, - label: 'Code-Review', - }, - }, - added: true, - }, - }, - remove: {}, - }; - const updateObj = {add: {}, remove: {}}; - element._recursivelyUpdateAddRemoveObj(obj, updateObj); - assert.deepEqual(updateObj, expectedResult); + test('_handleSaveForReview with no changes', () => { + assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}}); + }); + + test('_handleSaveForReview parent change', () => { + element._inheritsFrom = { + id: 'test-project', + }; + element._originalInheritsFrom = { + id: 'test-project-original', + }; + assert.deepEqual(element._computeAddAndRemove(), { + parent: 'test-project', add: {}, remove: {}, }); + }); - test('_handleSaveForReview with no changes', () => { - assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}}); + test('_handleSaveForReview new parent with spaces', () => { + element._inheritsFrom = {id: 'spaces+in+project+name'}; + element._originalInheritsFrom = {id: 'old-project'}; + assert.deepEqual(element._computeAddAndRemove(), { + parent: 'spaces in project name', add: {}, remove: {}, }); + }); - test('_handleSaveForReview parent change', () => { - element._inheritsFrom = { - id: 'test-project', - }; - element._originalInheritsFrom = { - id: 'test-project-original', - }; - assert.deepEqual(element._computeAddAndRemove(), { - parent: 'test-project', add: {}, remove: {}, - }); + test('_handleSaveForReview rules', () => { + // Delete a rule. + element._local['refs/*'].permissions.owner.rules[123].deleted = true; + let expectedInput = { + add: {}, + remove: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {}, + }, + }, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Undo deleting a rule. + delete element._local['refs/*'].permissions.owner.rules[123].deleted; + + // Modify a rule. + element._local['refs/*'].permissions.owner.rules[123].modified = true; + expectedInput = { + add: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {action: 'DENY', modified: true}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {}, + }, + }, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + }); + + test('_computeAddAndRemove permissions', () => { + // Add a new rule to a permission. + let expectedInput = { + add: { + 'refs/*': { + permissions: { + owner: { + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + }, + }, + }, + }, + }, + }, + remove: {}, + }; + + element.shadowRoot + .querySelector('gr-access-section').shadowRoot + .querySelector('gr-permission') + ._handleAddRuleItem( + {detail: {value: {id: 'Maintainers'}}}); + + flushAsynchronousOperations(); + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Remove the added rule. + delete element._local['refs/*'].permissions.owner.rules.Maintainers; + + // Delete a permission. + element._local['refs/*'].permissions.owner.deleted = true; + expectedInput = { + add: {}, + remove: { + 'refs/*': { + permissions: { + owner: {rules: {}}, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Undo delete permission. + delete element._local['refs/*'].permissions.owner.deleted; + + // Modify a permission. + element._local['refs/*'].permissions.owner.modified = true; + expectedInput = { + add: { + 'refs/*': { + permissions: { + owner: { + modified: true, + rules: { + 234: {action: 'ALLOW'}, + 123: {action: 'DENY'}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: { + owner: {rules: {}}, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + }); + + test('_computeAddAndRemove sections', () => { + // Add a new permission to a section + let expectedInput = { + add: { + 'refs/*': { + permissions: { + 'label-Code-Review': { + added: true, + rules: {}, + label: 'Code-Review', + }, + }, + }, + }, + remove: {}, + }; + element.shadowRoot + .querySelector('gr-access-section')._handleAddPermission(); + flushAsynchronousOperations(); + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Add a new rule to the new permission. + expectedInput = { + add: { + 'refs/*': { + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + min: -2, + max: 2, + action: 'ALLOW', + added: true, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: {}, + }; + const newPermission = + dom(element.shadowRoot + .querySelector('gr-access-section').root).querySelectorAll( + 'gr-permission')[2]; + newPermission._handleAddRuleItem( + {detail: {value: {id: 'Maintainers'}}}); + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Modify a section reference. + element._local['refs/*'].updatedId = 'refs/for/bar'; + element._local['refs/*'].modified = true; + expectedInput = { + add: { + 'refs/for/bar': { + modified: true, + updatedId: 'refs/for/bar', + permissions: { + 'owner': { + rules: { + 234: {action: 'ALLOW'}, + 123: {action: 'DENY'}, + }, + }, + 'read': { + rules: { + 234: {action: 'ALLOW'}, + }, + }, + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + min: -2, + max: 2, + action: 'ALLOW', + added: true, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Delete a section. + element._local['refs/*'].deleted = true; + expectedInput = { + add: {}, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + }); + + test('_computeAddAndRemove new section', () => { + // Add a new permission to a section + let expectedInput = { + add: { + 'refs/for/*': { + added: true, + permissions: {}, + }, + }, + remove: {}, + }; + MockInteractions.tap(element.$.addReferenceBtn); + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + expectedInput = { + add: { + 'refs/for/*': { + added: true, + permissions: { + 'label-Code-Review': { + added: true, + rules: {}, + label: 'Code-Review', + }, + }, + }, + }, + remove: {}, + }; + const newSection = dom(element.root) + .querySelectorAll('gr-access-section')[1]; + newSection._handleAddPermission(); + flushAsynchronousOperations(); + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Add rule to the new permission. + expectedInput = { + add: { + 'refs/for/*': { + added: true, + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + max: 2, + min: -2, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: {}, + }; + + newSection.shadowRoot + .querySelector('gr-permission')._handleAddRuleItem( + {detail: {value: {id: 'Maintainers'}}}); + + flushAsynchronousOperations(); + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Modify a the reference from the default value. + element._local['refs/for/*'].updatedId = 'refs/for/new'; + expectedInput = { + add: { + 'refs/for/new': { + added: true, + updatedId: 'refs/for/new', + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + max: 2, + min: -2, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: {}, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + }); + + test('_computeAddAndRemove combinations', () => { + // Modify rule and delete permission that it is inside of. + element._local['refs/*'].permissions.owner.rules[123].modified = true; + element._local['refs/*'].permissions.owner.deleted = true; + let expectedInput = { + add: {}, + remove: { + 'refs/*': { + permissions: { + owner: {rules: {}}, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + // Delete rule and delete permission that it is inside of. + element._local['refs/*'].permissions.owner.rules[123].modified = false; + element._local['refs/*'].permissions.owner.rules[123].deleted = true; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Also modify a different rule inside of another permission. + element._local['refs/*'].permissions.read.modified = true; + expectedInput = { + add: { + 'refs/*': { + permissions: { + read: { + modified: true, + rules: { + 234: {action: 'ALLOW'}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: { + owner: {rules: {}}, + read: {rules: {}}, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + // Modify both permissions with an exclusive bit. Owner is still + // deleted. + element._local['refs/*'].permissions.owner.exclusive = true; + element._local['refs/*'].permissions.owner.modified = true; + element._local['refs/*'].permissions.read.exclusive = true; + element._local['refs/*'].permissions.read.modified = true; + expectedInput = { + add: { + 'refs/*': { + permissions: { + read: { + exclusive: true, + modified: true, + rules: { + 234: {action: 'ALLOW'}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: { + owner: {rules: {}}, + read: {rules: {}}, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Add a rule to the existing permission; + const readPermission = + dom(element.shadowRoot + .querySelector('gr-access-section').root).querySelectorAll( + 'gr-permission')[1]; + readPermission._handleAddRuleItem( + {detail: {value: {id: 'Maintainers'}}}); + + expectedInput = { + add: { + 'refs/*': { + permissions: { + read: { + exclusive: true, + modified: true, + rules: { + 234: {action: 'ALLOW'}, + Maintainers: {action: 'ALLOW', added: true}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: { + owner: {rules: {}}, + read: {rules: {}}, + }, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Change one of the refs + element._local['refs/*'].updatedId = 'refs/for/bar'; + element._local['refs/*'].modified = true; + + expectedInput = { + add: { + 'refs/for/bar': { + modified: true, + updatedId: 'refs/for/bar', + permissions: { + read: { + exclusive: true, + modified: true, + rules: { + 234: {action: 'ALLOW'}, + Maintainers: {action: 'ALLOW', added: true}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + expectedInput = { + add: {}, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + element._local['refs/*'].deleted = true; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Add a new section. + MockInteractions.tap(element.$.addReferenceBtn); + let newSection = dom(element.root) + .querySelectorAll('gr-access-section')[1]; + newSection._handleAddPermission(); + flushAsynchronousOperations(); + newSection.shadowRoot + .querySelector('gr-permission')._handleAddRuleItem( + {detail: {value: {id: 'Maintainers'}}}); + // Modify a the reference from the default value. + element._local['refs/for/*'].updatedId = 'refs/for/new'; + + expectedInput = { + add: { + 'refs/for/new': { + added: true, + updatedId: 'refs/for/new', + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + max: 2, + min: -2, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Modify newly added rule inside new ref. + element._local['refs/for/*'].permissions['label-Code-Review']. + rules['Maintainers'].modified = true; + expectedInput = { + add: { + 'refs/for/new': { + added: true, + updatedId: 'refs/for/new', + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + modified: true, + max: 2, + min: -2, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + + // Add a second new section. + MockInteractions.tap(element.$.addReferenceBtn); + newSection = dom(element.root) + .querySelectorAll('gr-access-section')[2]; + newSection._handleAddPermission(); + flushAsynchronousOperations(); + newSection.shadowRoot + .querySelector('gr-permission')._handleAddRuleItem( + {detail: {value: {id: 'Maintainers'}}}); + // Modify a the reference from the default value. + element._local['refs/for/**'].updatedId = 'refs/for/new2'; + expectedInput = { + add: { + 'refs/for/new': { + added: true, + updatedId: 'refs/for/new', + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + modified: true, + max: 2, + min: -2, + }, + }, + label: 'Code-Review', + }, + }, + }, + 'refs/for/new2': { + added: true, + updatedId: 'refs/for/new2', + permissions: { + 'label-Code-Review': { + added: true, + rules: { + Maintainers: { + action: 'ALLOW', + added: true, + max: 2, + min: -2, + }, + }, + label: 'Code-Review', + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: {}, + }, + }, + }; + assert.deepEqual(element._computeAddAndRemove(), expectedInput); + }); + + test('Unsaved added refs are discarded when edit cancelled', () => { + // Unsaved changes are discarded when editing is cancelled. + MockInteractions.tap(element.$.editBtn); + assert.equal(element._sections.length, 1); + assert.equal(Object.keys(element._local).length, 1); + MockInteractions.tap(element.$.addReferenceBtn); + assert.equal(element._sections.length, 2); + assert.equal(Object.keys(element._local).length, 2); + MockInteractions.tap(element.$.editBtn); + assert.equal(element._sections.length, 1); + assert.equal(Object.keys(element._local).length, 1); + }); + + test('_handleSave', done => { + const repoAccessInput = { + add: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {action: 'DENY', modified: true}, + }, + }, + }, + }, + }, + remove: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {}, + }, + }, + }, + }, + }, + }; + sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns( + Promise.resolve(JSON.parse(JSON.stringify(accessRes)))); + sandbox.stub(Gerrit.Nav, 'navigateToChange'); + let resolver; + const saveStub = sandbox.stub(element.$.restAPI, + 'setRepoAccessRights') + .returns(new Promise(r => resolver = r)); + + element.repo = 'test-repo'; + sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput); + + element._modified = true; + MockInteractions.tap(element.$.saveBtn); + assert.equal(element.$.saveBtn.hasAttribute('loading'), true); + resolver({_number: 1}); + flush(() => { + assert.isTrue(saveStub.called); + assert.isTrue(Gerrit.Nav.navigateToChange.notCalled); + done(); }); + }); - test('_handleSaveForReview new parent with spaces', () => { - element._inheritsFrom = {id: 'spaces+in+project+name'}; - element._originalInheritsFrom = {id: 'old-project'}; - assert.deepEqual(element._computeAddAndRemove(), { - parent: 'spaces in project name', add: {}, remove: {}, - }); - }); - - test('_handleSaveForReview rules', () => { - // Delete a rule. - element._local['refs/*'].permissions.owner.rules[123].deleted = true; - let expectedInput = { - add: {}, - remove: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {}, - }, + test('_handleSaveForReview', done => { + const repoAccessInput = { + add: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {action: 'DENY', modified: true}, }, }, }, }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Undo deleting a rule. - delete element._local['refs/*'].permissions.owner.rules[123].deleted; - - // Modify a rule. - element._local['refs/*'].permissions.owner.rules[123].modified = true; - expectedInput = { - add: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {action: 'DENY', modified: true}, - }, + }, + remove: { + 'refs/*': { + permissions: { + owner: { + rules: { + 123: {}, }, }, }, }, - remove: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {}, - }, - }, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - }); + }, + }; + sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns( + Promise.resolve(JSON.parse(JSON.stringify(accessRes)))); + sandbox.stub(Gerrit.Nav, 'navigateToChange'); + let resolver; + const saveForReviewStub = sandbox.stub(element.$.restAPI, + 'setRepoAccessRightsForReview') + .returns(new Promise(r => resolver = r)); - test('_computeAddAndRemove permissions', () => { - // Add a new rule to a permission. - let expectedInput = { - add: { - 'refs/*': { - permissions: { - owner: { - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - }, - }, - }, - }, - }, - }, - remove: {}, - }; + element.repo = 'test-repo'; + sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput); - element.shadowRoot - .querySelector('gr-access-section').shadowRoot - .querySelector('gr-permission') - ._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); - - flushAsynchronousOperations(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Remove the added rule. - delete element._local['refs/*'].permissions.owner.rules.Maintainers; - - // Delete a permission. - element._local['refs/*'].permissions.owner.deleted = true; - expectedInput = { - add: {}, - remove: { - 'refs/*': { - permissions: { - owner: {rules: {}}, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Undo delete permission. - delete element._local['refs/*'].permissions.owner.deleted; - - // Modify a permission. - element._local['refs/*'].permissions.owner.modified = true; - expectedInput = { - add: { - 'refs/*': { - permissions: { - owner: { - modified: true, - rules: { - 234: {action: 'ALLOW'}, - 123: {action: 'DENY'}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: { - owner: {rules: {}}, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - }); - - test('_computeAddAndRemove sections', () => { - // Add a new permission to a section - let expectedInput = { - add: { - 'refs/*': { - permissions: { - 'label-Code-Review': { - added: true, - rules: {}, - label: 'Code-Review', - }, - }, - }, - }, - remove: {}, - }; - element.shadowRoot - .querySelector('gr-access-section')._handleAddPermission(); - flushAsynchronousOperations(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Add a new rule to the new permission. - expectedInput = { - add: { - 'refs/*': { - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - min: -2, - max: 2, - action: 'ALLOW', - added: true, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: {}, - }; - const newPermission = - Polymer.dom(element.shadowRoot - .querySelector('gr-access-section').root).querySelectorAll( - 'gr-permission')[2]; - newPermission._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Modify a section reference. - element._local['refs/*'].updatedId = 'refs/for/bar'; - element._local['refs/*'].modified = true; - expectedInput = { - add: { - 'refs/for/bar': { - modified: true, - updatedId: 'refs/for/bar', - permissions: { - 'owner': { - rules: { - 234: {action: 'ALLOW'}, - 123: {action: 'DENY'}, - }, - }, - 'read': { - rules: { - 234: {action: 'ALLOW'}, - }, - }, - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - min: -2, - max: 2, - action: 'ALLOW', - added: true, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Delete a section. - element._local['refs/*'].deleted = true; - expectedInput = { - add: {}, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - }); - - test('_computeAddAndRemove new section', () => { - // Add a new permission to a section - let expectedInput = { - add: { - 'refs/for/*': { - added: true, - permissions: {}, - }, - }, - remove: {}, - }; - MockInteractions.tap(element.$.addReferenceBtn); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - expectedInput = { - add: { - 'refs/for/*': { - added: true, - permissions: { - 'label-Code-Review': { - added: true, - rules: {}, - label: 'Code-Review', - }, - }, - }, - }, - remove: {}, - }; - const newSection = Polymer.dom(element.root) - .querySelectorAll('gr-access-section')[1]; - newSection._handleAddPermission(); - flushAsynchronousOperations(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Add rule to the new permission. - expectedInput = { - add: { - 'refs/for/*': { - added: true, - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - max: 2, - min: -2, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: {}, - }; - - newSection.shadowRoot - .querySelector('gr-permission')._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); - - flushAsynchronousOperations(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Modify a the reference from the default value. - element._local['refs/for/*'].updatedId = 'refs/for/new'; - expectedInput = { - add: { - 'refs/for/new': { - added: true, - updatedId: 'refs/for/new', - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - max: 2, - min: -2, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: {}, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - }); - - test('_computeAddAndRemove combinations', () => { - // Modify rule and delete permission that it is inside of. - element._local['refs/*'].permissions.owner.rules[123].modified = true; - element._local['refs/*'].permissions.owner.deleted = true; - let expectedInput = { - add: {}, - remove: { - 'refs/*': { - permissions: { - owner: {rules: {}}, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - // Delete rule and delete permission that it is inside of. - element._local['refs/*'].permissions.owner.rules[123].modified = false; - element._local['refs/*'].permissions.owner.rules[123].deleted = true; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Also modify a different rule inside of another permission. - element._local['refs/*'].permissions.read.modified = true; - expectedInput = { - add: { - 'refs/*': { - permissions: { - read: { - modified: true, - rules: { - 234: {action: 'ALLOW'}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: { - owner: {rules: {}}, - read: {rules: {}}, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - // Modify both permissions with an exclusive bit. Owner is still - // deleted. - element._local['refs/*'].permissions.owner.exclusive = true; - element._local['refs/*'].permissions.owner.modified = true; - element._local['refs/*'].permissions.read.exclusive = true; - element._local['refs/*'].permissions.read.modified = true; - expectedInput = { - add: { - 'refs/*': { - permissions: { - read: { - exclusive: true, - modified: true, - rules: { - 234: {action: 'ALLOW'}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: { - owner: {rules: {}}, - read: {rules: {}}, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Add a rule to the existing permission; - const readPermission = - Polymer.dom(element.shadowRoot - .querySelector('gr-access-section').root).querySelectorAll( - 'gr-permission')[1]; - readPermission._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); - - expectedInput = { - add: { - 'refs/*': { - permissions: { - read: { - exclusive: true, - modified: true, - rules: { - 234: {action: 'ALLOW'}, - Maintainers: {action: 'ALLOW', added: true}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: { - owner: {rules: {}}, - read: {rules: {}}, - }, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Change one of the refs - element._local['refs/*'].updatedId = 'refs/for/bar'; - element._local['refs/*'].modified = true; - - expectedInput = { - add: { - 'refs/for/bar': { - modified: true, - updatedId: 'refs/for/bar', - permissions: { - read: { - exclusive: true, - modified: true, - rules: { - 234: {action: 'ALLOW'}, - Maintainers: {action: 'ALLOW', added: true}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - expectedInput = { - add: {}, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - element._local['refs/*'].deleted = true; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Add a new section. - MockInteractions.tap(element.$.addReferenceBtn); - let newSection = Polymer.dom(element.root) - .querySelectorAll('gr-access-section')[1]; - newSection._handleAddPermission(); - flushAsynchronousOperations(); - newSection.shadowRoot - .querySelector('gr-permission')._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); - // Modify a the reference from the default value. - element._local['refs/for/*'].updatedId = 'refs/for/new'; - - expectedInput = { - add: { - 'refs/for/new': { - added: true, - updatedId: 'refs/for/new', - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - max: 2, - min: -2, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Modify newly added rule inside new ref. - element._local['refs/for/*'].permissions['label-Code-Review']. - rules['Maintainers'].modified = true; - expectedInput = { - add: { - 'refs/for/new': { - added: true, - updatedId: 'refs/for/new', - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - modified: true, - max: 2, - min: -2, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - - // Add a second new section. - MockInteractions.tap(element.$.addReferenceBtn); - newSection = Polymer.dom(element.root) - .querySelectorAll('gr-access-section')[2]; - newSection._handleAddPermission(); - flushAsynchronousOperations(); - newSection.shadowRoot - .querySelector('gr-permission')._handleAddRuleItem( - {detail: {value: {id: 'Maintainers'}}}); - // Modify a the reference from the default value. - element._local['refs/for/**'].updatedId = 'refs/for/new2'; - expectedInput = { - add: { - 'refs/for/new': { - added: true, - updatedId: 'refs/for/new', - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - modified: true, - max: 2, - min: -2, - }, - }, - label: 'Code-Review', - }, - }, - }, - 'refs/for/new2': { - added: true, - updatedId: 'refs/for/new2', - permissions: { - 'label-Code-Review': { - added: true, - rules: { - Maintainers: { - action: 'ALLOW', - added: true, - max: 2, - min: -2, - }, - }, - label: 'Code-Review', - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: {}, - }, - }, - }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); - }); - - test('Unsaved added refs are discarded when edit cancelled', () => { - // Unsaved changes are discarded when editing is cancelled. - MockInteractions.tap(element.$.editBtn); - assert.equal(element._sections.length, 1); - assert.equal(Object.keys(element._local).length, 1); - MockInteractions.tap(element.$.addReferenceBtn); - assert.equal(element._sections.length, 2); - assert.equal(Object.keys(element._local).length, 2); - MockInteractions.tap(element.$.editBtn); - assert.equal(element._sections.length, 1); - assert.equal(Object.keys(element._local).length, 1); - }); - - test('_handleSave', done => { - const repoAccessInput = { - add: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {action: 'DENY', modified: true}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {}, - }, - }, - }, - }, - }, - }; - sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns( - Promise.resolve(JSON.parse(JSON.stringify(accessRes)))); - sandbox.stub(Gerrit.Nav, 'navigateToChange'); - let resolver; - const saveStub = sandbox.stub(element.$.restAPI, - 'setRepoAccessRights') - .returns(new Promise(r => resolver = r)); - - element.repo = 'test-repo'; - sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput); - - element._modified = true; - MockInteractions.tap(element.$.saveBtn); - assert.equal(element.$.saveBtn.hasAttribute('loading'), true); - resolver({_number: 1}); - flush(() => { - assert.isTrue(saveStub.called); - assert.isTrue(Gerrit.Nav.navigateToChange.notCalled); - done(); - }); - }); - - test('_handleSaveForReview', done => { - const repoAccessInput = { - add: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {action: 'DENY', modified: true}, - }, - }, - }, - }, - }, - remove: { - 'refs/*': { - permissions: { - owner: { - rules: { - 123: {}, - }, - }, - }, - }, - }, - }; - sandbox.stub(element.$.restAPI, 'getRepoAccessRights').returns( - Promise.resolve(JSON.parse(JSON.stringify(accessRes)))); - sandbox.stub(Gerrit.Nav, 'navigateToChange'); - let resolver; - const saveForReviewStub = sandbox.stub(element.$.restAPI, - 'setRepoAccessRightsForReview') - .returns(new Promise(r => resolver = r)); - - element.repo = 'test-repo'; - sandbox.stub(element, '_computeAddAndRemove').returns(repoAccessInput); - - element._modified = true; - MockInteractions.tap(element.$.saveReviewBtn); - assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true); - resolver({_number: 1}); - flush(() => { - assert.isTrue(saveForReviewStub.called); - assert.isTrue(Gerrit.Nav.navigateToChange - .lastCall.calledWithExactly({_number: 1})); - done(); - }); + element._modified = true; + MockInteractions.tap(element.$.saveReviewBtn); + assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true); + resolver({_number: 1}); + flush(() => { + assert.isTrue(saveForReviewStub.called); + assert.isTrue(Gerrit.Nav.navigateToChange + .lastCall.calledWithExactly({_number: 1})); + done(); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js index 622bfe4..53b4989 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command.js
@@ -14,34 +14,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrRepoCommand extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-repo-command'; } +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-command_html.js'; - static get properties() { - return { - title: String, - disabled: Boolean, - tooltip: String, - }; - } +/** @extends Polymer.Element */ +class GrRepoCommand extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** - * Fired when command button is tapped. - * - * @event command-tap - */ + static get is() { return 'gr-repo-command'; } - _onCommandTap() { - this.dispatchEvent( - new CustomEvent('command-tap', {bubbles: true, composed: true})); - } + static get properties() { + return { + title: String, + disabled: Boolean, + tooltip: String, + }; } - customElements.define(GrRepoCommand.is, GrRepoCommand); -})(); + /** + * Fired when command button is tapped. + * + * @event command-tap + */ + + _onCommandTap() { + this.dispatchEvent( + new CustomEvent('command-tap', {bubbles: true, composed: true})); + } +} + +customElements.define(GrRepoCommand.is, GrRepoCommand);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js index 29bc02d..10d22fc 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> - -<dom-module id="gr-repo-command"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -27,13 +24,7 @@ } </style> <h3>[[title]]</h3> - <gr-button - title$="[[tooltip]]" - disabled$="[[disabled]]" - on-click - ="_onCommandTap"> + <gr-button title\$="[[tooltip]]" disabled\$="[[disabled]]" on-click="_onCommandTap"> [[title]] </gr-button> - </template> - <script src="gr-repo-command.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html index f4988a5..a3f507b 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-command/gr-repo-command_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-command</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-command.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-command.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-command.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,21 +40,24 @@ </template> </test-fixture> -<script> - suite('gr-repo-command tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-command.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-repo-command tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); - - test('dispatched command-tap on button tap', done => { - element.addEventListener('command-tap', () => { - done(); - }); - MockInteractions.tap( - Polymer.dom(element.root).querySelector('gr-button')); - }); + setup(() => { + element = fixture('basic'); }); + + test('dispatched command-tap on button tap', done => { + element.addEventListener('command-tap', () => { + done(); + }); + MockInteractions.tap( + dom(element.root).querySelector('gr-button')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js index 80b187a..de9d8e2 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands.js
@@ -14,113 +14,131 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const GC_MESSAGE = 'Garbage collection completed successfully.'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-subpage-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-create-change-dialog/gr-create-change-dialog.js'; +import '../gr-repo-command/gr-repo-command.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-commands_html.js'; - const CONFIG_BRANCH = 'refs/meta/config'; - const CONFIG_PATH = 'project.config'; - const EDIT_CONFIG_SUBJECT = 'Edit Repo Config'; - const INITIAL_PATCHSET = 1; - const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.'; - const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change'; +const GC_MESSAGE = 'Garbage collection completed successfully.'; - /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrRepoCommands extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-commands'; } +const CONFIG_BRANCH = 'refs/meta/config'; +const CONFIG_PATH = 'project.config'; +const EDIT_CONFIG_SUBJECT = 'Edit Repo Config'; +const INITIAL_PATCHSET = 1; +const CREATE_CHANGE_FAILED_MESSAGE = 'Failed to create change.'; +const CREATE_CHANGE_SUCCEEDED_MESSAGE = 'Navigating to change'; - static get properties() { - return { - params: Object, - repo: String, - _loading: { - type: Boolean, - value: true, - }, - /** @type {?} */ - _repoConfig: Object, - _canCreate: Boolean, - }; - } +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrRepoCommands extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this._loadRepo(); + static get is() { return 'gr-repo-commands'; } - this.fire('title-change', {title: 'Repo Commands'}); - } - - _loadRepo() { - if (!this.repo) { return Promise.resolve(); } - - const errFn = response => { - this.fire('page-error', {response}); - }; - - return this.$.restAPI.getProjectConfig(this.repo, errFn) - .then(config => { - if (!config) { return Promise.resolve(); } - - this._repoConfig = config; - this._loading = false; - }); - } - - _computeLoadingClass(loading) { - return loading ? 'loading' : ''; - } - - _isLoading() { - return this._loading || this._loading === undefined; - } - - _handleRunningGC() { - return this.$.restAPI.runRepoGC(this.repo).then(response => { - if (response.status === 200) { - this.dispatchEvent(new CustomEvent( - 'show-alert', - {detail: {message: GC_MESSAGE}, bubbles: true, composed: true})); - } - }); - } - - _createNewChange() { - this.$.createChangeOverlay.open(); - } - - _handleCreateChange() { - this.$.createNewChangeModal.handleCreateChange(); - this._handleCloseCreateChange(); - } - - _handleCloseCreateChange() { - this.$.createChangeOverlay.close(); - } - - _handleEditRepoConfig() { - return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH, - EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => { - const message = change ? - CREATE_CHANGE_SUCCEEDED_MESSAGE : - CREATE_CHANGE_FAILED_MESSAGE; - this.dispatchEvent(new CustomEvent('show-alert', - {detail: {message}, bubbles: true, composed: true})); - if (!change) { return; } - - Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff( - change, CONFIG_PATH, INITIAL_PATCHSET)); - }); - } + static get properties() { + return { + params: Object, + repo: String, + _loading: { + type: Boolean, + value: true, + }, + /** @type {?} */ + _repoConfig: Object, + _canCreate: Boolean, + }; } - customElements.define(GrRepoCommands.is, GrRepoCommands); -})(); + /** @override */ + attached() { + super.attached(); + this._loadRepo(); + + this.fire('title-change', {title: 'Repo Commands'}); + } + + _loadRepo() { + if (!this.repo) { return Promise.resolve(); } + + const errFn = response => { + this.fire('page-error', {response}); + }; + + return this.$.restAPI.getProjectConfig(this.repo, errFn) + .then(config => { + if (!config) { return Promise.resolve(); } + + this._repoConfig = config; + this._loading = false; + }); + } + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + } + + _isLoading() { + return this._loading || this._loading === undefined; + } + + _handleRunningGC() { + return this.$.restAPI.runRepoGC(this.repo).then(response => { + if (response.status === 200) { + this.dispatchEvent(new CustomEvent( + 'show-alert', + {detail: {message: GC_MESSAGE}, bubbles: true, composed: true})); + } + }); + } + + _createNewChange() { + this.$.createChangeOverlay.open(); + } + + _handleCreateChange() { + this.$.createNewChangeModal.handleCreateChange(); + this._handleCloseCreateChange(); + } + + _handleCloseCreateChange() { + this.$.createChangeOverlay.close(); + } + + _handleEditRepoConfig() { + return this.$.restAPI.createChange(this.repo, CONFIG_BRANCH, + EDIT_CONFIG_SUBJECT, undefined, false, true).then(change => { + const message = change ? + CREATE_CHANGE_SUCCEEDED_MESSAGE : + CREATE_CHANGE_FAILED_MESSAGE; + this.dispatchEvent(new CustomEvent('show-alert', + {detail: {message}, bubbles: true, composed: true})); + if (!change) { return; } + + Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getEditUrlForDiff( + change, CONFIG_PATH, INITIAL_PATCHSET)); + }); + } +} + +customElements.define(GrRepoCommands.is, GrRepoCommands);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js index b610460..ce19555 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_html.js
@@ -1,36 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-subpage-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-create-change-dialog/gr-create-change-dialog.html"> -<link rel="import" href="../gr-repo-command/gr-repo-command.html"> - -<dom-module id="gr-repo-commands"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -42,24 +28,15 @@ </style> <main class="gr-form-styles read-only"> <h1 id="Title">Repository Commands</h1> - <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div> - <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> + <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div> + <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]"> <h2 id="options">Command</h2> <div id="form"> - <gr-repo-command - title="Create change" - on-command-tap="_createNewChange"> + <gr-repo-command title="Create change" on-command-tap="_createNewChange"> </gr-repo-command> - <gr-repo-command - id="editRepoConfig" - title="Edit repo config" - on-command-tap="_handleEditRepoConfig"> + <gr-repo-command id="editRepoConfig" title="Edit repo config" on-command-tap="_handleEditRepoConfig"> </gr-repo-command> - <gr-repo-command - title="[[_repoConfig.actions.gc.label]]" - tooltip="[[_repoConfig.actions.gc.title]]" - hidden$="[[!_repoConfig.actions.gc.enabled]]" - on-command-tap="_handleRunningGC"> + <gr-repo-command title="[[_repoConfig.actions.gc.label]]" tooltip="[[_repoConfig.actions.gc.title]]" hidden\$="[[!_repoConfig.actions.gc.enabled]]" on-command-tap="_handleRunningGC"> </gr-repo-command> <gr-endpoint-decorator name="repo-command"> <gr-endpoint-param name="config" value="[[_repoConfig]]"> @@ -70,25 +47,15 @@ </div> </div> </main> - <gr-overlay id="createChangeOverlay" with-backdrop> - <gr-dialog - id="createChangeDialog" - confirm-label="Create" - disabled="[[!_canCreate]]" - on-confirm="_handleCreateChange" - on-cancel="_handleCloseCreateChange"> + <gr-overlay id="createChangeOverlay" with-backdrop=""> + <gr-dialog id="createChangeDialog" confirm-label="Create" disabled="[[!_canCreate]]" on-confirm="_handleCreateChange" on-cancel="_handleCloseCreateChange"> <div class="header" slot="header"> Create Change </div> <div class="main" slot="main"> - <gr-create-change-dialog - id="createNewChangeModal" - can-create="{{_canCreate}}" - repo-name="[[repo]]"></gr-create-change-dialog> + <gr-create-change-dialog id="createNewChangeModal" can-create="{{_canCreate}}" repo-name="[[repo]]"></gr-create-change-dialog> </div> </gr-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-commands.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html index da8b57f..c2f71e7 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-commands</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-commands.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-commands.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-commands.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,111 +41,113 @@ </template> </test-fixture> -<script> - suite('gr-repo-commands tests', async () => { - await readyToTest(); - let element; - let sandbox; - let repoStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-commands.js'; +suite('gr-repo-commands tests', () => { + let element; + let sandbox; + let repoStub; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + repoStub = sandbox.stub( + element.$.restAPI, + 'getProjectConfig', + () => Promise.resolve({})); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('create new change dialog', () => { + test('_createNewChange opens modal', () => { + const openStub = sandbox.stub(element.$.createChangeOverlay, 'open'); + element._createNewChange(); + assert.isTrue(openStub.called); + }); + + test('_handleCreateChange called when confirm fired', () => { + sandbox.stub(element, '_handleCreateChange'); + element.$.createChangeDialog.fire('confirm'); + assert.isTrue(element._handleCreateChange.called); + }); + + test('_handleCloseCreateChange called when cancel fired', () => { + sandbox.stub(element, '_handleCloseCreateChange'); + element.$.createChangeDialog.fire('cancel'); + assert.isTrue(element._handleCloseCreateChange.called); + }); + }); + + suite('edit repo config', () => { + let createChangeStub; + let urlStub; + let handleSpy; + let alertStub; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - repoStub = sandbox.stub( - element.$.restAPI, - 'getProjectConfig', - () => Promise.resolve({})); + createChangeStub = sandbox.stub(element.$.restAPI, 'createChange'); + urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'); + sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); + handleSpy = sandbox.spy(element, '_handleEditRepoConfig'); + alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); }); - teardown(() => { - sandbox.restore(); - }); + test('successful creation of change', () => { + const change = {_number: '1'}; + createChangeStub.returns(Promise.resolve(change)); + MockInteractions.tap(element.$.editRepoConfig.shadowRoot + .querySelector('gr-button')); + return handleSpy.lastCall.returnValue.then(() => { + flushAsynchronousOperations(); - suite('create new change dialog', () => { - test('_createNewChange opens modal', () => { - const openStub = sandbox.stub(element.$.createChangeOverlay, 'open'); - element._createNewChange(); - assert.isTrue(openStub.called); - }); - - test('_handleCreateChange called when confirm fired', () => { - sandbox.stub(element, '_handleCreateChange'); - element.$.createChangeDialog.fire('confirm'); - assert.isTrue(element._handleCreateChange.called); - }); - - test('_handleCloseCreateChange called when cancel fired', () => { - sandbox.stub(element, '_handleCloseCreateChange'); - element.$.createChangeDialog.fire('cancel'); - assert.isTrue(element._handleCloseCreateChange.called); + assert.isTrue(alertStub.called); + assert.equal(alertStub.lastCall.args[0].detail.message, + 'Navigating to change'); + assert.isTrue(urlStub.called); + assert.deepEqual(urlStub.lastCall.args, + [change, 'project.config', 1]); }); }); - suite('edit repo config', () => { - let createChangeStub; - let urlStub; - let handleSpy; - let alertStub; + test('unsuccessful creation of change', () => { + createChangeStub.returns(Promise.resolve(null)); + MockInteractions.tap(element.$.editRepoConfig.shadowRoot + .querySelector('gr-button')); + return handleSpy.lastCall.returnValue.then(() => { + flushAsynchronousOperations(); - setup(() => { - createChangeStub = sandbox.stub(element.$.restAPI, 'createChange'); - urlStub = sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'); - sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); - handleSpy = sandbox.spy(element, '_handleEditRepoConfig'); - alertStub = sandbox.stub(); - element.addEventListener('show-alert', alertStub); - }); - - test('successful creation of change', () => { - const change = {_number: '1'}; - createChangeStub.returns(Promise.resolve(change)); - MockInteractions.tap(element.$.editRepoConfig.shadowRoot - .querySelector('gr-button')); - return handleSpy.lastCall.returnValue.then(() => { - flushAsynchronousOperations(); - - assert.isTrue(alertStub.called); - assert.equal(alertStub.lastCall.args[0].detail.message, - 'Navigating to change'); - assert.isTrue(urlStub.called); - assert.deepEqual(urlStub.lastCall.args, - [change, 'project.config', 1]); - }); - }); - - test('unsuccessful creation of change', () => { - createChangeStub.returns(Promise.resolve(null)); - MockInteractions.tap(element.$.editRepoConfig.shadowRoot - .querySelector('gr-button')); - return handleSpy.lastCall.returnValue.then(() => { - flushAsynchronousOperations(); - - assert.isTrue(alertStub.called); - assert.equal(alertStub.lastCall.args[0].detail.message, - 'Failed to create change.'); - assert.isFalse(urlStub.called); - }); - }); - }); - - suite('404', () => { - test('fires page-error', done => { - repoStub.restore(); - - element.repo = 'test'; - - const response = {status: 404}; - sandbox.stub( - element.$.restAPI, 'getProjectConfig', (repo, errFn) => { - errFn(response); - }); - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element._loadRepo(); + assert.isTrue(alertStub.called); + assert.equal(alertStub.lastCall.args[0].detail.message, + 'Failed to create change.'); + assert.isFalse(urlStub.called); }); }); }); + + suite('404', () => { + test('fires page-error', done => { + repoStub.restore(); + + element.repo = 'test'; + + const response = {status: 404}; + sandbox.stub( + element.$.restAPI, 'getProjectConfig', (repo, errFn) => { + errFn(response); + }); + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + element._loadRepo(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js index 8e09263..e1f38c9 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2018 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. @@ -14,89 +14,100 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrRepoDashboards extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-dashboards'; } +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-dashboards_html.js'; - static get properties() { - return { - repo: { - type: String, - observer: '_repoChanged', - }, - _loading: { - type: Boolean, - value: true, - }, - _dashboards: Array, - }; - } +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrRepoDashboards extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _repoChanged(repo) { - this._loading = true; - if (!repo) { return Promise.resolve(); } + static get is() { return 'gr-repo-dashboards'; } - const errFn = response => { - this.fire('page-error', {response}); - }; - - this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => { - if (!res) { return Promise.resolve(); } - - // Group by ref and sort by id. - const dashboards = res.concat.apply([], res).sort((a, b) => - (a.id < b.id ? -1 : 1)); - const dashboardsByRef = {}; - dashboards.forEach(d => { - if (!dashboardsByRef[d.ref]) { - dashboardsByRef[d.ref] = []; - } - dashboardsByRef[d.ref].push(d); - }); - - const dashboardBuilder = []; - Object.keys(dashboardsByRef).sort() - .forEach(ref => { - dashboardBuilder.push({ - section: ref, - dashboards: dashboardsByRef[ref], - }); - }); - - this._dashboards = dashboardBuilder; - this._loading = false; - Polymer.dom.flush(); - }); - } - - _getUrl(project, id) { - if (!project || !id) { return ''; } - - return Gerrit.Nav.getUrlForRepoDashboard(project, id); - } - - _computeLoadingClass(loading) { - return loading ? 'loading' : ''; - } - - _computeInheritedFrom(project, definingProject) { - return project === definingProject ? '' : definingProject; - } - - _computeIsDefault(isDefault) { - return isDefault ? '✓' : ''; - } + static get properties() { + return { + repo: { + type: String, + observer: '_repoChanged', + }, + _loading: { + type: Boolean, + value: true, + }, + _dashboards: Array, + }; } - customElements.define(GrRepoDashboards.is, GrRepoDashboards); -})(); + _repoChanged(repo) { + this._loading = true; + if (!repo) { return Promise.resolve(); } + + const errFn = response => { + this.fire('page-error', {response}); + }; + + this.$.restAPI.getRepoDashboards(this.repo, errFn).then(res => { + if (!res) { return Promise.resolve(); } + + // Group by ref and sort by id. + const dashboards = res.concat.apply([], res).sort((a, b) => + (a.id < b.id ? -1 : 1)); + const dashboardsByRef = {}; + dashboards.forEach(d => { + if (!dashboardsByRef[d.ref]) { + dashboardsByRef[d.ref] = []; + } + dashboardsByRef[d.ref].push(d); + }); + + const dashboardBuilder = []; + Object.keys(dashboardsByRef).sort() + .forEach(ref => { + dashboardBuilder.push({ + section: ref, + dashboards: dashboardsByRef[ref], + }); + }); + + this._dashboards = dashboardBuilder; + this._loading = false; + flush(); + }); + } + + _getUrl(project, id) { + if (!project || !id) { return ''; } + + return Gerrit.Nav.getUrlForRepoDashboard(project, id); + } + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + } + + _computeInheritedFrom(project, definingProject) { + return project === definingProject ? '' : definingProject; + } + + _computeIsDefault(isDefault) { + return isDefault ? '✓' : ''; + } +} + +customElements.define(GrRepoDashboards.is, GrRepoDashboards);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js index f74f705..3bac16c 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-repo-dashboards"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -38,8 +33,8 @@ <style include="gr-table-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <table id="list" class$="genericList [[_computeLoadingClass(_loading)]]"> - <tr class="headerRow"> + <table id="list" class\$="genericList [[_computeLoadingClass(_loading)]]"> + <tbody><tr class="headerRow"> <th class="topHeader">Dashboard name</th> <th class="topHeader">Dashboard title</th> <th class="topHeader">Dashboard description</th> @@ -49,14 +44,14 @@ <tr id="loadingContainer"> <td>Loading...</td> </tr> - <tbody id="dashboards"> + </tbody><tbody id="dashboards"> <template is="dom-repeat" items="[[_dashboards]]"> <tr class="groupHeader"> <td colspan="5">[[item.section]]</td> </tr> <template is="dom-repeat" items="[[item.dashboards]]"> <tr class="table"> - <td class="name"><a href$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td> + <td class="name"><a href\$="[[_getUrl(item.project, item.id)]]">[[item.path]]</a></td> <td class="title">[[item.title]]</td> <td class="desc">[[item.description]]</td> <td class="inherited">[[_computeInheritedFrom(item.project, item.defining_project)]]</td> @@ -67,6 +62,4 @@ </tbody> </table> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-dashboards.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html index 681ee19..1d6f05e 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-dashboards</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-dashboards.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-dashboards.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-dashboards.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,128 +40,130 @@ </template> </test-fixture> -<script> - suite('gr-repo-dashboards tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-dashboards.js'; +suite('gr-repo-dashboards tests', () => { + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('dashboard table', () => { setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns( + Promise.resolve([ + { + id: 'default:contributor', + project: 'gerrit', + defining_project: 'gerrit', + ref: 'default', + path: 'contributor', + description: 'Own contributions.', + foreach: 'owner:self', + url: '/dashboard/?params', + title: 'Contributor Dashboard', + sections: [ + { + name: 'Mine To Rebase', + query: 'is:open -is:mergeable', + }, + { + name: 'My Recently Merged', + query: 'is:merged limit:10', + }, + ], + }, + { + id: 'custom:custom2', + project: 'gerrit', + defining_project: 'Public-Projects', + ref: 'custom', + path: 'open', + description: 'Recent open changes.', + url: '/dashboard/?params', + title: 'Open Changes', + sections: [ + { + name: 'Open Changes', + query: 'status:open project:${project} -age:7w', + }, + ], + }, + { + id: 'default:abc', + project: 'gerrit', + ref: 'default', + }, + { + id: 'custom:custom1', + project: 'gerrit', + ref: 'custom', + }, + ])); }); - teardown(() => { - sandbox.restore(); - }); - - suite('dashboard table', () => { - setup(() => { - sandbox.stub(element.$.restAPI, 'getRepoDashboards').returns( - Promise.resolve([ - { - id: 'default:contributor', - project: 'gerrit', - defining_project: 'gerrit', - ref: 'default', - path: 'contributor', - description: 'Own contributions.', - foreach: 'owner:self', - url: '/dashboard/?params', - title: 'Contributor Dashboard', - sections: [ - { - name: 'Mine To Rebase', - query: 'is:open -is:mergeable', - }, - { - name: 'My Recently Merged', - query: 'is:merged limit:10', - }, - ], - }, - { - id: 'custom:custom2', - project: 'gerrit', - defining_project: 'Public-Projects', - ref: 'custom', - path: 'open', - description: 'Recent open changes.', - url: '/dashboard/?params', - title: 'Open Changes', - sections: [ - { - name: 'Open Changes', - query: 'status:open project:${project} -age:7w', - }, - ], - }, - { - id: 'default:abc', - project: 'gerrit', - ref: 'default', - }, - { - id: 'custom:custom1', - project: 'gerrit', - ref: 'custom', - }, - ])); - }); - - test('loading, sections, and ordering', done => { - assert.isTrue(element._loading); - assert.notEqual(getComputedStyle(element.$.loadingContainer).display, + test('loading, sections, and ordering', done => { + assert.isTrue(element._loading); + assert.notEqual(getComputedStyle(element.$.loadingContainer).display, + 'none'); + assert.equal(getComputedStyle(element.$.dashboards).display, + 'none'); + element.repo = 'test'; + flush(() => { + assert.equal(getComputedStyle(element.$.loadingContainer).display, 'none'); - assert.equal(getComputedStyle(element.$.dashboards).display, + assert.notEqual(getComputedStyle(element.$.dashboards).display, 'none'); - element.repo = 'test'; - flush(() => { - assert.equal(getComputedStyle(element.$.loadingContainer).display, - 'none'); - assert.notEqual(getComputedStyle(element.$.dashboards).display, - 'none'); - assert.equal(element._dashboards.length, 2); - assert.equal(element._dashboards[0].section, 'custom'); - assert.equal(element._dashboards[1].section, 'default'); + assert.equal(element._dashboards.length, 2); + assert.equal(element._dashboards[0].section, 'custom'); + assert.equal(element._dashboards[1].section, 'default'); - const dashboards = element._dashboards[0].dashboards; - assert.equal(dashboards.length, 2); - assert.equal(dashboards[0].id, 'custom:custom1'); - assert.equal(dashboards[1].id, 'custom:custom2'); + const dashboards = element._dashboards[0].dashboards; + assert.equal(dashboards.length, 2); + assert.equal(dashboards[0].id, 'custom:custom1'); + assert.equal(dashboards[1].id, 'custom:custom2'); - done(); - }); - }); - }); - - suite('test url', () => { - test('_getUrl', () => { - sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard', - () => '/r/dashboard/test'); - - assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test'); - - assert.equal(element._getUrl(undefined, undefined), ''); - }); - }); - - suite('404', () => { - test('fires page-error', done => { - const response = {status: 404}; - sandbox.stub( - element.$.restAPI, 'getRepoDashboards', (repo, errFn) => { - errFn(response); - }); - - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element.repo = 'test'; + done(); }); }); }); + + suite('test url', () => { + test('_getUrl', () => { + sandbox.stub(Gerrit.Nav, 'getUrlForRepoDashboard', + () => '/r/dashboard/test'); + + assert.equal(element._getUrl('/dashboard/test', {}), '/r/dashboard/test'); + + assert.equal(element._getUrl(undefined, undefined), ''); + }); + }); + + suite('404', () => { + test('fires page-error', done => { + const response = {status: 404}; + sandbox.stub( + element.$.restAPI, 'getRepoDashboards', (repo, errFn) => { + errFn(response); + }); + + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + element.repo = 'test'; + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js index ccfdfc6..82a6a4c 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list.js
@@ -14,279 +14,302 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js'; - const DETAIL_TYPES = { - BRANCHES: 'branches', - TAGS: 'tags', - }; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-account-link/gr-account-link.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-list-view/gr-list-view.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-create-pointer-dialog/gr-create-pointer-dialog.js'; +import '../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-detail-list_html.js'; - const PGP_START = '-----BEGIN PGP SIGNATURE-----'; +const DETAIL_TYPES = { + BRANCHES: 'branches', + TAGS: 'tags', +}; - /** - * @appliesMixin Gerrit.ListViewMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrRepoDetailList extends Polymer.mixinBehaviors( [ - Gerrit.ListViewBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-detail-list'; } +const PGP_START = '-----BEGIN PGP SIGNATURE-----'; - static get properties() { - return { +/** + * @appliesMixin Gerrit.ListViewMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrRepoDetailList extends mixinBehaviors( [ + Gerrit.ListViewBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-repo-detail-list'; } + + static get properties() { + return { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, /** - * URL params passed from the router. + * The kind of detail we are displaying, possibilities are determined by + * the const DETAIL_TYPES. */ - params: { - type: Object, - observer: '_paramsChanged', - }, - /** - * The kind of detail we are displaying, possibilities are determined by - * the const DETAIL_TYPES. - */ - detailType: String, + detailType: String, - _editing: { - type: Boolean, - value: false, - }, - _isOwner: { - type: Boolean, - value: false, - }, - _loggedIn: { - type: Boolean, - value: false, - }, - /** - * Offset of currently visible query results. - */ - _offset: Number, - _repo: Object, - _items: Array, - /** - * Because we request one more than the projectsPerPage, _shownProjects - * maybe one less than _projects. - */ - _shownItems: { - type: Array, - computed: 'computeShownItems(_items)', - }, - _itemsPerPage: { - type: Number, - value: 25, - }, - _loading: { - type: Boolean, - value: true, - }, - _filter: String, - _refName: String, - _hasNewItemName: Boolean, - _isEditing: Boolean, - _revisedRef: String, - }; - } + _editing: { + type: Boolean, + value: false, + }, + _isOwner: { + type: Boolean, + value: false, + }, + _loggedIn: { + type: Boolean, + value: false, + }, + /** + * Offset of currently visible query results. + */ + _offset: Number, + _repo: Object, + _items: Array, + /** + * Because we request one more than the projectsPerPage, _shownProjects + * maybe one less than _projects. + */ + _shownItems: { + type: Array, + computed: 'computeShownItems(_items)', + }, + _itemsPerPage: { + type: Number, + value: 25, + }, + _loading: { + type: Boolean, + value: true, + }, + _filter: String, + _refName: String, + _hasNewItemName: Boolean, + _isEditing: Boolean, + _revisedRef: String, + }; + } - _determineIfOwner(repo) { - return this.$.restAPI.getRepoAccess(repo) - .then(access => - this._isOwner = access && !!access[repo].is_owner); - } + _determineIfOwner(repo) { + return this.$.restAPI.getRepoAccess(repo) + .then(access => + this._isOwner = access && !!access[repo].is_owner); + } - _paramsChanged(params) { - if (!params || !params.repo) { return; } + _paramsChanged(params) { + if (!params || !params.repo) { return; } - this._repo = params.repo; + this._repo = params.repo; - this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - if (loggedIn) { - this._determineIfOwner(this._repo); - } + this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + if (loggedIn) { + this._determineIfOwner(this._repo); + } + }); + + this.detailType = params.detail; + + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getItems(this._filter, this._repo, + this._itemsPerPage, this._offset, this.detailType); + } + + _getItems(filter, repo, itemsPerPage, offset, detailType) { + this._loading = true; + this._items = []; + flush(); + const errFn = response => { + this.fire('page-error', {response}); + }; + if (detailType === DETAIL_TYPES.BRANCHES) { + return this.$.restAPI.getRepoBranches( + filter, repo, itemsPerPage, offset, errFn).then(items => { + if (!items) { return; } + this._items = items; + this._loading = false; }); - - this.detailType = params.detail; - - this._filter = this.getFilterValue(params); - this._offset = this.getOffsetValue(params); - - return this._getItems(this._filter, this._repo, - this._itemsPerPage, this._offset, this.detailType); - } - - _getItems(filter, repo, itemsPerPage, offset, detailType) { - this._loading = true; - this._items = []; - Polymer.dom.flush(); - const errFn = response => { - this.fire('page-error', {response}); - }; - if (detailType === DETAIL_TYPES.BRANCHES) { - return this.$.restAPI.getRepoBranches( - filter, repo, itemsPerPage, offset, errFn).then(items => { - if (!items) { return; } - this._items = items; - this._loading = false; - }); - } else if (detailType === DETAIL_TYPES.TAGS) { - return this.$.restAPI.getRepoTags( - filter, repo, itemsPerPage, offset, errFn).then(items => { - if (!items) { return; } - this._items = items; - this._loading = false; - }); - } - } - - _getPath(repo) { - return `/admin/repos/${this.encodeURL(repo, false)},` + - `${this.detailType}`; - } - - _computeWeblink(repo) { - if (!repo.web_links) { return ''; } - const webLinks = repo.web_links; - return webLinks.length ? webLinks : null; - } - - _computeMessage(message) { - if (!message) { return; } - // Strip PGP info. - return message.split(PGP_START)[0]; - } - - _stripRefs(item, detailType) { - if (detailType === DETAIL_TYPES.BRANCHES) { - return item.replace('refs/heads/', ''); - } else if (detailType === DETAIL_TYPES.TAGS) { - return item.replace('refs/tags/', ''); - } - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _computeEditingClass(isEditing) { - return isEditing ? 'editing' : ''; - } - - _computeCanEditClass(ref, detailType, isOwner) { - return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ? - 'canEdit' : ''; - } - - _handleEditRevision(e) { - this._revisedRef = e.model.get('item.revision'); - this._isEditing = true; - } - - _handleCancelRevision() { - this._isEditing = false; - } - - _handleSaveRevision(e) { - this._setRepoHead(this._repo, this._revisedRef, e); - } - - _setRepoHead(repo, ref, e) { - return this.$.restAPI.setRepoHead(repo, ref).then(res => { - if (res.status < 400) { - this._isEditing = false; - e.model.set('item.revision', ref); - // This is needed to refresh _items property with fresh data, - // specifically can_delete from the json response. - this._getItems( - this._filter, this._repo, this._itemsPerPage, - this._offset, this.detailType); - } + } else if (detailType === DETAIL_TYPES.TAGS) { + return this.$.restAPI.getRepoTags( + filter, repo, itemsPerPage, offset, errFn).then(items => { + if (!items) { return; } + this._items = items; + this._loading = false; }); } - - _computeItemName(detailType) { - if (detailType === DETAIL_TYPES.BRANCHES) { - return 'Branch'; - } else if (detailType === DETAIL_TYPES.TAGS) { - return 'Tag'; - } - } - - _handleDeleteItemConfirm() { - this.$.overlay.close(); - if (this.detailType === DETAIL_TYPES.BRANCHES) { - return this.$.restAPI.deleteRepoBranches(this._repo, this._refName) - .then(itemDeleted => { - if (itemDeleted.status === 204) { - this._getItems( - this._filter, this._repo, this._itemsPerPage, - this._offset, this.detailType); - } - }); - } else if (this.detailType === DETAIL_TYPES.TAGS) { - return this.$.restAPI.deleteRepoTags(this._repo, this._refName) - .then(itemDeleted => { - if (itemDeleted.status === 204) { - this._getItems( - this._filter, this._repo, this._itemsPerPage, - this._offset, this.detailType); - } - }); - } - } - - _handleConfirmDialogCancel() { - this.$.overlay.close(); - } - - _handleDeleteItem(e) { - const name = this._stripRefs(e.model.get('item.ref'), this.detailType); - if (!name) { return; } - this._refName = name; - this.$.overlay.open(); - } - - _computeHideDeleteClass(owner, canDelete) { - if (canDelete || owner) { - return 'show'; - } - - return ''; - } - - _handleCreateItem() { - this.$.createNewModal.handleCreateItem(); - this._handleCloseCreate(); - } - - _handleCloseCreate() { - this.$.createOverlay.close(); - } - - _handleCreateClicked() { - this.$.createOverlay.open(); - } - - _hideIfBranch(type) { - if (type === DETAIL_TYPES.BRANCHES) { - return 'hideItem'; - } - - return ''; - } - - _computeHideTagger(tagger) { - return tagger ? '' : 'hide'; - } } - customElements.define(GrRepoDetailList.is, GrRepoDetailList); -})(); + _getPath(repo) { + return `/admin/repos/${this.encodeURL(repo, false)},` + + `${this.detailType}`; + } + + _computeWeblink(repo) { + if (!repo.web_links) { return ''; } + const webLinks = repo.web_links; + return webLinks.length ? webLinks : null; + } + + _computeMessage(message) { + if (!message) { return; } + // Strip PGP info. + return message.split(PGP_START)[0]; + } + + _stripRefs(item, detailType) { + if (detailType === DETAIL_TYPES.BRANCHES) { + return item.replace('refs/heads/', ''); + } else if (detailType === DETAIL_TYPES.TAGS) { + return item.replace('refs/tags/', ''); + } + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _computeEditingClass(isEditing) { + return isEditing ? 'editing' : ''; + } + + _computeCanEditClass(ref, detailType, isOwner) { + return isOwner && this._stripRefs(ref, detailType) === 'HEAD' ? + 'canEdit' : ''; + } + + _handleEditRevision(e) { + this._revisedRef = e.model.get('item.revision'); + this._isEditing = true; + } + + _handleCancelRevision() { + this._isEditing = false; + } + + _handleSaveRevision(e) { + this._setRepoHead(this._repo, this._revisedRef, e); + } + + _setRepoHead(repo, ref, e) { + return this.$.restAPI.setRepoHead(repo, ref).then(res => { + if (res.status < 400) { + this._isEditing = false; + e.model.set('item.revision', ref); + // This is needed to refresh _items property with fresh data, + // specifically can_delete from the json response. + this._getItems( + this._filter, this._repo, this._itemsPerPage, + this._offset, this.detailType); + } + }); + } + + _computeItemName(detailType) { + if (detailType === DETAIL_TYPES.BRANCHES) { + return 'Branch'; + } else if (detailType === DETAIL_TYPES.TAGS) { + return 'Tag'; + } + } + + _handleDeleteItemConfirm() { + this.$.overlay.close(); + if (this.detailType === DETAIL_TYPES.BRANCHES) { + return this.$.restAPI.deleteRepoBranches(this._repo, this._refName) + .then(itemDeleted => { + if (itemDeleted.status === 204) { + this._getItems( + this._filter, this._repo, this._itemsPerPage, + this._offset, this.detailType); + } + }); + } else if (this.detailType === DETAIL_TYPES.TAGS) { + return this.$.restAPI.deleteRepoTags(this._repo, this._refName) + .then(itemDeleted => { + if (itemDeleted.status === 204) { + this._getItems( + this._filter, this._repo, this._itemsPerPage, + this._offset, this.detailType); + } + }); + } + } + + _handleConfirmDialogCancel() { + this.$.overlay.close(); + } + + _handleDeleteItem(e) { + const name = this._stripRefs(e.model.get('item.ref'), this.detailType); + if (!name) { return; } + this._refName = name; + this.$.overlay.open(); + } + + _computeHideDeleteClass(owner, canDelete) { + if (canDelete || owner) { + return 'show'; + } + + return ''; + } + + _handleCreateItem() { + this.$.createNewModal.handleCreateItem(); + this._handleCloseCreate(); + } + + _handleCloseCreate() { + this.$.createOverlay.close(); + } + + _handleCreateClicked() { + this.$.createOverlay.open(); + } + + _hideIfBranch(type) { + if (type === DETAIL_TYPES.BRANCHES) { + return 'hideItem'; + } + + return ''; + } + + _computeHideTagger(tagger) { + return tagger ? '' : 'hide'; + } +} + +customElements.define(GrRepoDetailList.is, GrRepoDetailList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js index 467cef0..0d232f2 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_html.js
@@ -1,40 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-create-pointer-dialog/gr-create-pointer-dialog.html"> -<link rel="import" href="../gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html"> - -<dom-module id="gr-repo-detail-list"> - <template> +export const htmlTemplate = html` <style include="gr-form-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -88,100 +70,68 @@ <style include="gr-table-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <gr-list-view - create-new="[[_loggedIn]]" - filter="[[_filter]]" - items-per-page="[[_itemsPerPage]]" - items="[[_items]]" - loading="[[_loading]]" - offset="[[_offset]]" - on-create-clicked="_handleCreateClicked" - path="[[_getPath(_repo, detailType)]]"> + <gr-list-view create-new="[[_loggedIn]]" filter="[[_filter]]" items-per-page="[[_itemsPerPage]]" items="[[_items]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_getPath(_repo, detailType)]]"> <table id="list" class="genericList gr-form-styles"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="name topHeader">Name</th> <th class="revision topHeader">Revision</th> - <th class$="message topHeader [[_hideIfBranch(detailType)]]"> + <th class\$="message topHeader [[_hideIfBranch(detailType)]]"> Message</th> - <th class$="tagger topHeader [[_hideIfBranch(detailType)]]"> + <th class\$="tagger topHeader [[_hideIfBranch(detailType)]]"> Tagger</th> <th class="repositoryBrowser topHeader"> Repository Browser</th> <th class="delete topHeader"></th> </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]"> <td>Loading...</td> </tr> - <tbody class$="[[computeLoadingClass(_loading)]]"> + </tbody><tbody class\$="[[computeLoadingClass(_loading)]]"> <template is="dom-repeat" items="[[_shownItems]]"> <tr class="table"> - <td class$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td> - <td class$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]"> + <td class\$="[[detailType]] name">[[_stripRefs(item.ref, detailType)]]</td> + <td class\$="[[detailType]] revision [[_computeCanEditClass(item.ref, detailType, _isOwner)]]"> <span class="revisionNoEditing"> [[item.revision]] </span> - <span class$="revisionEdit [[_computeEditingClass(_isEditing)]]"> + <span class\$="revisionEdit [[_computeEditingClass(_isEditing)]]"> <span class="revisionWithEditing"> [[item.revision]] </span> - <gr-button - link - on-click="_handleEditRevision" - class="editBtn"> + <gr-button link="" on-click="_handleEditRevision" class="editBtn"> edit </gr-button> - <iron-input - bind-value="{{_revisedRef}}" - class="editItem"> - <input - is="iron-input" - bind-value="{{_revisedRef}}"> + <iron-input bind-value="{{_revisedRef}}" class="editItem"> + <input is="iron-input" bind-value="{{_revisedRef}}"> </iron-input> - <gr-button - link - on-click="_handleCancelRevision" - class="cancelBtn editItem"> + <gr-button link="" on-click="_handleCancelRevision" class="cancelBtn editItem"> Cancel </gr-button> - <gr-button - link - on-click="_handleSaveRevision" - class="saveBtn editItem" - disabled="[[!_revisedRef]]"> + <gr-button link="" on-click="_handleSaveRevision" class="saveBtn editItem" disabled="[[!_revisedRef]]"> Save </gr-button> </span> </td> - <td class$="message [[_hideIfBranch(detailType)]]"> + <td class\$="message [[_hideIfBranch(detailType)]]"> [[_computeMessage(item.message)]] </td> - <td class$="tagger [[_hideIfBranch(detailType)]]"> - <div class$="tagger [[_computeHideTagger(item.tagger)]]"> - <gr-account-link - account="[[item.tagger]]"> + <td class\$="tagger [[_hideIfBranch(detailType)]]"> + <div class\$="tagger [[_computeHideTagger(item.tagger)]]"> + <gr-account-link account="[[item.tagger]]"> </gr-account-link> - (<gr-date-formatter - has-tooltip - date-str="[[item.tagger.date]]"> + (<gr-date-formatter has-tooltip="" date-str="[[item.tagger.date]]"> </gr-date-formatter>) </div> </td> <td class="repositoryBrowser"> - <template is="dom-repeat" - items="[[_computeWeblink(item)]]" as="link"> - <a href$="[[link.url]]" - class="webLink" - rel="noopener" - target="_blank"> + <template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link"> + <a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank"> ([[link.name]]) </a> </template> </td> <td class="delete"> - <gr-button - link - class$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]" - on-click="_handleDeleteItem"> + <gr-button link="" class\$="deleteButton [[_computeHideDeleteClass(_isOwner, item.can_delete)]]" on-click="_handleDeleteItem"> Delete </gr-button> </td> @@ -189,36 +139,19 @@ </template> </tbody> </table> - <gr-overlay id="overlay" with-backdrop> - <gr-confirm-delete-item-dialog - class="confirmDialog" - on-confirm="_handleDeleteItemConfirm" - on-cancel="_handleConfirmDialogCancel" - item="[[_refName]]" - item-type="[[detailType]]"></gr-confirm-delete-item-dialog> + <gr-overlay id="overlay" with-backdrop=""> + <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteItemConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_refName]]" item-type="[[detailType]]"></gr-confirm-delete-item-dialog> </gr-overlay> </gr-list-view> - <gr-overlay id="createOverlay" with-backdrop> - <gr-dialog - id="createDialog" - disabled="[[!_hasNewItemName]]" - confirm-label="Create" - on-confirm="_handleCreateItem" - on-cancel="_handleCloseCreate"> + <gr-overlay id="createOverlay" with-backdrop=""> + <gr-dialog id="createDialog" disabled="[[!_hasNewItemName]]" confirm-label="Create" on-confirm="_handleCreateItem" on-cancel="_handleCloseCreate"> <div class="header" slot="header"> Create [[_computeItemName(detailType)]] </div> <div class="main" slot="main"> - <gr-create-pointer-dialog - id="createNewModal" - detail-type="[[_computeItemName(detailType)]]" - has-new-item-name="{{_hasNewItemName}}" - item-detail="[[detailType]]" - repo-name="[[_repo]]"></gr-create-pointer-dialog> + <gr-create-pointer-dialog id="createNewModal" detail-type="[[_computeItemName(detailType)]]" has-new-item-name="{{_hasNewItemName}}" item-detail="[[detailType]]" repo-name="[[_repo]]"></gr-create-pointer-dialog> </div> </gr-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-detail-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html index d466f28..13510d8 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-detail-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-detail-list.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-detail-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-detail-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,533 +41,536 @@ </template> </test-fixture> -<script> - let counter; - const branchGenerator = () => { - return { - ref: `refs/heads/test${++counter}`, - revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d', - web_links: [ - { - name: 'diffusion', - url: `https://git.example.org/branch/test;refs/heads/test${counter}`, - }, - ], - }; - }; - const tagGenerator = () => { - return { - ref: `refs/tags/test${++counter}`, - revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d', - web_links: [ - { - name: 'diffusion', - url: `https://git.example.org/tag/test;refs/tags/test${counter}`, - }, - ], - message: 'Annotated tag', - tagger: { - name: 'Test User', - email: 'test.user@gmail.com', - date: '2017-09-19 14:54:00.000000000', - tz: 540, +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-detail-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +let counter; +const branchGenerator = () => { + return { + ref: `refs/heads/test${++counter}`, + revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d', + web_links: [ + { + name: 'diffusion', + url: `https://git.example.org/branch/test;refs/heads/test${counter}`, }, - }; + ], }; +}; +const tagGenerator = () => { + return { + ref: `refs/tags/test${++counter}`, + revision: '9c9d08a438e55e52f33b608415e6dddd9b18550d', + web_links: [ + { + name: 'diffusion', + url: `https://git.example.org/tag/test;refs/tags/test${counter}`, + }, + ], + message: 'Annotated tag', + tagger: { + name: 'Test User', + email: 'test.user@gmail.com', + date: '2017-09-19 14:54:00.000000000', + tz: 540, + }, + }; +}; - suite('gr-repo-detail-list', async () => { - await readyToTest(); - suite('Branches', () => { - let element; - let branches; - let sandbox; +suite('gr-repo-detail-list', () => { + suite('Branches', () => { + let element; + let branches; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.detailType = 'branches'; - counter = 0; - sandbox.stub(page, 'show'); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.detailType = 'branches'; + counter = 0; + sandbox.stub(page, 'show'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list of repo branches', () => { + setup(done => { + branches = [{ + ref: 'HEAD', + revision: 'master', + }].concat(_.times(25, branchGenerator)); + + stub('gr-rest-api-interface', { + getRepoBranches(num, project, offset) { + return Promise.resolve(branches); + }, + }); + + const params = { + repo: 'test', + detail: 'branches', + }; + + element._paramsChanged(params).then(() => { flush(done); }); }); - teardown(() => { - sandbox.restore(); - }); - - suite('list of repo branches', () => { - setup(done => { - branches = [{ - ref: 'HEAD', - revision: 'master', - }].concat(_.times(25, branchGenerator)); - - stub('gr-rest-api-interface', { - getRepoBranches(num, project, offset) { - return Promise.resolve(branches); - }, - }); - - const params = { - repo: 'test', - detail: 'branches', - }; - - element._paramsChanged(params).then(() => { flush(done); }); - }); - - test('test for branch in the list', done => { - flush(() => { - assert.equal(element._items[2].ref, 'refs/heads/test2'); - done(); - }); - }); - - test('test for web links in the branches list', done => { - flush(() => { - assert.equal(element._items[2].web_links[0].url, - 'https://git.example.org/branch/test;refs/heads/test2'); - done(); - }); - }); - - test('test for refs/heads/ being striped from ref', done => { - flush(() => { - assert.equal(element._stripRefs(element._items[2].ref, - element.detailType), 'test2'); - done(); - }); - }); - - test('_shownItems', () => { - assert.equal(element._shownItems.length, 25); - }); - - test('Edit HEAD button not admin', done => { - sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); - sandbox.stub(element.$.restAPI, 'getRepoAccess').returns( - Promise.resolve({ - test: {is_owner: false}, - })); - element._determineIfOwner('test').then(() => { - assert.equal(element._isOwner, false); - assert.equal(getComputedStyle(Polymer.dom(element.root) - .querySelector('.revisionNoEditing')).display, 'inline'); - assert.equal(getComputedStyle(Polymer.dom(element.root) - .querySelector('.revisionEdit')).display, 'none'); - done(); - }); - }); - - test('Edit HEAD button admin', done => { - const saveBtn = Polymer.dom(element.root).querySelector('.saveBtn'); - const cancelBtn = Polymer.dom(element.root).querySelector('.cancelBtn'); - const editBtn = Polymer.dom(element.root).querySelector('.editBtn'); - const revisionNoEditing = Polymer.dom(element.root) - .querySelector('.revisionNoEditing'); - const revisionWithEditing = Polymer.dom(element.root) - .querySelector('.revisionWithEditing'); - - sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); - sandbox.stub(element.$.restAPI, 'getRepoAccess').returns( - Promise.resolve({ - test: {is_owner: true}, - })); - sandbox.stub(element, '_handleSaveRevision'); - element._determineIfOwner('test').then(() => { - assert.equal(element._isOwner, true); - // The revision container for non-editing enabled row is not visible. - assert.equal(getComputedStyle(revisionNoEditing).display, 'none'); - - // The revision container for editing enabled row is visible. - assert.notEqual(getComputedStyle(Polymer.dom(element.root) - .querySelector('.revisionEdit')).display, 'none'); - - // The revision and edit button are visible. - assert.notEqual(getComputedStyle(revisionWithEditing).display, - 'none'); - assert.notEqual(getComputedStyle(editBtn).display, 'none'); - - // The input, cancel, and save buttons are not visible. - const hiddenElements = Polymer.dom(element.root) - .querySelectorAll('.canEdit .editItem'); - - for (const item of hiddenElements) { - assert.equal(getComputedStyle(item).display, 'none'); - } - - MockInteractions.tap(editBtn); - flushAsynchronousOperations(); - // The revision and edit button are not visible. - assert.equal(getComputedStyle(revisionWithEditing).display, 'none'); - assert.equal(getComputedStyle(editBtn).display, 'none'); - - // The input, cancel, and save buttons are not visible. - for (const item of hiddenElements) { - assert.notEqual(getComputedStyle(item).display, 'none'); - } - - // The revised ref was set correctly - assert.equal(element._revisedRef, 'master'); - - assert.isFalse(saveBtn.disabled); - - // Delete the ref. - element._revisedRef = ''; - assert.isTrue(saveBtn.disabled); - - // Change the ref to something else - element._revisedRef = 'newRef'; - element._repo = 'test'; - assert.isFalse(saveBtn.disabled); - - // Save button calls handleSave. since this is stubbed, the edit - // section remains open. - MockInteractions.tap(saveBtn); - assert.isTrue(element._handleSaveRevision.called); - - // When cancel is tapped, the edit secion closes. - MockInteractions.tap(cancelBtn); - flushAsynchronousOperations(); - - // The revision and edit button are visible. - assert.notEqual(getComputedStyle(revisionWithEditing).display, - 'none'); - assert.notEqual(getComputedStyle(editBtn).display, 'none'); - - // The input, cancel, and save buttons are not visible. - for (const item of hiddenElements) { - assert.equal(getComputedStyle(item).display, 'none'); - } - done(); - }); - }); - - test('_handleSaveRevision with invalid rev', done => { - const event = {model: {set: sandbox.stub()}}; - element._isEditing = true; - sandbox.stub(element.$.restAPI, 'setRepoHead').returns( - Promise.resolve({ - status: 400, - }) - ); - - element._setRepoHead('test', 'newRef', event).then(() => { - assert.isTrue(element._isEditing); - assert.isFalse(event.model.set.called); - done(); - }); - }); - - test('_handleSaveRevision with valid rev', done => { - const event = {model: {set: sandbox.stub()}}; - element._isEditing = true; - sandbox.stub(element.$.restAPI, 'setRepoHead').returns( - Promise.resolve({ - status: 200, - }) - ); - - element._setRepoHead('test', 'newRef', event).then(() => { - assert.isFalse(element._isEditing); - assert.isTrue(event.model.set.called); - done(); - }); - }); - - test('test _computeItemName', () => { - assert.deepEqual(element._computeItemName('branches'), 'Branch'); - assert.deepEqual(element._computeItemName('tags'), 'Tag'); + test('test for branch in the list', done => { + flush(() => { + assert.equal(element._items[2].ref, 'refs/heads/test2'); + done(); }); }); - suite('list with less then 25 branches', () => { - setup(done => { - branches = _.times(25, branchGenerator); - - stub('gr-rest-api-interface', { - getRepoBranches(num, repo, offset) { - return Promise.resolve(branches); - }, - }); - - const params = { - repo: 'test', - detail: 'branches', - }; - - element._paramsChanged(params).then(() => { flush(done); }); - }); - - test('_shownItems', () => { - assert.equal(element._shownItems.length, 25); + test('test for web links in the branches list', done => { + flush(() => { + assert.equal(element._items[2].web_links[0].url, + 'https://git.example.org/branch/test;refs/heads/test2'); + done(); }); }); - suite('filter', () => { - test('_paramsChanged', done => { - sandbox.stub( - element.$.restAPI, - 'getRepoBranches', - () => Promise.resolve(branches)); - const params = { - detail: 'branches', - repo: 'test', - filter: 'test', - offset: 25, - }; - element._paramsChanged(params).then(() => { - assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[0], - 'test'); - assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[1], - 'test'); - assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[2], - 25); - assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[3], - 25); - done(); - }); + test('test for refs/heads/ being striped from ref', done => { + flush(() => { + assert.equal(element._stripRefs(element._items[2].ref, + element.detailType), 'test2'); + done(); }); }); - suite('404', () => { - test('fires page-error', done => { - const response = {status: 404}; - sandbox.stub(element.$.restAPI, 'getRepoBranches', - (filter, repo, reposBranchesPerPage, opt_offset, errFn) => { - errFn(response); - }); + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); + test('Edit HEAD button not admin', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); + sandbox.stub(element.$.restAPI, 'getRepoAccess').returns( + Promise.resolve({ + test: {is_owner: false}, + })); + element._determineIfOwner('test').then(() => { + assert.equal(element._isOwner, false); + assert.equal(getComputedStyle(dom(element.root) + .querySelector('.revisionNoEditing')).display, 'inline'); + assert.equal(getComputedStyle(dom(element.root) + .querySelector('.revisionEdit')).display, 'none'); + done(); + }); + }); - const params = { - detail: 'branches', - repo: 'test', - filter: 'test', - offset: 25, - }; - element._paramsChanged(params); + test('Edit HEAD button admin', done => { + const saveBtn = dom(element.root).querySelector('.saveBtn'); + const cancelBtn = dom(element.root).querySelector('.cancelBtn'); + const editBtn = dom(element.root).querySelector('.editBtn'); + const revisionNoEditing = dom(element.root) + .querySelector('.revisionNoEditing'); + const revisionWithEditing = dom(element.root) + .querySelector('.revisionWithEditing'); + + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); + sandbox.stub(element.$.restAPI, 'getRepoAccess').returns( + Promise.resolve({ + test: {is_owner: true}, + })); + sandbox.stub(element, '_handleSaveRevision'); + element._determineIfOwner('test').then(() => { + assert.equal(element._isOwner, true); + // The revision container for non-editing enabled row is not visible. + assert.equal(getComputedStyle(revisionNoEditing).display, 'none'); + + // The revision container for editing enabled row is visible. + assert.notEqual(getComputedStyle(dom(element.root) + .querySelector('.revisionEdit')).display, 'none'); + + // The revision and edit button are visible. + assert.notEqual(getComputedStyle(revisionWithEditing).display, + 'none'); + assert.notEqual(getComputedStyle(editBtn).display, 'none'); + + // The input, cancel, and save buttons are not visible. + const hiddenElements = dom(element.root) + .querySelectorAll('.canEdit .editItem'); + + for (const item of hiddenElements) { + assert.equal(getComputedStyle(item).display, 'none'); + } + + MockInteractions.tap(editBtn); + flushAsynchronousOperations(); + // The revision and edit button are not visible. + assert.equal(getComputedStyle(revisionWithEditing).display, 'none'); + assert.equal(getComputedStyle(editBtn).display, 'none'); + + // The input, cancel, and save buttons are not visible. + for (const item of hiddenElements) { + assert.notEqual(getComputedStyle(item).display, 'none'); + } + + // The revised ref was set correctly + assert.equal(element._revisedRef, 'master'); + + assert.isFalse(saveBtn.disabled); + + // Delete the ref. + element._revisedRef = ''; + assert.isTrue(saveBtn.disabled); + + // Change the ref to something else + element._revisedRef = 'newRef'; + element._repo = 'test'; + assert.isFalse(saveBtn.disabled); + + // Save button calls handleSave. since this is stubbed, the edit + // section remains open. + MockInteractions.tap(saveBtn); + assert.isTrue(element._handleSaveRevision.called); + + // When cancel is tapped, the edit secion closes. + MockInteractions.tap(cancelBtn); + flushAsynchronousOperations(); + + // The revision and edit button are visible. + assert.notEqual(getComputedStyle(revisionWithEditing).display, + 'none'); + assert.notEqual(getComputedStyle(editBtn).display, 'none'); + + // The input, cancel, and save buttons are not visible. + for (const item of hiddenElements) { + assert.equal(getComputedStyle(item).display, 'none'); + } + done(); + }); + }); + + test('_handleSaveRevision with invalid rev', done => { + const event = {model: {set: sandbox.stub()}}; + element._isEditing = true; + sandbox.stub(element.$.restAPI, 'setRepoHead').returns( + Promise.resolve({ + status: 400, + }) + ); + + element._setRepoHead('test', 'newRef', event).then(() => { + assert.isTrue(element._isEditing); + assert.isFalse(event.model.set.called); + done(); + }); + }); + + test('_handleSaveRevision with valid rev', done => { + const event = {model: {set: sandbox.stub()}}; + element._isEditing = true; + sandbox.stub(element.$.restAPI, 'setRepoHead').returns( + Promise.resolve({ + status: 200, + }) + ); + + element._setRepoHead('test', 'newRef', event).then(() => { + assert.isFalse(element._isEditing); + assert.isTrue(event.model.set.called); + done(); + }); + }); + + test('test _computeItemName', () => { + assert.deepEqual(element._computeItemName('branches'), 'Branch'); + assert.deepEqual(element._computeItemName('tags'), 'Tag'); + }); + }); + + suite('list with less then 25 branches', () => { + setup(done => { + branches = _.times(25, branchGenerator); + + stub('gr-rest-api-interface', { + getRepoBranches(num, repo, offset) { + return Promise.resolve(branches); + }, + }); + + const params = { + repo: 'test', + detail: 'branches', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); + }); + + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub( + element.$.restAPI, + 'getRepoBranches', + () => Promise.resolve(branches)); + const params = { + detail: 'branches', + repo: 'test', + filter: 'test', + offset: 25, + }; + element._paramsChanged(params).then(() => { + assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[0], + 'test'); + assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[1], + 'test'); + assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[2], + 25); + assert.equal(element.$.restAPI.getRepoBranches.lastCall.args[3], + 25); + done(); }); }); }); - suite('Tags', () => { - let element; - let tags; - let sandbox; + suite('404', () => { + test('fires page-error', done => { + const response = {status: 404}; + sandbox.stub(element.$.restAPI, 'getRepoBranches', + (filter, repo, reposBranchesPerPage, opt_offset, errFn) => { + errFn(response); + }); - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.detailType = 'tags'; - counter = 0; - sandbox.stub(page, 'show'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('_computeMessage', () => { - let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' + - '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' + - 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' + - 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' + - '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' + - 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' + - 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' + - 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' + - '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' + - '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' + - 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' + - 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' + - '--'; - assert.equal(element._computeMessage(message), 'v2.15-rc1↵'); - message = 'v2.15-rc1'; - assert.equal(element._computeMessage(message), 'v2.15-rc1'); - }); - - suite('list of repo tags', () => { - setup(done => { - tags = _.times(26, tagGenerator); - - stub('gr-rest-api-interface', { - getRepoTags(num, repo, offset) { - return Promise.resolve(tags); - }, - }); - - const params = { - repo: 'test', - detail: 'tags', - }; - - element._paramsChanged(params).then(() => { flush(done); }); + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); }); - test('test for tag in the list', done => { - flush(() => { - assert.equal(element._items[1].ref, 'refs/tags/test2'); - done(); - }); - }); - - test('test for tag message in the list', done => { - flush(() => { - assert.equal(element._items[1].message, 'Annotated tag'); - done(); - }); - }); - - test('test for tagger in the tag list', done => { - const tagger = { - name: 'Test User', - email: 'test.user@gmail.com', - date: '2017-09-19 14:54:00.000000000', - tz: 540, - }; - flush(() => { - assert.deepEqual(element._items[1].tagger, tagger); - done(); - }); - }); - - test('test for web links in the tags list', done => { - flush(() => { - assert.equal(element._items[1].web_links[0].url, - 'https://git.example.org/tag/test;refs/tags/test2'); - done(); - }); - }); - - test('test for refs/tags/ being striped from ref', done => { - flush(() => { - assert.equal(element._stripRefs(element._items[1].ref, - element.detailType), 'test2'); - done(); - }); - }); - - test('_shownItems', () => { - assert.equal(element._shownItems.length, 25); - }); - - test('_computeHideTagger', () => { - const testObject1 = { - tagger: 'test', - }; - assert.equal(element._computeHideTagger(testObject1), ''); - - assert.equal(element._computeHideTagger(undefined), 'hide'); - }); - }); - - suite('list with less then 25 tags', () => { - setup(done => { - tags = _.times(25, tagGenerator); - - stub('gr-rest-api-interface', { - getRepoTags(num, project, offset) { - return Promise.resolve(tags); - }, - }); - - const params = { - repo: 'test', - detail: 'tags', - }; - - element._paramsChanged(params).then(() => { flush(done); }); - }); - - test('_shownItems', () => { - assert.equal(element._shownItems.length, 25); - }); - }); - - suite('filter', () => { - test('_paramsChanged', done => { - sandbox.stub( - element.$.restAPI, - 'getRepoTags', - () => Promise.resolve(tags)); - const params = { - repo: 'test', - detail: 'tags', - filter: 'test', - offset: 25, - }; - element._paramsChanged(params).then(() => { - assert.equal(element.$.restAPI.getRepoTags.lastCall.args[0], - 'test'); - assert.equal(element.$.restAPI.getRepoTags.lastCall.args[1], - 'test'); - assert.equal(element.$.restAPI.getRepoTags.lastCall.args[2], - 25); - assert.equal(element.$.restAPI.getRepoTags.lastCall.args[3], - 25); - done(); - }); - }); - }); - - suite('create new', () => { - test('_handleCreateClicked called when create-click fired', () => { - sandbox.stub(element, '_handleCreateClicked'); - element.shadowRoot - .querySelector('gr-list-view').fire('create-clicked'); - assert.isTrue(element._handleCreateClicked.called); - }); - - test('_handleCreateClicked opens modal', () => { - const openStub = sandbox.stub(element.$.createOverlay, 'open'); - element._handleCreateClicked(); - assert.isTrue(openStub.called); - }); - - test('_handleCreateItem called when confirm fired', () => { - sandbox.stub(element, '_handleCreateItem'); - element.$.createDialog.fire('confirm'); - assert.isTrue(element._handleCreateItem.called); - }); - - test('_handleCloseCreate called when cancel fired', () => { - sandbox.stub(element, '_handleCloseCreate'); - element.$.createDialog.fire('cancel'); - assert.isTrue(element._handleCloseCreate.called); - }); - }); - - suite('404', () => { - test('fires page-error', done => { - const response = {status: 404}; - sandbox.stub(element.$.restAPI, 'getRepoTags', - (filter, repo, reposTagsPerPage, opt_offset, errFn) => { - errFn(response); - }); - - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - const params = { - repo: 'test', - detail: 'tags', - filter: 'test', - offset: 25, - }; - element._paramsChanged(params); - }); - }); - - test('test _computeHideDeleteClass', () => { - assert.deepEqual(element._computeHideDeleteClass(true, false), 'show'); - assert.deepEqual(element._computeHideDeleteClass(false, true), 'show'); - assert.deepEqual(element._computeHideDeleteClass(false, false), ''); + const params = { + detail: 'branches', + repo: 'test', + filter: 'test', + offset: 25, + }; + element._paramsChanged(params); }); }); }); + + suite('Tags', () => { + let element; + let tags; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.detailType = 'tags'; + counter = 0; + sandbox.stub(page, 'show'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeMessage', () => { + let message = 'v2.15-rc1↵-----BEGIN PGP SIGNATURE-----↵Version: GnuPG v' + + '1↵↵iQIcBAABAgAGBQJZ27O7AAoJEF/XxZqaEoiMy6kQAMoQCpGr3J6JITI4BVWsr7QM↵xy' + + 'EcWH5YPUko5EPTbkABHmaVyFmKGkuIQdn6c+NIbqJOk+5XT4oUyRSo1T569HPJ↵3kyxEJi' + + 'T1ryvp5BIHwdvHx58fjw1+YkiWLZuZq1FFkUYqnWTYCrkv7Fok98pdOmV↵CL1Hgugi5uK8' + + '/kxf1M7+Nv6piaZ140pwSb1h6QdAjaZVfaBCnoxlG4LRUqHvEYay↵f4QYgFT67auHIGkZ4' + + 'moUcsp2Du/1jSsCWL/CPwjPFGbbckVAjLCMT9yD3NKwpEZF↵pfsiZyHI9dL0M+QjVrM+RD' + + 'HwIIJwra8R0IMkDlQ6MDrFlKNqNBbo588S6UPrm71L↵YuiwWlcrK9ZIybxT6LzbR65Rvez' + + 'DSitQ+xeIfpZE19/X6BCnvlARLE8k/tC2JksI↵lEZi7Lf3FQdIcwwyt98tJkS9HX9v9jbC' + + '5QXifnoj3Li8tHSLuQ1dJCxHQiis6ojI↵OWUFkm0IHBXVNHA2dqYBdM+pL12mlI3wp6Ica' + + '4cdEVDwzu+j1xnVSFUa+d+Y2xJF↵7mytuyhHiKG4hm+zbhMv6WD8Q3FoDsJZeLY99l0hYQ' + + 'SnnkMduFVroIs45pAs8gUA↵RvYla8mm9w/543IJAPzzFarPVLSsSyQ7tJl3UBzjKRNH/rX' + + 'W+F22qyWD1zyHPUIR↵C00ItmwlAvveImYKpQAH↵=L+K9↵-----END PGP SIGNATURE---' + + '--'; + assert.equal(element._computeMessage(message), 'v2.15-rc1↵'); + message = 'v2.15-rc1'; + assert.equal(element._computeMessage(message), 'v2.15-rc1'); + }); + + suite('list of repo tags', () => { + setup(done => { + tags = _.times(26, tagGenerator); + + stub('gr-rest-api-interface', { + getRepoTags(num, repo, offset) { + return Promise.resolve(tags); + }, + }); + + const params = { + repo: 'test', + detail: 'tags', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('test for tag in the list', done => { + flush(() => { + assert.equal(element._items[1].ref, 'refs/tags/test2'); + done(); + }); + }); + + test('test for tag message in the list', done => { + flush(() => { + assert.equal(element._items[1].message, 'Annotated tag'); + done(); + }); + }); + + test('test for tagger in the tag list', done => { + const tagger = { + name: 'Test User', + email: 'test.user@gmail.com', + date: '2017-09-19 14:54:00.000000000', + tz: 540, + }; + flush(() => { + assert.deepEqual(element._items[1].tagger, tagger); + done(); + }); + }); + + test('test for web links in the tags list', done => { + flush(() => { + assert.equal(element._items[1].web_links[0].url, + 'https://git.example.org/tag/test;refs/tags/test2'); + done(); + }); + }); + + test('test for refs/tags/ being striped from ref', done => { + flush(() => { + assert.equal(element._stripRefs(element._items[1].ref, + element.detailType), 'test2'); + done(); + }); + }); + + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); + + test('_computeHideTagger', () => { + const testObject1 = { + tagger: 'test', + }; + assert.equal(element._computeHideTagger(testObject1), ''); + + assert.equal(element._computeHideTagger(undefined), 'hide'); + }); + }); + + suite('list with less then 25 tags', () => { + setup(done => { + tags = _.times(25, tagGenerator); + + stub('gr-rest-api-interface', { + getRepoTags(num, project, offset) { + return Promise.resolve(tags); + }, + }); + + const params = { + repo: 'test', + detail: 'tags', + }; + + element._paramsChanged(params).then(() => { flush(done); }); + }); + + test('_shownItems', () => { + assert.equal(element._shownItems.length, 25); + }); + }); + + suite('filter', () => { + test('_paramsChanged', done => { + sandbox.stub( + element.$.restAPI, + 'getRepoTags', + () => Promise.resolve(tags)); + const params = { + repo: 'test', + detail: 'tags', + filter: 'test', + offset: 25, + }; + element._paramsChanged(params).then(() => { + assert.equal(element.$.restAPI.getRepoTags.lastCall.args[0], + 'test'); + assert.equal(element.$.restAPI.getRepoTags.lastCall.args[1], + 'test'); + assert.equal(element.$.restAPI.getRepoTags.lastCall.args[2], + 25); + assert.equal(element.$.restAPI.getRepoTags.lastCall.args[3], + 25); + done(); + }); + }); + }); + + suite('create new', () => { + test('_handleCreateClicked called when create-click fired', () => { + sandbox.stub(element, '_handleCreateClicked'); + element.shadowRoot + .querySelector('gr-list-view').fire('create-clicked'); + assert.isTrue(element._handleCreateClicked.called); + }); + + test('_handleCreateClicked opens modal', () => { + const openStub = sandbox.stub(element.$.createOverlay, 'open'); + element._handleCreateClicked(); + assert.isTrue(openStub.called); + }); + + test('_handleCreateItem called when confirm fired', () => { + sandbox.stub(element, '_handleCreateItem'); + element.$.createDialog.fire('confirm'); + assert.isTrue(element._handleCreateItem.called); + }); + + test('_handleCloseCreate called when cancel fired', () => { + sandbox.stub(element, '_handleCloseCreate'); + element.$.createDialog.fire('cancel'); + assert.isTrue(element._handleCloseCreate.called); + }); + }); + + suite('404', () => { + test('fires page-error', done => { + const response = {status: 404}; + sandbox.stub(element.$.restAPI, 'getRepoTags', + (filter, repo, reposTagsPerPage, opt_offset, errFn) => { + errFn(response); + }); + + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); + }); + + const params = { + repo: 'test', + detail: 'tags', + filter: 'test', + offset: 25, + }; + element._paramsChanged(params); + }); + }); + + test('test _computeHideDeleteClass', () => { + assert.deepEqual(element._computeHideDeleteClass(true, false), 'show'); + assert.deepEqual(element._computeHideDeleteClass(false, true), 'show'); + assert.deepEqual(element._computeHideDeleteClass(false, false), ''); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js index c509717..24cc9a8 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list.js
@@ -14,160 +14,174 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-list-view/gr-list-view.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-create-repo-dialog/gr-create-repo-dialog.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-list_html.js'; + +/** + * @appliesMixin Gerrit.ListViewMixin + * @extends Polymer.Element + */ +class GrRepoList extends mixinBehaviors( [ + Gerrit.ListViewBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-repo-list'; } + + static get properties() { + return { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, + + /** + * Offset of currently visible query results. + */ + _offset: Number, + _path: { + type: String, + readOnly: true, + value: '/admin/repos', + }, + _hasNewRepoName: Boolean, + _createNewCapability: { + type: Boolean, + value: false, + }, + _repos: Array, + + /** + * Because we request one more than the projectsPerPage, _shownProjects + * maybe one less than _projects. + * */ + _shownRepos: { + type: Array, + computed: 'computeShownItems(_repos)', + }, + + _reposPerPage: { + type: Number, + value: 25, + }, + + _loading: { + type: Boolean, + value: true, + }, + _filter: { + type: String, + value: '', + }, + }; + } + + /** @override */ + attached() { + super.attached(); + this._getCreateRepoCapability(); + this.fire('title-change', {title: 'Repos'}); + this._maybeOpenCreateOverlay(this.params); + } + + _paramsChanged(params) { + this._loading = true; + this._filter = this.getFilterValue(params); + this._offset = this.getOffsetValue(params); + + return this._getRepos(this._filter, this._reposPerPage, + this._offset); + } /** - * @appliesMixin Gerrit.ListViewMixin - * @extends Polymer.Element + * Opens the create overlay if the route has a hash 'create' + * + * @param {!Object} params */ - class GrRepoList extends Polymer.mixinBehaviors( [ - Gerrit.ListViewBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-list'; } - - static get properties() { - return { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - - /** - * Offset of currently visible query results. - */ - _offset: Number, - _path: { - type: String, - readOnly: true, - value: '/admin/repos', - }, - _hasNewRepoName: Boolean, - _createNewCapability: { - type: Boolean, - value: false, - }, - _repos: Array, - - /** - * Because we request one more than the projectsPerPage, _shownProjects - * maybe one less than _projects. - * */ - _shownRepos: { - type: Array, - computed: 'computeShownItems(_repos)', - }, - - _reposPerPage: { - type: Number, - value: 25, - }, - - _loading: { - type: Boolean, - value: true, - }, - _filter: { - type: String, - value: '', - }, - }; - } - - /** @override */ - attached() { - super.attached(); - this._getCreateRepoCapability(); - this.fire('title-change', {title: 'Repos'}); - this._maybeOpenCreateOverlay(this.params); - } - - _paramsChanged(params) { - this._loading = true; - this._filter = this.getFilterValue(params); - this._offset = this.getOffsetValue(params); - - return this._getRepos(this._filter, this._reposPerPage, - this._offset); - } - - /** - * Opens the create overlay if the route has a hash 'create' - * - * @param {!Object} params - */ - _maybeOpenCreateOverlay(params) { - if (params && params.openCreateModal) { - this.$.createOverlay.open(); - } - } - - _computeRepoUrl(name) { - return this.getUrl(this._path + '/', name); - } - - _computeChangesLink(name) { - return Gerrit.Nav.getUrlForProjectChanges(name); - } - - _getCreateRepoCapability() { - return this.$.restAPI.getAccount().then(account => { - if (!account) { return; } - return this.$.restAPI.getAccountCapabilities(['createProject']) - .then(capabilities => { - if (capabilities.createProject) { - this._createNewCapability = true; - } - }); - }); - } - - _getRepos(filter, reposPerPage, offset) { - this._repos = []; - return this.$.restAPI.getRepos(filter, reposPerPage, offset) - .then(repos => { - // Late response. - if (filter !== this._filter || !repos) { return; } - this._repos = repos; - this._loading = false; - }); - } - - _refreshReposList() { - this.$.restAPI.invalidateReposCache(); - return this._getRepos(this._filter, this._reposPerPage, - this._offset); - } - - _handleCreateRepo() { - this.$.createNewModal.handleCreateRepo().then(() => { - this._refreshReposList(); - }); - } - - _handleCloseCreate() { - this.$.createOverlay.close(); - } - - _handleCreateClicked() { + _maybeOpenCreateOverlay(params) { + if (params && params.openCreateModal) { this.$.createOverlay.open(); } - - _readOnly(item) { - return item.state === 'READ_ONLY' ? 'Y' : ''; - } - - _computeWeblink(repo) { - if (!repo.web_links) { return ''; } - const webLinks = repo.web_links; - return webLinks.length ? webLinks : null; - } } - customElements.define(GrRepoList.is, GrRepoList); -})(); + _computeRepoUrl(name) { + return this.getUrl(this._path + '/', name); + } + + _computeChangesLink(name) { + return Gerrit.Nav.getUrlForProjectChanges(name); + } + + _getCreateRepoCapability() { + return this.$.restAPI.getAccount().then(account => { + if (!account) { return; } + return this.$.restAPI.getAccountCapabilities(['createProject']) + .then(capabilities => { + if (capabilities.createProject) { + this._createNewCapability = true; + } + }); + }); + } + + _getRepos(filter, reposPerPage, offset) { + this._repos = []; + return this.$.restAPI.getRepos(filter, reposPerPage, offset) + .then(repos => { + // Late response. + if (filter !== this._filter || !repos) { return; } + this._repos = repos; + this._loading = false; + }); + } + + _refreshReposList() { + this.$.restAPI.invalidateReposCache(); + return this._getRepos(this._filter, this._reposPerPage, + this._offset); + } + + _handleCreateRepo() { + this.$.createNewModal.handleCreateRepo().then(() => { + this._refreshReposList(); + }); + } + + _handleCloseCreate() { + this.$.createOverlay.close(); + } + + _handleCreateClicked() { + this.$.createOverlay.open(); + } + + _readOnly(item) { + return item.state === 'READ_ONLY' ? 'Y' : ''; + } + + _computeWeblink(repo) { + if (!repo.web_links) { return ''; } + const webLinks = repo.web_links; + return webLinks.length ? webLinks : null; + } +} + +customElements.define(GrRepoList.is, GrRepoList);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js index 08fd45c..d498869 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_html.js
@@ -1,32 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-create-repo-dialog/gr-create-repo-dialog.html"> - -<dom-module id="gr-repo-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -47,44 +37,32 @@ white-space:nowrap; } </style> - <gr-list-view - create-new=[[_createNewCapability]] - filter="[[_filter]]" - items-per-page="[[_reposPerPage]]" - items="[[_repos]]" - loading="[[_loading]]" - offset="[[_offset]]" - on-create-clicked="_handleCreateClicked" - path="[[_path]]"> + <gr-list-view create-new="[[_createNewCapability]]" filter="[[_filter]]" items-per-page="[[_reposPerPage]]" items="[[_repos]]" loading="[[_loading]]" offset="[[_offset]]" on-create-clicked="_handleCreateClicked" path="[[_path]]"> <table id="list" class="genericList"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="name topHeader">Repository Name</th> <th class="repositoryBrowser topHeader">Repository Browser</th> <th class="changesLink topHeader">Changes</th> <th class="topHeader readOnly">Read only</th> <th class="description topHeader">Repository Description</th> </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]"> <td>Loading...</td> </tr> - <tbody class$="[[computeLoadingClass(_loading)]]"> + </tbody><tbody class\$="[[computeLoadingClass(_loading)]]"> <template is="dom-repeat" items="[[_shownRepos]]"> <tr class="table"> <td class="name"> - <a href$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a> + <a href\$="[[_computeRepoUrl(item.name)]]">[[item.name]]</a> </td> <td class="repositoryBrowser"> - <template is="dom-repeat" - items="[[_computeWeblink(item)]]" as="link"> - <a href$="[[link.url]]" - class="webLink" - rel="noopener" - target="_blank"> + <template is="dom-repeat" items="[[_computeWeblink(item)]]" as="link"> + <a href\$="[[link.url]]" class="webLink" rel="noopener" target="_blank"> [[link.name]] </a> </template> </td> - <td class="changesLink"><a href$="[[_computeChangesLink(item.name)]]">view all</a></td> + <td class="changesLink"><a href\$="[[_computeChangesLink(item.name)]]">view all</a></td> <td class="readOnly">[[_readOnly(item)]]</td> <td class="description">[[item.description]]</td> </tr> @@ -92,26 +70,15 @@ </tbody> </table> </gr-list-view> - <gr-overlay id="createOverlay" with-backdrop> - <gr-dialog - id="createDialog" - class="confirmDialog" - disabled="[[!_hasNewRepoName]]" - confirm-label="Create" - on-confirm="_handleCreateRepo" - on-cancel="_handleCloseCreate"> + <gr-overlay id="createOverlay" with-backdrop=""> + <gr-dialog id="createDialog" class="confirmDialog" disabled="[[!_hasNewRepoName]]" confirm-label="Create" on-confirm="_handleCreateRepo" on-cancel="_handleCloseCreate"> <div class="header" slot="header"> Create Repository </div> <div class="main" slot="main"> - <gr-create-repo-dialog - has-new-repo-name="{{_hasNewRepoName}}" - params="[[params]]" - id="createNewModal"></gr-create-repo-dialog> + <gr-create-repo-dialog has-new-repo-name="{{_hasNewRepoName}}" params="[[params]]" id="createNewModal"></gr-create-repo-dialog> </div> </gr-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html index 4003b15..fbd1099 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-list.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,166 +41,168 @@ </template> </test-fixture> -<script> - let counter; - const repoGenerator = () => { - return { - id: `test${++counter}`, - state: 'ACTIVE', - web_links: [ - { - name: 'diffusion', - url: `https://phabricator.example.org/r/project/test${counter}`, - }, - ], - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-list.js'; +let counter; +const repoGenerator = () => { + return { + id: `test${++counter}`, + state: 'ACTIVE', + web_links: [ + { + name: 'diffusion', + url: `https://phabricator.example.org/r/project/test${counter}`, + }, + ], }; +}; - suite('gr-repo-list tests', async () => { - await readyToTest(); - let element; - let repos; - let sandbox; - let value; +suite('gr-repo-list tests', () => { + let element; + let repos; + let sandbox; + let value; + setup(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(page, 'show'); + element = fixture('basic'); + counter = 0; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list with repos', () => { + setup(done => { + repos = _.times(26, repoGenerator); + stub('gr-rest-api-interface', { + getRepos(num, offset) { + return Promise.resolve(repos); + }, + }); + element._paramsChanged(value).then(() => { flush(done); }); + }); + + test('test for test repo in the list', done => { + flush(() => { + assert.equal(element._repos[1].id, 'test2'); + done(); + }); + }); + + test('_shownRepos', () => { + assert.equal(element._shownRepos.length, 25); + }); + + test('_maybeOpenCreateOverlay', () => { + const overlayOpen = sandbox.stub(element.$.createOverlay, 'open'); + element._maybeOpenCreateOverlay(); + assert.isFalse(overlayOpen.called); + const params = {}; + element._maybeOpenCreateOverlay(params); + assert.isFalse(overlayOpen.called); + params.openCreateModal = true; + element._maybeOpenCreateOverlay(params); + assert.isTrue(overlayOpen.called); + }); + }); + + suite('list with less then 25 repos', () => { + setup(done => { + repos = _.times(25, repoGenerator); + + stub('gr-rest-api-interface', { + getRepos(num, offset) { + return Promise.resolve(repos); + }, + }); + + element._paramsChanged(value).then(() => { flush(done); }); + }); + + test('_shownRepos', () => { + assert.equal(element._shownRepos.length, 25); + }); + }); + + suite('filter', () => { + let reposFiltered; setup(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(page, 'show'); - element = fixture('basic'); - counter = 0; + repos = _.times(25, repoGenerator); + reposFiltered = _.times(1, repoGenerator); }); - teardown(() => { - sandbox.restore(); - }); - - suite('list with repos', () => { - setup(done => { - repos = _.times(26, repoGenerator); - stub('gr-rest-api-interface', { - getRepos(num, offset) { - return Promise.resolve(repos); - }, - }); - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('test for test repo in the list', done => { - flush(() => { - assert.equal(element._repos[1].id, 'test2'); - done(); - }); - }); - - test('_shownRepos', () => { - assert.equal(element._shownRepos.length, 25); - }); - - test('_maybeOpenCreateOverlay', () => { - const overlayOpen = sandbox.stub(element.$.createOverlay, 'open'); - element._maybeOpenCreateOverlay(); - assert.isFalse(overlayOpen.called); - const params = {}; - element._maybeOpenCreateOverlay(params); - assert.isFalse(overlayOpen.called); - params.openCreateModal = true; - element._maybeOpenCreateOverlay(params); - assert.isTrue(overlayOpen.called); + test('_paramsChanged', done => { + sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos)); + const value = { + filter: 'test', + offset: 25, + }; + element._paramsChanged(value).then(() => { + assert.isTrue(element.$.restAPI.getRepos.lastCall + .calledWithExactly('test', 25, 25)); + done(); }); }); - suite('list with less then 25 repos', () => { - setup(done => { - repos = _.times(25, repoGenerator); + test('latest repos requested are always set', done => { + const repoStub = sandbox.stub(element.$.restAPI, 'getRepos'); + repoStub.withArgs('test').returns(Promise.resolve(repos)); + repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered)); + element._filter = 'test'; - stub('gr-rest-api-interface', { - getRepos(num, offset) { - return Promise.resolve(repos); - }, - }); - - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('_shownRepos', () => { - assert.equal(element._shownRepos.length, 25); - }); - }); - - suite('filter', () => { - let reposFiltered; - setup(() => { - repos = _.times(25, repoGenerator); - reposFiltered = _.times(1, repoGenerator); - }); - - test('_paramsChanged', done => { - sandbox.stub(element.$.restAPI, 'getRepos', () => Promise.resolve(repos)); - const value = { - filter: 'test', - offset: 25, - }; - element._paramsChanged(value).then(() => { - assert.isTrue(element.$.restAPI.getRepos.lastCall - .calledWithExactly('test', 25, 25)); - done(); - }); - }); - - test('latest repos requested are always set', done => { - const repoStub = sandbox.stub(element.$.restAPI, 'getRepos'); - repoStub.withArgs('test').returns(Promise.resolve(repos)); - repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered)); - element._filter = 'test'; - - // Repos are not set because the element._filter differs. - element._getRepos('filter', 25, 0).then(() => { - assert.deepEqual(element._repos, []); - done(); - }); - }); - }); - - suite('loading', () => { - test('correct contents are displayed', () => { - assert.isTrue(element._loading); - assert.equal(element.computeLoadingClass(element._loading), 'loading'); - assert.equal(getComputedStyle(element.$.loading).display, 'block'); - - element._loading = false; - element._repos = _.times(25, repoGenerator); - - flushAsynchronousOperations(); - assert.equal(element.computeLoadingClass(element._loading), ''); - assert.equal(getComputedStyle(element.$.loading).display, 'none'); - }); - }); - - suite('create new', () => { - test('_handleCreateClicked called when create-click fired', () => { - sandbox.stub(element, '_handleCreateClicked'); - element.shadowRoot - .querySelector('gr-list-view').fire('create-clicked'); - assert.isTrue(element._handleCreateClicked.called); - }); - - test('_handleCreateClicked opens modal', () => { - const openStub = sandbox.stub(element.$.createOverlay, 'open'); - element._handleCreateClicked(); - assert.isTrue(openStub.called); - }); - - test('_handleCreateRepo called when confirm fired', () => { - sandbox.stub(element, '_handleCreateRepo'); - element.$.createDialog.fire('confirm'); - assert.isTrue(element._handleCreateRepo.called); - }); - - test('_handleCloseCreate called when cancel fired', () => { - sandbox.stub(element, '_handleCloseCreate'); - element.$.createDialog.fire('cancel'); - assert.isTrue(element._handleCloseCreate.called); + // Repos are not set because the element._filter differs. + element._getRepos('filter', 25, 0).then(() => { + assert.deepEqual(element._repos, []); + done(); }); }); }); + + suite('loading', () => { + test('correct contents are displayed', () => { + assert.isTrue(element._loading); + assert.equal(element.computeLoadingClass(element._loading), 'loading'); + assert.equal(getComputedStyle(element.$.loading).display, 'block'); + + element._loading = false; + element._repos = _.times(25, repoGenerator); + + flushAsynchronousOperations(); + assert.equal(element.computeLoadingClass(element._loading), ''); + assert.equal(getComputedStyle(element.$.loading).display, 'none'); + }); + }); + + suite('create new', () => { + test('_handleCreateClicked called when create-click fired', () => { + sandbox.stub(element, '_handleCreateClicked'); + element.shadowRoot + .querySelector('gr-list-view').fire('create-clicked'); + assert.isTrue(element._handleCreateClicked.called); + }); + + test('_handleCreateClicked opens modal', () => { + const openStub = sandbox.stub(element.$.createOverlay, 'open'); + element._handleCreateClicked(); + assert.isTrue(openStub.called); + }); + + test('_handleCreateRepo called when confirm fired', () => { + sandbox.stub(element, '_handleCreateRepo'); + element.$.createDialog.fire('confirm'); + assert.isTrue(element._handleCreateRepo.called); + }); + + test('_handleCloseCreate called when cancel fired', () => { + sandbox.stub(element, '_handleCloseCreate'); + element.$.createDialog.fire('cancel'); + assert.isTrue(element._handleCloseCreate.called); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js index 7368eb8..826bf93 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config.js
@@ -14,128 +14,145 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-input/iron-input.js'; +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import '../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-subpage-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-select/gr-select.js'; +import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; +import '../gr-plugin-config-array-editor/gr-plugin-config-array-editor.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-plugin-config_html.js'; + +/** + * @appliesMixin Gerrit.RepoPluginConfigMixin + * @extends Polymer.Element + */ +class GrRepoPluginConfig extends mixinBehaviors( [ + Gerrit.RepoPluginConfig, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-repo-plugin-config'; } /** - * @appliesMixin Gerrit.RepoPluginConfigMixin - * @extends Polymer.Element + * Fired when the plugin config changes. + * + * @event plugin-config-changed */ - class GrRepoPluginConfig extends Polymer.mixinBehaviors( [ - Gerrit.RepoPluginConfig, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-plugin-config'; } - /** - * Fired when the plugin config changes. - * - * @event plugin-config-changed - */ - static get properties() { - return { - /** @type {?} */ - pluginData: Object, - /** @type {Array} */ - _pluginConfigOptions: { - type: Array, - computed: '_computePluginConfigOptions(pluginData.*)', - }, - }; - } - - _computePluginConfigOptions(dataRecord) { - if (!dataRecord || !dataRecord.base || !dataRecord.base.config) { - return []; - } - const {config} = dataRecord.base; - return Object.keys(config) - .map(_key => { return {_key, info: config[_key]}; }); - } - - _isArray(type) { - return type === this.ENTRY_TYPES.ARRAY; - } - - _isBoolean(type) { - return type === this.ENTRY_TYPES.BOOLEAN; - } - - _isList(type) { - return type === this.ENTRY_TYPES.LIST; - } - - _isString(type) { - // Treat numbers like strings for simplicity. - return type === this.ENTRY_TYPES.STRING || - type === this.ENTRY_TYPES.INT || - type === this.ENTRY_TYPES.LONG; - } - - _computeDisabled(editable) { - return editable === 'false'; - } - - /** - * @param {string} value - fallback to 'false' if undefined - */ - _computeChecked(value = 'false') { - return JSON.parse(value); - } - - _handleStringChange(e) { - const el = Polymer.dom(e).localTarget; - const _key = el.getAttribute('data-option-key'); - const configChangeInfo = - this._buildConfigChangeInfo(el.value, _key); - this._handleChange(configChangeInfo); - } - - _handleListChange(e) { - const el = Polymer.dom(e).localTarget; - const _key = el.getAttribute('data-option-key'); - const configChangeInfo = - this._buildConfigChangeInfo(el.value, _key); - this._handleChange(configChangeInfo); - } - - _handleBooleanChange(e) { - const el = Polymer.dom(e).localTarget; - const _key = el.getAttribute('data-option-key'); - const configChangeInfo = - this._buildConfigChangeInfo(JSON.stringify(el.checked), _key); - this._handleChange(configChangeInfo); - } - - _buildConfigChangeInfo(value, _key) { - const info = this.pluginData.config[_key]; - info.value = value; - return { - _key, - info, - notifyPath: `${_key}.value`, - }; - } - - _handleArrayChange({detail}) { - this._handleChange(detail); - } - - _handleChange({_key, info, notifyPath}) { - const {name, config} = this.pluginData; - - /** @type {Object} */ - const detail = { - name, - config: Object.assign(config, {[_key]: info}, {}), - notifyPath: `${name}.${notifyPath}`, - }; - - this.dispatchEvent(new CustomEvent( - this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true})); - } + static get properties() { + return { + /** @type {?} */ + pluginData: Object, + /** @type {Array} */ + _pluginConfigOptions: { + type: Array, + computed: '_computePluginConfigOptions(pluginData.*)', + }, + }; } - customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig); -})(); + _computePluginConfigOptions(dataRecord) { + if (!dataRecord || !dataRecord.base || !dataRecord.base.config) { + return []; + } + const {config} = dataRecord.base; + return Object.keys(config) + .map(_key => { return {_key, info: config[_key]}; }); + } + + _isArray(type) { + return type === this.ENTRY_TYPES.ARRAY; + } + + _isBoolean(type) { + return type === this.ENTRY_TYPES.BOOLEAN; + } + + _isList(type) { + return type === this.ENTRY_TYPES.LIST; + } + + _isString(type) { + // Treat numbers like strings for simplicity. + return type === this.ENTRY_TYPES.STRING || + type === this.ENTRY_TYPES.INT || + type === this.ENTRY_TYPES.LONG; + } + + _computeDisabled(editable) { + return editable === 'false'; + } + + /** + * @param {string} value - fallback to 'false' if undefined + */ + _computeChecked(value = 'false') { + return JSON.parse(value); + } + + _handleStringChange(e) { + const el = dom(e).localTarget; + const _key = el.getAttribute('data-option-key'); + const configChangeInfo = + this._buildConfigChangeInfo(el.value, _key); + this._handleChange(configChangeInfo); + } + + _handleListChange(e) { + const el = dom(e).localTarget; + const _key = el.getAttribute('data-option-key'); + const configChangeInfo = + this._buildConfigChangeInfo(el.value, _key); + this._handleChange(configChangeInfo); + } + + _handleBooleanChange(e) { + const el = dom(e).localTarget; + const _key = el.getAttribute('data-option-key'); + const configChangeInfo = + this._buildConfigChangeInfo(JSON.stringify(el.checked), _key); + this._handleChange(configChangeInfo); + } + + _buildConfigChangeInfo(value, _key) { + const info = this.pluginData.config[_key]; + info.value = value; + return { + _key, + info, + notifyPath: `${_key}.value`, + }; + } + + _handleArrayChange({detail}) { + this._handleChange(detail); + } + + _handleChange({_key, info, notifyPath}) { + const {name, config} = this.pluginData; + + /** @type {Object} */ + const detail = { + name, + config: Object.assign(config, {[_key]: info}, {}), + notifyPath: `${name}.${notifyPath}`, + }; + + this.dispatchEvent(new CustomEvent( + this.PLUGIN_CONFIG_CHANGED, {detail, bubbles: true, composed: true})); + } +} + +customElements.define(GrRepoPluginConfig.is, GrRepoPluginConfig);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js index ef5b755..fa4617d 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_html.js
@@ -1,35 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html"> - -<link rel="import" href="../../../behaviors/gr-repo-plugin-config-behavior/gr-repo-plugin-config-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-subpage-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../gr-plugin-config-array-editor/gr-plugin-config-array-editor.html"> - -<dom-module id="gr-repo-plugin-config"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -53,55 +40,31 @@ <fieldset> <h4>[[pluginData.name]]</h4> <template is="dom-repeat" items="[[_pluginConfigOptions]]" as="option"> - <section class$="section [[option.info.type]]"> + <section class\$="section [[option.info.type]]"> <span class="title"> - <gr-tooltip-content - has-tooltip="[[option.info.description]]" - show-icon="[[option.info.description]]" - title="[[option.info.description]]"> + <gr-tooltip-content has-tooltip="[[option.info.description]]" show-icon="[[option.info.description]]" title="[[option.info.description]]"> <span>[[option.info.display_name]]</span> </gr-tooltip-content> </span> <span class="value"> <template is="dom-if" if="[[_isArray(option.info.type)]]"> - <gr-plugin-config-array-editor - on-plugin-config-option-changed="_handleArrayChange" - plugin-option="[[option]]"></gr-plugin-config-array-editor> + <gr-plugin-config-array-editor on-plugin-config-option-changed="_handleArrayChange" plugin-option="[[option]]"></gr-plugin-config-array-editor> </template> <template is="dom-if" if="[[_isBoolean(option.info.type)]]"> - <paper-toggle-button - checked="[[_computeChecked(option.info.value)]]" - on-change="_handleBooleanChange" - data-option-key$="[[option._key]]" - disabled$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button> + <paper-toggle-button checked="[[_computeChecked(option.info.value)]]" on-change="_handleBooleanChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"></paper-toggle-button> </template> <template is="dom-if" if="[[_isList(option.info.type)]]"> - <gr-select - bind-value$="[[option.info.value]]" - on-change="_handleListChange"> - <select - data-option-key$="[[option._key]]" - disabled$="[[_computeDisabled(option.info.editable)]]"> - <template is="dom-repeat" - items="[[option.info.permitted_values]]" - as="value"> - <option value$="[[value]]">[[value]]</option> + <gr-select bind-value\$="[[option.info.value]]" on-change="_handleListChange"> + <select data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"> + <template is="dom-repeat" items="[[option.info.permitted_values]]" as="value"> + <option value\$="[[value]]">[[value]]</option> </template> </select> </gr-select> </template> <template is="dom-if" if="[[_isString(option.info.type)]]"> - <iron-input - bind-value="[[option.info.value]]" - on-input="_handleStringChange" - data-option-key$="[[option._key]]" - disabled$="[[_computeDisabled(option.info.editable)]]"> - <input - is="iron-input" - value="[[option.info.value]]" - on-input="_handleStringChange" - data-option-key$="[[option._key]]" - disabled$="[[_computeDisabled(option.info.editable)]]"> + <iron-input bind-value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"> + <input is="iron-input" value="[[option.info.value]]" on-input="_handleStringChange" data-option-key\$="[[option._key]]" disabled\$="[[_computeDisabled(option.info.editable)]]"> </iron-input> </template> <template is="dom-if" if="[[option.info.inherited_value]]"> @@ -114,6 +77,4 @@ </template> </fieldset> </div> - </template> - <script src="gr-repo-plugin-config.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html index 8313edf..f70d3ea 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo-plugin-config/gr-repo-plugin-config_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-plugin-config</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-plugin-config.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-plugin-config.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-plugin-config.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,150 +40,152 @@ </template> </test-fixture> -<script> - suite('gr-repo-plugin-config tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-plugin-config.js'; +suite('gr-repo-plugin-config tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => sandbox.restore()); + + test('_computePluginConfigOptions', () => { + assert.deepEqual(element._computePluginConfigOptions(), []); + assert.deepEqual(element._computePluginConfigOptions({}), []); + assert.deepEqual(element._computePluginConfigOptions({base: {}}), []); + assert.deepEqual(element._computePluginConfigOptions( + {base: {config: {}}}), []); + assert.deepEqual(element._computePluginConfigOptions( + {base: {config: {testKey: 'testInfo'}}}), + [{_key: 'testKey', info: 'testInfo'}]); + }); + + test('_computeDisabled', () => { + assert.isFalse(element._computeDisabled('true')); + assert.isTrue(element._computeDisabled('false')); + }); + + test('_handleChange', () => { + const eventStub = sandbox.stub(element, 'dispatchEvent'); + element.pluginData = { + name: 'testName', + config: {plugin: {value: 'test'}}, + }; + element._handleChange({ + _key: 'plugin', + info: {value: 'newTest'}, + notifyPath: 'plugin.value', + }); + + assert.isTrue(eventStub.called); + + const {detail} = eventStub.lastCall.args[0]; + assert.equal(detail.name, 'testName'); + assert.deepEqual(detail.config, {plugin: {value: 'newTest'}}); + assert.equal(detail.notifyPath, 'testName.plugin.value'); + }); + + suite('option types', () => { + let changeStub; + let buildStub; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + changeStub = sandbox.stub(element, '_handleChange'); + buildStub = sandbox.stub(element, '_buildConfigChangeInfo'); }); - teardown(() => sandbox.restore()); - - test('_computePluginConfigOptions', () => { - assert.deepEqual(element._computePluginConfigOptions(), []); - assert.deepEqual(element._computePluginConfigOptions({}), []); - assert.deepEqual(element._computePluginConfigOptions({base: {}}), []); - assert.deepEqual(element._computePluginConfigOptions( - {base: {config: {}}}), []); - assert.deepEqual(element._computePluginConfigOptions( - {base: {config: {testKey: 'testInfo'}}}), - [{_key: 'testKey', info: 'testInfo'}]); - }); - - test('_computeDisabled', () => { - assert.isFalse(element._computeDisabled('true')); - assert.isTrue(element._computeDisabled('false')); - }); - - test('_handleChange', () => { - const eventStub = sandbox.stub(element, 'dispatchEvent'); + test('ARRAY type option', () => { element.pluginData = { name: 'testName', - config: {plugin: {value: 'test'}}, + config: {plugin: {value: 'test', type: 'ARRAY'}}, }; - element._handleChange({ - _key: 'plugin', - info: {value: 'newTest'}, - notifyPath: 'plugin.value', - }); + flushAsynchronousOperations(); - assert.isTrue(eventStub.called); - - const {detail} = eventStub.lastCall.args[0]; - assert.equal(detail.name, 'testName'); - assert.deepEqual(detail.config, {plugin: {value: 'newTest'}}); - assert.equal(detail.notifyPath, 'testName.plugin.value'); + const editor = element.shadowRoot + .querySelector('gr-plugin-config-array-editor'); + assert.ok(editor); + element._handleArrayChange({detail: 'test'}); + assert.isTrue(changeStub.called); + assert.equal(changeStub.lastCall.args[0], 'test'); }); - suite('option types', () => { - let changeStub; - let buildStub; - - setup(() => { - changeStub = sandbox.stub(element, '_handleChange'); - buildStub = sandbox.stub(element, '_buildConfigChangeInfo'); - }); - - test('ARRAY type option', () => { - element.pluginData = { - name: 'testName', - config: {plugin: {value: 'test', type: 'ARRAY'}}, - }; - flushAsynchronousOperations(); - - const editor = element.shadowRoot - .querySelector('gr-plugin-config-array-editor'); - assert.ok(editor); - element._handleArrayChange({detail: 'test'}); - assert.isTrue(changeStub.called); - assert.equal(changeStub.lastCall.args[0], 'test'); - }); - - test('BOOLEAN type option', () => { - element.pluginData = { - name: 'testName', - config: {plugin: {value: 'true', type: 'BOOLEAN'}}, - }; - flushAsynchronousOperations(); - - const toggle = element.shadowRoot - .querySelector('paper-toggle-button'); - assert.ok(toggle); - toggle.click(); - flushAsynchronousOperations(); - - assert.isTrue(buildStub.called); - assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']); - - assert.isTrue(changeStub.called); - }); - - test('INT/LONG/STRING type option', () => { - element.pluginData = { - name: 'testName', - config: {plugin: {value: 'test', type: 'STRING'}}, - }; - flushAsynchronousOperations(); - - const input = element.shadowRoot - .querySelector('input'); - assert.ok(input); - input.value = 'newTest'; - input.dispatchEvent(new Event('input')); - flushAsynchronousOperations(); - - assert.isTrue(buildStub.called); - assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']); - - assert.isTrue(changeStub.called); - }); - - test('LIST type option', () => { - const permitted_values = ['test', 'newTest']; - element.pluginData = { - name: 'testName', - config: {plugin: {value: 'test', type: 'LIST', permitted_values}}, - }; - flushAsynchronousOperations(); - - const select = element.shadowRoot - .querySelector('select'); - assert.ok(select); - select.value = 'newTest'; - select.dispatchEvent(new Event( - 'change', {bubbles: true, composed: true})); - flushAsynchronousOperations(); - - assert.isTrue(buildStub.called); - assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']); - - assert.isTrue(changeStub.called); - }); - }); - - test('_buildConfigChangeInfo', () => { + test('BOOLEAN type option', () => { element.pluginData = { name: 'testName', - config: {plugin: {value: 'test'}}, + config: {plugin: {value: 'true', type: 'BOOLEAN'}}, }; - const detail = element._buildConfigChangeInfo('newTest', 'plugin'); - assert.equal(detail._key, 'plugin'); - assert.deepEqual(detail.info, {value: 'newTest'}); - assert.equal(detail.notifyPath, 'plugin.value'); + flushAsynchronousOperations(); + + const toggle = element.shadowRoot + .querySelector('paper-toggle-button'); + assert.ok(toggle); + toggle.click(); + flushAsynchronousOperations(); + + assert.isTrue(buildStub.called); + assert.deepEqual(buildStub.lastCall.args, ['false', 'plugin']); + + assert.isTrue(changeStub.called); + }); + + test('INT/LONG/STRING type option', () => { + element.pluginData = { + name: 'testName', + config: {plugin: {value: 'test', type: 'STRING'}}, + }; + flushAsynchronousOperations(); + + const input = element.shadowRoot + .querySelector('input'); + assert.ok(input); + input.value = 'newTest'; + input.dispatchEvent(new Event('input')); + flushAsynchronousOperations(); + + assert.isTrue(buildStub.called); + assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']); + + assert.isTrue(changeStub.called); + }); + + test('LIST type option', () => { + const permitted_values = ['test', 'newTest']; + element.pluginData = { + name: 'testName', + config: {plugin: {value: 'test', type: 'LIST', permitted_values}}, + }; + flushAsynchronousOperations(); + + const select = element.shadowRoot + .querySelector('select'); + assert.ok(select); + select.value = 'newTest'; + select.dispatchEvent(new Event( + 'change', {bubbles: true, composed: true})); + flushAsynchronousOperations(); + + assert.isTrue(buildStub.called); + assert.deepEqual(buildStub.lastCall.args, ['newTest', 'plugin']); + + assert.isTrue(changeStub.called); }); }); + + test('_buildConfigChangeInfo', () => { + element.pluginData = { + name: 'testName', + config: {plugin: {value: 'test'}}, + }; + const detail = element._buildConfigChangeInfo('newTest', 'plugin'); + assert.equal(detail._key, 'plugin'); + assert.deepEqual(detail.info, {value: 'newTest'}); + assert.equal(detail.notifyPath, 'plugin.value'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js index f6328de..fd85f1a 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo.js
@@ -14,348 +14,366 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const STATES = { - active: {value: 'ACTIVE', label: 'Active'}, - readOnly: {value: 'READ_ONLY', label: 'Read Only'}, - hidden: {value: 'HIDDEN', label: 'Hidden'}, - }; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../shared/gr-download-commands/gr-download-commands.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-subpage-styles.js'; +import '../../../styles/shared-styles.js'; +import '../gr-repo-plugin-config/gr-repo-plugin-config.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo_html.js'; - const SUBMIT_TYPES = { - // Exclude INHERIT, which is handled specially. - mergeIfNecessary: { - value: 'MERGE_IF_NECESSARY', - label: 'Merge if necessary', - }, - fastForwardOnly: { - value: 'FAST_FORWARD_ONLY', - label: 'Fast forward only', - }, - rebaseAlways: { - value: 'REBASE_ALWAYS', - label: 'Rebase Always', - }, - rebaseIfNecessary: { - value: 'REBASE_IF_NECESSARY', - label: 'Rebase if necessary', - }, - mergeAlways: { - value: 'MERGE_ALWAYS', - label: 'Merge always', - }, - cherryPick: { - value: 'CHERRY_PICK', - label: 'Cherry pick', - }, - }; +const STATES = { + active: {value: 'ACTIVE', label: 'Active'}, + readOnly: {value: 'READ_ONLY', label: 'Read Only'}, + hidden: {value: 'HIDDEN', label: 'Hidden'}, +}; - /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrRepo extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo'; } +const SUBMIT_TYPES = { + // Exclude INHERIT, which is handled specially. + mergeIfNecessary: { + value: 'MERGE_IF_NECESSARY', + label: 'Merge if necessary', + }, + fastForwardOnly: { + value: 'FAST_FORWARD_ONLY', + label: 'Fast forward only', + }, + rebaseAlways: { + value: 'REBASE_ALWAYS', + label: 'Rebase Always', + }, + rebaseIfNecessary: { + value: 'REBASE_IF_NECESSARY', + label: 'Rebase if necessary', + }, + mergeAlways: { + value: 'MERGE_ALWAYS', + label: 'Merge always', + }, + cherryPick: { + value: 'CHERRY_PICK', + label: 'Cherry pick', + }, +}; - static get properties() { - return { - params: Object, - repo: String, +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrRepo extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _configChanged: { - type: Boolean, - value: false, + static get is() { return 'gr-repo'; } + + static get properties() { + return { + params: Object, + repo: String, + + _configChanged: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + _loggedIn: { + type: Boolean, + value: false, + observer: '_loggedInChanged', + }, + /** @type {?} */ + _repoConfig: Object, + /** @type {?} */ + _pluginData: { + type: Array, + computed: '_computePluginData(_repoConfig.plugin_config.*)', + }, + _readOnly: { + type: Boolean, + value: true, + }, + _states: { + type: Array, + value() { + return Object.values(STATES); }, - _loading: { - type: Boolean, - value: true, + }, + _submitTypes: { + type: Array, + value() { + return Object.values(SUBMIT_TYPES); }, - _loggedIn: { - type: Boolean, - value: false, - observer: '_loggedInChanged', - }, - /** @type {?} */ - _repoConfig: Object, - /** @type {?} */ - _pluginData: { - type: Array, - computed: '_computePluginData(_repoConfig.plugin_config.*)', - }, - _readOnly: { - type: Boolean, - value: true, - }, - _states: { - type: Array, - value() { - return Object.values(STATES); - }, - }, - _submitTypes: { - type: Array, - value() { - return Object.values(SUBMIT_TYPES); - }, - }, - _schemes: { - type: Array, - value() { return []; }, - computed: '_computeSchemes(_schemesObj)', - observer: '_schemesChanged', - }, - _selectedCommand: { - type: String, - value: 'Clone', - }, - _selectedScheme: String, - _schemesObj: Object, - }; - } + }, + _schemes: { + type: Array, + value() { return []; }, + computed: '_computeSchemes(_schemesObj)', + observer: '_schemesChanged', + }, + _selectedCommand: { + type: String, + value: 'Clone', + }, + _selectedScheme: String, + _schemesObj: Object, + }; + } - static get observers() { - return [ - '_handleConfigChanged(_repoConfig.*)', - ]; - } + static get observers() { + return [ + '_handleConfigChanged(_repoConfig.*)', + ]; + } - /** @override */ - attached() { - super.attached(); - this._loadRepo(); + /** @override */ + attached() { + super.attached(); + this._loadRepo(); - this.fire('title-change', {title: this.repo}); - } + this.fire('title-change', {title: this.repo}); + } - _computePluginData(configRecord) { - if (!configRecord || - !configRecord.base) { return []; } + _computePluginData(configRecord) { + if (!configRecord || + !configRecord.base) { return []; } - const pluginConfig = configRecord.base; - return Object.keys(pluginConfig) - .map(name => { return {name, config: pluginConfig[name]}; }); - } + const pluginConfig = configRecord.base; + return Object.keys(pluginConfig) + .map(name => { return {name, config: pluginConfig[name]}; }); + } - _loadRepo() { - if (!this.repo) { return Promise.resolve(); } + _loadRepo() { + if (!this.repo) { return Promise.resolve(); } - const promises = []; + const promises = []; - const errFn = response => { - this.fire('page-error', {response}); - }; + const errFn = response => { + this.fire('page-error', {response}); + }; - promises.push(this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - if (loggedIn) { - this.$.restAPI.getRepoAccess(this.repo).then(access => { - if (!access) { return Promise.resolve(); } + promises.push(this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + if (loggedIn) { + this.$.restAPI.getRepoAccess(this.repo).then(access => { + if (!access) { return Promise.resolve(); } - // If the user is not an owner, is_owner is not a property. - this._readOnly = !access[this.repo].is_owner; - }); - } - })); - - promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn) - .then(config => { - if (!config) { return Promise.resolve(); } - - if (config.default_submit_type) { - // The gr-select is bound to submit_type, which needs to be the - // *configured* submit type. When default_submit_type is - // present, the server reports the *effective* submit type in - // submit_type, so we need to overwrite it before storing the - // config in this. - config.submit_type = - config.default_submit_type.configured_value; - } - if (!config.state) { - config.state = STATES.active.value; - } - this._repoConfig = config; - this._loading = false; - })); - - promises.push(this.$.restAPI.getConfig().then(config => { - if (!config) { return Promise.resolve(); } - - this._schemesObj = config.download.schemes; - })); - - return Promise.all(promises); - } - - _computeLoadingClass(loading) { - return loading ? 'loading' : ''; - } - - _computeHideClass(arr) { - return !arr || !arr.length ? 'hide' : ''; - } - - _loggedInChanged(_loggedIn) { - if (!_loggedIn) { return; } - this.$.restAPI.getPreferences().then(prefs => { - if (prefs.download_scheme) { - // Note (issue 5180): normalize the download scheme with lower-case. - this._selectedScheme = prefs.download_scheme.toLowerCase(); - } - }); - } - - _formatBooleanSelect(item) { - if (!item) { return; } - let inheritLabel = 'Inherit'; - if (!(item.inherited_value === undefined)) { - inheritLabel = `Inherit (${item.inherited_value})`; - } - return [ - { - label: inheritLabel, - value: 'INHERIT', - }, - { - label: 'True', - value: 'TRUE', - }, { - label: 'False', - value: 'FALSE', - }, - ]; - } - - _formatSubmitTypeSelect(projectConfig) { - if (!projectConfig) { return; } - const allValues = Object.values(SUBMIT_TYPES); - const type = projectConfig.default_submit_type; - if (!type) { - // Server is too old to report default_submit_type, so assume INHERIT - // is not a valid value. - return allValues; - } - - let inheritLabel = 'Inherit'; - if (type.inherited_value) { - let inherited = type.inherited_value; - for (const val of allValues) { - if (val.value === type.inherited_value) { - inherited = val.label; - break; - } - } - inheritLabel = `Inherit (${inherited})`; - } - return [ - { - label: inheritLabel, - value: 'INHERIT', - }, - ...allValues, - ]; - } - - _isLoading() { - return this._loading || this._loading === undefined; - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _formatRepoConfigForSave(repoConfig) { - const configInputObj = {}; - for (const key in repoConfig) { - if (repoConfig.hasOwnProperty(key)) { - if (key === 'default_submit_type') { - // default_submit_type is not in the input type, and the - // configured value was already copied to submit_type by - // _loadProject. Omit this property when saving. - continue; - } - if (key === 'plugin_config') { - configInputObj.plugin_config_values = repoConfig[key]; - } else if (typeof repoConfig[key] === 'object') { - configInputObj[key] = repoConfig[key].configured_value; - } else { - configInputObj[key] = repoConfig[key]; - } - } - } - return configInputObj; - } - - _handleSaveRepoConfig() { - return this.$.restAPI.saveRepoConfig(this.repo, - this._formatRepoConfigForSave(this._repoConfig)).then(() => { - this._configChanged = false; - }); - } - - _handleConfigChanged() { - if (this._isLoading()) { return; } - this._configChanged = true; - } - - _computeButtonDisabled(readOnly, configChanged) { - return readOnly || !configChanged; - } - - _computeHeaderClass(configChanged) { - return configChanged ? 'edited' : ''; - } - - _computeSchemes(schemesObj) { - return Object.keys(schemesObj); - } - - _schemesChanged(schemes) { - if (schemes.length === 0) { return; } - if (!schemes.includes(this._selectedScheme)) { - this._selectedScheme = schemes.sort()[0]; - } - } - - _computeCommands(repo, schemesObj, _selectedScheme) { - if (!schemesObj || !repo || !_selectedScheme) { - return []; - } - const commands = []; - let commandObj; - if (schemesObj.hasOwnProperty(_selectedScheme)) { - commandObj = schemesObj[_selectedScheme].clone_commands; - } - for (const title in commandObj) { - if (!commandObj.hasOwnProperty(title)) { continue; } - commands.push({ - title, - command: commandObj[title] - .replace(/\$\{project\}/gi, encodeURI(repo)) - .replace(/\$\{project-base-name\}/gi, - encodeURI(repo.substring(repo.lastIndexOf('/') + 1))), + // If the user is not an owner, is_owner is not a property. + this._readOnly = !access[this.repo].is_owner; }); } - return commands; + })); + + promises.push(this.$.restAPI.getProjectConfig(this.repo, errFn) + .then(config => { + if (!config) { return Promise.resolve(); } + + if (config.default_submit_type) { + // The gr-select is bound to submit_type, which needs to be the + // *configured* submit type. When default_submit_type is + // present, the server reports the *effective* submit type in + // submit_type, so we need to overwrite it before storing the + // config in this. + config.submit_type = + config.default_submit_type.configured_value; + } + if (!config.state) { + config.state = STATES.active.value; + } + this._repoConfig = config; + this._loading = false; + })); + + promises.push(this.$.restAPI.getConfig().then(config => { + if (!config) { return Promise.resolve(); } + + this._schemesObj = config.download.schemes; + })); + + return Promise.all(promises); + } + + _computeLoadingClass(loading) { + return loading ? 'loading' : ''; + } + + _computeHideClass(arr) { + return !arr || !arr.length ? 'hide' : ''; + } + + _loggedInChanged(_loggedIn) { + if (!_loggedIn) { return; } + this.$.restAPI.getPreferences().then(prefs => { + if (prefs.download_scheme) { + // Note (issue 5180): normalize the download scheme with lower-case. + this._selectedScheme = prefs.download_scheme.toLowerCase(); + } + }); + } + + _formatBooleanSelect(item) { + if (!item) { return; } + let inheritLabel = 'Inherit'; + if (!(item.inherited_value === undefined)) { + inheritLabel = `Inherit (${item.inherited_value})`; + } + return [ + { + label: inheritLabel, + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]; + } + + _formatSubmitTypeSelect(projectConfig) { + if (!projectConfig) { return; } + const allValues = Object.values(SUBMIT_TYPES); + const type = projectConfig.default_submit_type; + if (!type) { + // Server is too old to report default_submit_type, so assume INHERIT + // is not a valid value. + return allValues; } - _computeRepositoriesClass(config) { - return config ? 'showConfig': ''; + let inheritLabel = 'Inherit'; + if (type.inherited_value) { + let inherited = type.inherited_value; + for (const val of allValues) { + if (val.value === type.inherited_value) { + inherited = val.label; + break; + } + } + inheritLabel = `Inherit (${inherited})`; } + return [ + { + label: inheritLabel, + value: 'INHERIT', + }, + ...allValues, + ]; + } - _computeChangesUrl(name) { - return Gerrit.Nav.getUrlForProjectChanges(name); + _isLoading() { + return this._loading || this._loading === undefined; + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _formatRepoConfigForSave(repoConfig) { + const configInputObj = {}; + for (const key in repoConfig) { + if (repoConfig.hasOwnProperty(key)) { + if (key === 'default_submit_type') { + // default_submit_type is not in the input type, and the + // configured value was already copied to submit_type by + // _loadProject. Omit this property when saving. + continue; + } + if (key === 'plugin_config') { + configInputObj.plugin_config_values = repoConfig[key]; + } else if (typeof repoConfig[key] === 'object') { + configInputObj[key] = repoConfig[key].configured_value; + } else { + configInputObj[key] = repoConfig[key]; + } + } } + return configInputObj; + } - _handlePluginConfigChanged({detail: {name, config, notifyPath}}) { - this._repoConfig.plugin_config[name] = config; - this.notifyPath('_repoConfig.plugin_config.' + notifyPath); + _handleSaveRepoConfig() { + return this.$.restAPI.saveRepoConfig(this.repo, + this._formatRepoConfigForSave(this._repoConfig)).then(() => { + this._configChanged = false; + }); + } + + _handleConfigChanged() { + if (this._isLoading()) { return; } + this._configChanged = true; + } + + _computeButtonDisabled(readOnly, configChanged) { + return readOnly || !configChanged; + } + + _computeHeaderClass(configChanged) { + return configChanged ? 'edited' : ''; + } + + _computeSchemes(schemesObj) { + return Object.keys(schemesObj); + } + + _schemesChanged(schemes) { + if (schemes.length === 0) { return; } + if (!schemes.includes(this._selectedScheme)) { + this._selectedScheme = schemes.sort()[0]; } } - customElements.define(GrRepo.is, GrRepo); -})(); + _computeCommands(repo, schemesObj, _selectedScheme) { + if (!schemesObj || !repo || !_selectedScheme) { + return []; + } + const commands = []; + let commandObj; + if (schemesObj.hasOwnProperty(_selectedScheme)) { + commandObj = schemesObj[_selectedScheme].clone_commands; + } + for (const title in commandObj) { + if (!commandObj.hasOwnProperty(title)) { continue; } + commands.push({ + title, + command: commandObj[title] + .replace(/\$\{project\}/gi, encodeURI(repo)) + .replace(/\$\{project-base-name\}/gi, + encodeURI(repo.substring(repo.lastIndexOf('/') + 1))), + }); + } + return commands; + } + + _computeRepositoriesClass(config) { + return config ? 'showConfig': ''; + } + + _computeChangesUrl(name) { + return Gerrit.Nav.getUrlForProjectChanges(name); + } + + _handlePluginConfigChanged({detail: {name, config, notifyPath}}) { + this._repoConfig.plugin_config[name] = config; + this.notifyPath('_repoConfig.plugin_config.' + notifyPath); + } +} + +customElements.define(GrRepo.is, GrRepo);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js index 5e37261..2c7540f 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_html.js
@@ -1,37 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> - -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-subpage-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-repo-plugin-config/gr-repo-plugin-config.html"> - -<dom-module id="gr-repo"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -65,50 +50,37 @@ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <div class="info"> - <h1 id="Title" class$="name"> + <h1 id="Title" class\$="name"> [[repo]] - <hr/> + <hr> </h1> <div> - <a href$="[[_computeChangesUrl(repo)]]">(view changes)</a> + <a href\$="[[_computeChangesUrl(repo)]]">(view changes)</a> </div> </div> - <div id="loading" class$="[[_computeLoadingClass(_loading)]]">Loading...</div> - <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> - <div id="downloadContent" class$="[[_computeHideClass(_schemes)]]"> + <div id="loading" class\$="[[_computeLoadingClass(_loading)]]">Loading...</div> + <div id="loadedContent" class\$="[[_computeLoadingClass(_loading)]]"> + <div id="downloadContent" class\$="[[_computeHideClass(_schemes)]]"> <h2 id="download">Download</h2> <fieldset> - <gr-download-commands - id="downloadCommands" - commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]" - schemes="[[_schemes]]" - selected-scheme="{{_selectedScheme}}"></gr-download-commands> + <gr-download-commands id="downloadCommands" commands="[[_computeCommands(repo, _schemesObj, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands> </fieldset> </div> - <h2 id="configurations" - class$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2> + <h2 id="configurations" class\$="[[_computeHeaderClass(_configChanged)]]">Configurations</h2> <div id="form"> <fieldset> <h3 id="Description">Description</h3> <fieldset> - <iron-autogrow-textarea - id="descriptionInput" - class="description" - autocomplete="on" - placeholder="<Insert repo description here>" - bind-value="{{_repoConfig.description}}" - disabled$="[[_readOnly]]"></iron-autogrow-textarea> + <iron-autogrow-textarea id="descriptionInput" class="description" autocomplete="on" placeholder="<Insert repo description here>" bind-value="{{_repoConfig.description}}" disabled\$="[[_readOnly]]"></iron-autogrow-textarea> </fieldset> <h3 id="Options">Repository Options</h3> <fieldset id="options"> <section> <span class="title">State</span> <span class="value"> - <gr-select - id="stateSelect" - bind-value="{{_repoConfig.state}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" items=[[_states]]> + <gr-select id="stateSelect" bind-value="{{_repoConfig.state}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_states]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -118,12 +90,9 @@ <section> <span class="title">Submit type</span> <span class="value"> - <gr-select - id="submitTypeSelect" - bind-value="{{_repoConfig.submit_type}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatSubmitTypeSelect(_repoConfig)]]"> + <gr-select id="submitTypeSelect" bind-value="{{_repoConfig.submit_type}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatSubmitTypeSelect(_repoConfig)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -133,12 +102,9 @@ <section> <span class="title">Allow content merges</span> <span class="value"> - <gr-select - id="contentMergeSelect" - bind-value="{{_repoConfig.use_content_merge.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]"> + <gr-select id="contentMergeSelect" bind-value="{{_repoConfig.use_content_merge.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_content_merge)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -150,12 +116,9 @@ Create a new change for every commit not in the target branch </span> <span class="value"> - <gr-select - id="newChangeSelect" - bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]"> + <gr-select id="newChangeSelect" bind-value="{{_repoConfig.create_new_change_for_all_not_in_target.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.create_new_change_for_all_not_in_target)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -165,46 +128,33 @@ <section> <span class="title">Require Change-Id in commit message</span> <span class="value"> - <gr-select - id="requireChangeIdSelect" - bind-value="{{_repoConfig.require_change_id.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]"> + <gr-select id="requireChangeIdSelect" bind-value="{{_repoConfig.require_change_id.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_change_id)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> </gr-select> </span> </section> - <section - id="enableSignedPushSettings" - class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]"> + <section id="enableSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.enable_signed_push)]]"> <span class="title">Enable signed push</span> <span class="value"> - <gr-select - id="enableSignedPush" - bind-value="{{_repoConfig.enable_signed_push.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]"> + <gr-select id="enableSignedPush" bind-value="{{_repoConfig.enable_signed_push.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_signed_push)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> </gr-select> </span> </section> - <section - id="requireSignedPushSettings" - class$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]"> + <section id="requireSignedPushSettings" class\$="repositorySettings [[_computeRepositoriesClass(_repoConfig.require_signed_push)]]"> <span class="title">Require signed push</span> <span class="value"> - <gr-select - id="requireSignedPush" - bind-value="{{_repoConfig.require_signed_push.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]"> + <gr-select id="requireSignedPush" bind-value="{{_repoConfig.require_signed_push.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.require_signed_push)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -215,12 +165,9 @@ <span class="title"> Reject implicit merges when changes are pushed for review</span> <span class="value"> - <gr-select - id="rejectImplicitMergesSelect" - bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]"> + <gr-select id="rejectImplicitMergesSelect" bind-value="{{_repoConfig.reject_implicit_merges.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_implicit_merges)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -231,12 +178,9 @@ <span class="title"> Enable adding unregistered users as reviewers and CCs on changes</span> <span class="value"> - <gr-select - id="unRegisteredCcSelect" - bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]"> + <gr-select id="unRegisteredCcSelect" bind-value="{{_repoConfig.enable_reviewer_by_email.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.enable_reviewer_by_email)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -247,12 +191,9 @@ <span class="title"> Set all new changes private by default</span> <span class="value"> - <gr-select - id="setAllnewChangesPrivateByDefaultSelect" - bind-value="{{_repoConfig.private_by_default.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]"> + <gr-select id="setAllnewChangesPrivateByDefaultSelect" bind-value="{{_repoConfig.private_by_default.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.private_by_default)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -263,12 +204,9 @@ <span class="title"> Set new changes to "work in progress" by default</span> <span class="value"> - <gr-select - id="setAllNewChangesWorkInProgressByDefaultSelect" - bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]"> + <gr-select id="setAllNewChangesWorkInProgressByDefaultSelect" bind-value="{{_repoConfig.work_in_progress_by_default.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.work_in_progress_by_default)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -278,17 +216,8 @@ <section> <span class="title">Maximum Git object size limit</span> <span class="value"> - <iron-input - id="maxGitObjSizeIronInput" - bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" - type="text" - disabled$="[[_readOnly]]"> - <input - id="maxGitObjSizeInput" - bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" - is="iron-input" - type="text" - disabled$="[[_readOnly]]"> + <iron-input id="maxGitObjSizeIronInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" type="text" disabled\$="[[_readOnly]]"> + <input id="maxGitObjSizeInput" bind-value="{{_repoConfig.max_object_size_limit.configured_value}}" is="iron-input" type="text" disabled\$="[[_readOnly]]"> </iron-input> <template is="dom-if" if="[[_repoConfig.max_object_size_limit.value]]"> effective: [[_repoConfig.max_object_size_limit.value]] bytes @@ -298,12 +227,9 @@ <section> <span class="title">Match authored date with committer date upon submit</span> <span class="value"> - <gr-select - id="matchAuthoredDateWithCommitterDateSelect" - bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]"> + <gr-select id="matchAuthoredDateWithCommitterDateSelect" bind-value="{{_repoConfig.match_author_to_committer_date.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.match_author_to_committer_date)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -313,12 +239,9 @@ <section> <span class="title">Reject empty commit upon submit</span> <span class="value"> - <gr-select - id="rejectEmptyCommitSelect" - bind-value="{{_repoConfig.reject_empty_commit.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]"> + <gr-select id="rejectEmptyCommitSelect" bind-value="{{_repoConfig.reject_empty_commit.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.reject_empty_commit)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -332,12 +255,9 @@ <span class="title"> Require a valid contributor agreement to upload</span> <span class="value"> - <gr-select - id="contributorAgreementSelect" - bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]"> + <gr-select id="contributorAgreementSelect" bind-value="{{_repoConfig.use_contributor_agreements.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_contributor_agreements)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -347,12 +267,9 @@ <section> <span class="title">Require Signed-off-by in commit message</span> <span class="value"> - <gr-select - id="useSignedOffBySelect" - bind-value="{{_repoConfig.use_signed_off_by.configured_value}}"> - <select disabled$="[[_readOnly]]"> - <template is="dom-repeat" - items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]"> + <gr-select id="useSignedOffBySelect" bind-value="{{_repoConfig.use_signed_off_by.configured_value}}"> + <select disabled\$="[[_readOnly]]"> + <template is="dom-repeat" items="[[_formatBooleanSelect(_repoConfig.use_signed_off_by)]]"> <option value="[[item.value]]">[[item.label]]</option> </template> </select> @@ -360,18 +277,13 @@ </span> </section> </fieldset> - <div - class$="pluginConfig [[_computeHideClass(_pluginData)]]" - on-plugin-config-changed="_handlePluginConfigChanged"> + <div class\$="pluginConfig [[_computeHideClass(_pluginData)]]" on-plugin-config-changed="_handlePluginConfigChanged"> <h3>Plugins</h3> <template is="dom-repeat" items="[[_pluginData]]" as="data"> - <gr-repo-plugin-config - plugin-data="[[data]]"></gr-repo-plugin-config> + <gr-repo-plugin-config plugin-data="[[data]]"></gr-repo-plugin-config> </template> </div> - <gr-button - on-click="_handleSaveRepoConfig" - disabled$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button> + <gr-button on-click="_handleSaveRepoConfig" disabled\$="[[_computeButtonDisabled(_readOnly, _configChanged)]]">Save changes</gr-button> </fieldset> <gr-endpoint-decorator name="repo-config"> <gr-endpoint-param name="repoName" value="[[repo]]"></gr-endpoint-param> @@ -381,6 +293,4 @@ </div> </main> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html index da0c271..b9d5d56 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html +++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,367 +40,371 @@ </template> </test-fixture> -<script> - suite('gr-repo tests', async () => { - await readyToTest(); - let element; - let sandbox; - let repoStub; - const repoConf = { - description: 'Access inherited by all other projects.', - use_contributor_agreements: { - value: false, - configured_value: 'FALSE', - }, - use_content_merge: { - value: false, - configured_value: 'FALSE', - }, - use_signed_off_by: { - value: false, - configured_value: 'FALSE', - }, - create_new_change_for_all_not_in_target: { - value: false, - configured_value: 'FALSE', - }, - require_change_id: { - value: false, - configured_value: 'FALSE', - }, - enable_signed_push: { - value: false, - configured_value: 'FALSE', - }, - require_signed_push: { - value: false, - configured_value: 'FALSE', - }, - reject_implicit_merges: { - value: false, - configured_value: 'FALSE', - }, - private_by_default: { - value: false, - configured_value: 'FALSE', - }, - match_author_to_committer_date: { - value: false, - configured_value: 'FALSE', - }, - reject_empty_commit: { - value: false, - configured_value: 'FALSE', - }, - enable_reviewer_by_email: { - value: false, - configured_value: 'FALSE', - }, - max_object_size_limit: {}, - submit_type: 'MERGE_IF_NECESSARY', - default_submit_type: { - value: 'MERGE_IF_NECESSARY', - configured_value: 'INHERIT', - inherited_value: 'MERGE_IF_NECESSARY', - }, - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +suite('gr-repo tests', () => { + let element; + let sandbox; + let repoStub; + const repoConf = { + description: 'Access inherited by all other projects.', + use_contributor_agreements: { + value: false, + configured_value: 'FALSE', + }, + use_content_merge: { + value: false, + configured_value: 'FALSE', + }, + use_signed_off_by: { + value: false, + configured_value: 'FALSE', + }, + create_new_change_for_all_not_in_target: { + value: false, + configured_value: 'FALSE', + }, + require_change_id: { + value: false, + configured_value: 'FALSE', + }, + enable_signed_push: { + value: false, + configured_value: 'FALSE', + }, + require_signed_push: { + value: false, + configured_value: 'FALSE', + }, + reject_implicit_merges: { + value: false, + configured_value: 'FALSE', + }, + private_by_default: { + value: false, + configured_value: 'FALSE', + }, + match_author_to_committer_date: { + value: false, + configured_value: 'FALSE', + }, + reject_empty_commit: { + value: false, + configured_value: 'FALSE', + }, + enable_reviewer_by_email: { + value: false, + configured_value: 'FALSE', + }, + max_object_size_limit: {}, + submit_type: 'MERGE_IF_NECESSARY', + default_submit_type: { + value: 'MERGE_IF_NECESSARY', + configured_value: 'INHERIT', + inherited_value: 'MERGE_IF_NECESSARY', + }, + }; - const REPO = 'test-repo'; - const SCHEMES = {http: {}, repo: {}, ssh: {}}; + const REPO = 'test-repo'; + const SCHEMES = {http: {}, repo: {}, ssh: {}}; - function getFormFields() { - const selects = Array.from( - Polymer.dom(element.root).querySelectorAll('select')); - const textareas = Array.from( - Polymer.dom(element.root).querySelectorAll('iron-autogrow-textarea')); - const inputs = Array.from( - Polymer.dom(element.root).querySelectorAll('input')); - return inputs.concat(textareas).concat(selects); - } + function getFormFields() { + const selects = Array.from( + dom(element.root).querySelectorAll('select')); + const textareas = Array.from( + dom(element.root).querySelectorAll('iron-autogrow-textarea')); + const inputs = Array.from( + dom(element.root).querySelectorAll('input')); + return inputs.concat(textareas).concat(selects); + } - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - getConfig() { - return Promise.resolve({download: {}}); - }, - }); - element = fixture('basic'); - repoStub = sandbox.stub( - element.$.restAPI, - 'getProjectConfig', - () => Promise.resolve(repoConf)); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + getConfig() { + return Promise.resolve({download: {}}); + }, }); + element = fixture('basic'); + repoStub = sandbox.stub( + element.$.restAPI, + 'getProjectConfig', + () => Promise.resolve(repoConf)); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('_computePluginData', () => { - assert.deepEqual(element._computePluginData(), []); - assert.deepEqual(element._computePluginData({}), []); - assert.deepEqual(element._computePluginData({base: {}}), []); - assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}), - [{name: 'plugin', config: 'data'}]); - }); + test('_computePluginData', () => { + assert.deepEqual(element._computePluginData(), []); + assert.deepEqual(element._computePluginData({}), []); + assert.deepEqual(element._computePluginData({base: {}}), []); + assert.deepEqual(element._computePluginData({base: {plugin: 'data'}}), + [{name: 'plugin', config: 'data'}]); + }); - test('_handlePluginConfigChanged', () => { - const notifyStub = sandbox.stub(element, 'notifyPath'); - element._repoConfig = {plugin_config: {}}; - element._handlePluginConfigChanged({detail: { - name: 'test', - config: 'data', - notifyPath: 'path', - }}); - flushAsynchronousOperations(); + test('_handlePluginConfigChanged', () => { + const notifyStub = sandbox.stub(element, 'notifyPath'); + element._repoConfig = {plugin_config: {}}; + element._handlePluginConfigChanged({detail: { + name: 'test', + config: 'data', + notifyPath: 'path', + }}); + flushAsynchronousOperations(); - assert.equal(element._repoConfig.plugin_config.test, 'data'); - assert.equal(notifyStub.lastCall.args[0], - '_repoConfig.plugin_config.path'); - }); + assert.equal(element._repoConfig.plugin_config.test, 'data'); + assert.equal(notifyStub.lastCall.args[0], + '_repoConfig.plugin_config.path'); + }); - test('loading displays before repo config is loaded', () => { - assert.isTrue(element.$.loading.classList.contains('loading')); - assert.isFalse(getComputedStyle(element.$.loading).display === 'none'); - assert.isTrue(element.$.loadedContent.classList.contains('loading')); - assert.isTrue(getComputedStyle(element.$.loadedContent) - .display === 'none'); - }); + test('loading displays before repo config is loaded', () => { + assert.isTrue(element.$.loading.classList.contains('loading')); + assert.isFalse(getComputedStyle(element.$.loading).display === 'none'); + assert.isTrue(element.$.loadedContent.classList.contains('loading')); + assert.isTrue(getComputedStyle(element.$.loadedContent) + .display === 'none'); + }); - test('download commands visibility', () => { - element._loading = false; - flushAsynchronousOperations(); - assert.isTrue(element.$.downloadContent.classList.contains('hide')); - assert.isTrue(getComputedStyle(element.$.downloadContent) - .display == 'none'); - element._schemesObj = SCHEMES; - flushAsynchronousOperations(); - assert.isFalse(element.$.downloadContent.classList.contains('hide')); - assert.isFalse(getComputedStyle(element.$.downloadContent) - .display == 'none'); - }); + test('download commands visibility', () => { + element._loading = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.downloadContent.classList.contains('hide')); + assert.isTrue(getComputedStyle(element.$.downloadContent) + .display == 'none'); + element._schemesObj = SCHEMES; + flushAsynchronousOperations(); + assert.isFalse(element.$.downloadContent.classList.contains('hide')); + assert.isFalse(getComputedStyle(element.$.downloadContent) + .display == 'none'); + }); - test('form defaults to read only', () => { + test('form defaults to read only', () => { + assert.isTrue(element._readOnly); + }); + + test('form defaults to read only when not logged in', done => { + element.repo = REPO; + element._loadRepo().then(() => { assert.isTrue(element._readOnly); + done(); + }); + }); + + test('form defaults to read only when logged in and not admin', done => { + element.repo = REPO; + sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true)); + sandbox.stub( + element.$.restAPI, + 'getRepoAccess', + () => Promise.resolve({'test-repo': {}})); + element._loadRepo().then(() => { + assert.isTrue(element._readOnly); + done(); + }); + }); + + test('all form elements are disabled when not admin', done => { + element.repo = REPO; + element._loadRepo().then(() => { + flushAsynchronousOperations(); + const formFields = getFormFields(); + for (const field of formFields) { + assert.isTrue(field.hasAttribute('disabled')); + } + done(); + }); + }); + + test('_formatBooleanSelect', () => { + let item = {inherited_value: true}; + assert.deepEqual(element._formatBooleanSelect(item), [ + { + label: 'Inherit (true)', + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]); + + item = {inherited_value: false}; + assert.deepEqual(element._formatBooleanSelect(item), [ + { + label: 'Inherit (false)', + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]); + + // For items without inherited values + item = {}; + assert.deepEqual(element._formatBooleanSelect(item), [ + { + label: 'Inherit', + value: 'INHERIT', + }, + { + label: 'True', + value: 'TRUE', + }, { + label: 'False', + value: 'FALSE', + }, + ]); + }); + + test('fires page-error', done => { + repoStub.restore(); + + element.repo = 'test'; + + const response = {status: 404}; + sandbox.stub( + element.$.restAPI, 'getProjectConfig', (repo, errFn) => { + errFn(response); + }); + element.addEventListener('page-error', e => { + assert.deepEqual(e.detail.response, response); + done(); }); - test('form defaults to read only when not logged in', done => { - element.repo = REPO; - element._loadRepo().then(() => { - assert.isTrue(element._readOnly); - done(); - }); - }); + element._loadRepo(); + }); - test('form defaults to read only when logged in and not admin', done => { + suite('admin', () => { + setup(() => { element.repo = REPO; sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true)); sandbox.stub( element.$.restAPI, 'getRepoAccess', - () => Promise.resolve({'test-repo': {}})); - element._loadRepo().then(() => { - assert.isTrue(element._readOnly); - done(); - }); + () => Promise.resolve({'test-repo': {is_owner: true}})); }); - test('all form elements are disabled when not admin', done => { - element.repo = REPO; + test('all form elements are enabled', done => { element._loadRepo().then(() => { flushAsynchronousOperations(); const formFields = getFormFields(); for (const field of formFields) { - assert.isTrue(field.hasAttribute('disabled')); + assert.isFalse(field.hasAttribute('disabled')); } + assert.isFalse(element._loading); done(); }); }); - test('_formatBooleanSelect', () => { - let item = {inherited_value: true}; - assert.deepEqual(element._formatBooleanSelect(item), [ - { - label: 'Inherit (true)', - value: 'INHERIT', - }, - { - label: 'True', - value: 'TRUE', - }, { - label: 'False', - value: 'FALSE', - }, - ]); - - item = {inherited_value: false}; - assert.deepEqual(element._formatBooleanSelect(item), [ - { - label: 'Inherit (false)', - value: 'INHERIT', - }, - { - label: 'True', - value: 'TRUE', - }, { - label: 'False', - value: 'FALSE', - }, - ]); - - // For items without inherited values - item = {}; - assert.deepEqual(element._formatBooleanSelect(item), [ - { - label: 'Inherit', - value: 'INHERIT', - }, - { - label: 'True', - value: 'TRUE', - }, { - label: 'False', - value: 'FALSE', - }, - ]); + test('state gets set correctly', done => { + element._loadRepo().then(() => { + assert.equal(element._repoConfig.state, 'ACTIVE'); + assert.equal(element.$.stateSelect.bindValue, 'ACTIVE'); + done(); + }); }); - test('fires page-error', done => { - repoStub.restore(); - - element.repo = 'test'; - - const response = {status: 404}; - sandbox.stub( - element.$.restAPI, 'getProjectConfig', (repo, errFn) => { - errFn(response); + test('inherited submit type value is calculated correctly', done => { + element + ._loadRepo().then(() => { + const sel = element.$.submitTypeSelect; + assert.equal(sel.bindValue, 'INHERIT'); + assert.equal( + sel.nativeSelect.options[0].text, + 'Inherit (Merge if necessary)' + ); + done(); }); - element.addEventListener('page-error', e => { - assert.deepEqual(e.detail.response, response); - done(); - }); - - element._loadRepo(); }); - suite('admin', () => { - setup(() => { - element.repo = REPO; - sandbox.stub(element, '_getLoggedIn', () => Promise.resolve(true)); - sandbox.stub( - element.$.restAPI, - 'getRepoAccess', - () => Promise.resolve({'test-repo': {is_owner: true}})); - }); + test('fields update and save correctly', () => { + const configInputObj = { + description: 'new description', + use_contributor_agreements: 'TRUE', + use_content_merge: 'TRUE', + use_signed_off_by: 'TRUE', + create_new_change_for_all_not_in_target: 'TRUE', + require_change_id: 'TRUE', + enable_signed_push: 'TRUE', + require_signed_push: 'TRUE', + reject_implicit_merges: 'TRUE', + private_by_default: 'TRUE', + match_author_to_committer_date: 'TRUE', + reject_empty_commit: 'TRUE', + max_object_size_limit: 10, + submit_type: 'FAST_FORWARD_ONLY', + state: 'READ_ONLY', + enable_reviewer_by_email: 'TRUE', + }; - test('all form elements are enabled', done => { - element._loadRepo().then(() => { - flushAsynchronousOperations(); - const formFields = getFormFields(); - for (const field of formFields) { - assert.isFalse(field.hasAttribute('disabled')); - } - assert.isFalse(element._loading); - done(); - }); - }); + const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig' + , () => Promise.resolve({})); - test('state gets set correctly', done => { - element._loadRepo().then(() => { - assert.equal(element._repoConfig.state, 'ACTIVE'); - assert.equal(element.$.stateSelect.bindValue, 'ACTIVE'); - done(); - }); - }); + const button = dom(element.root).querySelector('gr-button'); - test('inherited submit type value is calculated correctly', done => { - element - ._loadRepo().then(() => { - const sel = element.$.submitTypeSelect; - assert.equal(sel.bindValue, 'INHERIT'); - assert.equal( - sel.nativeSelect.options[0].text, - 'Inherit (Merge if necessary)' - ); - done(); - }); - }); + return element._loadRepo().then(() => { + assert.isTrue(button.hasAttribute('disabled')); + assert.isFalse(element.$.Title.classList.contains('edited')); + element.$.descriptionInput.bindValue = configInputObj.description; + element.$.stateSelect.bindValue = configInputObj.state; + element.$.submitTypeSelect.bindValue = configInputObj.submit_type; + element.$.contentMergeSelect.bindValue = + configInputObj.use_content_merge; + element.$.newChangeSelect.bindValue = + configInputObj.create_new_change_for_all_not_in_target; + element.$.requireChangeIdSelect.bindValue = + configInputObj.require_change_id; + element.$.enableSignedPush.bindValue = + configInputObj.enable_signed_push; + element.$.requireSignedPush.bindValue = + configInputObj.require_signed_push; + element.$.rejectImplicitMergesSelect.bindValue = + configInputObj.reject_implicit_merges; + element.$.setAllnewChangesPrivateByDefaultSelect.bindValue = + configInputObj.private_by_default; + element.$.matchAuthoredDateWithCommitterDateSelect.bindValue = + configInputObj.match_author_to_committer_date; + const inputElement = PolymerElement ? + element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput; + inputElement.bindValue = configInputObj.max_object_size_limit; + element.$.contributorAgreementSelect.bindValue = + configInputObj.use_contributor_agreements; + element.$.useSignedOffBySelect.bindValue = + configInputObj.use_signed_off_by; + element.$.rejectEmptyCommitSelect.bindValue = + configInputObj.reject_empty_commit; + element.$.unRegisteredCcSelect.bindValue = + configInputObj.enable_reviewer_by_email; - test('fields update and save correctly', () => { - const configInputObj = { - description: 'new description', - use_contributor_agreements: 'TRUE', - use_content_merge: 'TRUE', - use_signed_off_by: 'TRUE', - create_new_change_for_all_not_in_target: 'TRUE', - require_change_id: 'TRUE', - enable_signed_push: 'TRUE', - require_signed_push: 'TRUE', - reject_implicit_merges: 'TRUE', - private_by_default: 'TRUE', - match_author_to_committer_date: 'TRUE', - reject_empty_commit: 'TRUE', - max_object_size_limit: 10, - submit_type: 'FAST_FORWARD_ONLY', - state: 'READ_ONLY', - enable_reviewer_by_email: 'TRUE', - }; + assert.isFalse(button.hasAttribute('disabled')); + assert.isTrue(element.$.configurations.classList.contains('edited')); - const saveStub = sandbox.stub(element.$.restAPI, 'saveRepoConfig' - , () => Promise.resolve({})); + const formattedObj = + element._formatRepoConfigForSave(element._repoConfig); + assert.deepEqual(formattedObj, configInputObj); - const button = Polymer.dom(element.root).querySelector('gr-button'); - - return element._loadRepo().then(() => { + return element._handleSaveRepoConfig().then(() => { assert.isTrue(button.hasAttribute('disabled')); assert.isFalse(element.$.Title.classList.contains('edited')); - element.$.descriptionInput.bindValue = configInputObj.description; - element.$.stateSelect.bindValue = configInputObj.state; - element.$.submitTypeSelect.bindValue = configInputObj.submit_type; - element.$.contentMergeSelect.bindValue = - configInputObj.use_content_merge; - element.$.newChangeSelect.bindValue = - configInputObj.create_new_change_for_all_not_in_target; - element.$.requireChangeIdSelect.bindValue = - configInputObj.require_change_id; - element.$.enableSignedPush.bindValue = - configInputObj.enable_signed_push; - element.$.requireSignedPush.bindValue = - configInputObj.require_signed_push; - element.$.rejectImplicitMergesSelect.bindValue = - configInputObj.reject_implicit_merges; - element.$.setAllnewChangesPrivateByDefaultSelect.bindValue = - configInputObj.private_by_default; - element.$.matchAuthoredDateWithCommitterDateSelect.bindValue = - configInputObj.match_author_to_committer_date; - const inputElement = Polymer.Element ? - element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput; - inputElement.bindValue = configInputObj.max_object_size_limit; - element.$.contributorAgreementSelect.bindValue = - configInputObj.use_contributor_agreements; - element.$.useSignedOffBySelect.bindValue = - configInputObj.use_signed_off_by; - element.$.rejectEmptyCommitSelect.bindValue = - configInputObj.reject_empty_commit; - element.$.unRegisteredCcSelect.bindValue = - configInputObj.enable_reviewer_by_email; - - assert.isFalse(button.hasAttribute('disabled')); - assert.isTrue(element.$.configurations.classList.contains('edited')); - - const formattedObj = - element._formatRepoConfigForSave(element._repoConfig); - assert.deepEqual(formattedObj, configInputObj); - - return element._handleSaveRepoConfig().then(() => { - assert.isTrue(button.hasAttribute('disabled')); - assert.isFalse(element.$.Title.classList.contains('edited')); - assert.isTrue(saveStub.lastCall.calledWithExactly(REPO, - configInputObj)); - }); + assert.isTrue(saveStub.lastCall.calledWithExactly(REPO, + configInputObj)); }); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js index ac98d33..2421c46 100644 --- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js +++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor.js
@@ -14,270 +14,286 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-access-behavior/gr-access-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-select/gr-select.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-rule-editor_html.js'; + +/** + * Fired when the rule has been modified or removed. + * + * @event access-modified + */ + +/** + * Fired when a rule that was previously added was removed. + * + * @event added-rule-removed + */ + +const PRIORITY_OPTIONS = [ + 'BATCH', + 'INTERACTIVE', +]; + +const Action = { + ALLOW: 'ALLOW', + DENY: 'DENY', + BLOCK: 'BLOCK', +}; + +const DROPDOWN_OPTIONS = [Action.ALLOW, Action.DENY, Action.BLOCK]; + +const ForcePushOptions = { + ALLOW: [ + {name: 'Allow pushing (but not force pushing)', value: false}, + {name: 'Allow pushing with or without force', value: true}, + ], + BLOCK: [ + {name: 'Block pushing with or without force', value: false}, + {name: 'Block force pushing', value: true}, + ], +}; + +const FORCE_EDIT_OPTIONS = [ + { + name: 'No Force Edit', + value: false, + }, + { + name: 'Force Edit', + value: true, + }, +]; + +/** + * @appliesMixin Gerrit.AccessMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrRuleEditor extends mixinBehaviors( [ + Gerrit.AccessBehavior, + Gerrit.BaseUrlBehavior, /** - * Fired when the rule has been modified or removed. - * - * @event access-modified + * Unused in this element, but called by other elements in tests + * e.g gr-permission_test. */ + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** - * Fired when a rule that was previously added was removed. - * - * @event added-rule-removed - */ + static get is() { return 'gr-rule-editor'; } - const PRIORITY_OPTIONS = [ - 'BATCH', - 'INTERACTIVE', - ]; + static get properties() { + return { + hasRange: Boolean, + /** @type {?} */ + label: Object, + editing: { + type: Boolean, + value: false, + observer: '_handleEditingChanged', + }, + groupId: String, + groupName: String, + permission: String, + /** @type {?} */ + rule: { + type: Object, + notify: true, + }, + section: String, - const Action = { - ALLOW: 'ALLOW', - DENY: 'DENY', - BLOCK: 'BLOCK', - }; + _deleted: { + type: Boolean, + value: false, + }, + _originalRuleValues: Object, + }; + } - const DROPDOWN_OPTIONS = [Action.ALLOW, Action.DENY, Action.BLOCK]; + static get observers() { + return [ + '_handleValueChange(rule.value.*)', + ]; + } - const ForcePushOptions = { - ALLOW: [ - {name: 'Allow pushing (but not force pushing)', value: false}, - {name: 'Allow pushing with or without force', value: true}, - ], - BLOCK: [ - {name: 'Block pushing with or without force', value: false}, - {name: 'Block force pushing', value: true}, - ], - }; + /** @override */ + created() { + super.created(); + this.addEventListener('access-saved', + () => this._handleAccessSaved()); + } - const FORCE_EDIT_OPTIONS = [ - { - name: 'No Force Edit', - value: false, - }, - { - name: 'Force Edit', - value: true, - }, - ]; + /** @override */ + ready() { + super.ready(); + // Called on ready rather than the observer because when new rules are + // added, the observer is triggered prior to being ready. + if (!this.rule) { return; } // Check needed for test purposes. + this._setupValues(this.rule); + } - /** - * @appliesMixin Gerrit.AccessMixin - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrRuleEditor extends Polymer.mixinBehaviors( [ - Gerrit.AccessBehavior, - Gerrit.BaseUrlBehavior, - /** - * Unused in this element, but called by other elements in tests - * e.g gr-permission_test. - */ - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-rule-editor'; } - - static get properties() { - return { - hasRange: Boolean, - /** @type {?} */ - label: Object, - editing: { - type: Boolean, - value: false, - observer: '_handleEditingChanged', - }, - groupId: String, - groupName: String, - permission: String, - /** @type {?} */ - rule: { - type: Object, - notify: true, - }, - section: String, - - _deleted: { - type: Boolean, - value: false, - }, - _originalRuleValues: Object, - }; - } - - static get observers() { - return [ - '_handleValueChange(rule.value.*)', - ]; - } - - /** @override */ - created() { - super.created(); - this.addEventListener('access-saved', - () => this._handleAccessSaved()); - } - - /** @override */ - ready() { - super.ready(); - // Called on ready rather than the observer because when new rules are - // added, the observer is triggered prior to being ready. - if (!this.rule) { return; } // Check needed for test purposes. - this._setupValues(this.rule); - } - - /** @override */ - attached() { - super.attached(); - if (!this.rule) { return; } // Check needed for test purposes. - if (!this._originalRuleValues) { - // Observer _handleValueChange is called after the ready() - // method finishes. Original values must be set later to - // avoid set .modified flag to true - this._setOriginalRuleValues(this.rule.value); - } - } - - _setupValues(rule) { - if (!rule.value) { - this._setDefaultRuleValues(); - } - } - - _computeForce(permission, action) { - if (this.permissionValues.push.id === permission && - action !== Action.DENY) { - return true; - } - - return this.permissionValues.editTopicName.id === permission; - } - - _computeForceClass(permission, action) { - return this._computeForce(permission, action) ? 'force' : ''; - } - - _computeGroupPath(group) { - return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`; - } - - _handleAccessSaved() { - // Set a new 'original' value to keep track of after the value has been - // saved. + /** @override */ + attached() { + super.attached(); + if (!this.rule) { return; } // Check needed for test purposes. + if (!this._originalRuleValues) { + // Observer _handleValueChange is called after the ready() + // method finishes. Original values must be set later to + // avoid set .modified flag to true this._setOriginalRuleValues(this.rule.value); } - - _handleEditingChanged(editing, editingOld) { - // Ignore when editing gets set initially. - if (!editingOld) { return; } - // Restore original values if no longer editing. - if (!editing) { - this._handleUndoChange(); - } - } - - _computeSectionClass(editing, deleted) { - const classList = []; - if (editing) { - classList.push('editing'); - } - if (deleted) { - classList.push('deleted'); - } - return classList.join(' '); - } - - _computeForceOptions(permission, action) { - if (permission === this.permissionValues.push.id) { - if (action === Action.ALLOW) { - return ForcePushOptions.ALLOW; - } else if (action === Action.BLOCK) { - return ForcePushOptions.BLOCK; - } else { - return []; - } - } else if (permission === this.permissionValues.editTopicName.id) { - return FORCE_EDIT_OPTIONS; - } - return []; - } - - _getDefaultRuleValues(permission, label) { - const ruleAction = Action.ALLOW; - const value = {}; - if (permission === 'priority') { - value.action = PRIORITY_OPTIONS[0]; - return value; - } else if (label) { - value.min = label.values[0].value; - value.max = label.values[label.values.length - 1].value; - } else if (this._computeForce(permission, ruleAction)) { - value.force = - this._computeForceOptions(permission, ruleAction)[0].value; - } - value.action = DROPDOWN_OPTIONS[0]; - return value; - } - - _setDefaultRuleValues() { - this.set('rule.value', this._getDefaultRuleValues(this.permission, - this.label)); - } - - _computeOptions(permission) { - if (permission === 'priority') { - return PRIORITY_OPTIONS; - } - return DROPDOWN_OPTIONS; - } - - _handleRemoveRule() { - if (this.rule.value.added) { - this.dispatchEvent(new CustomEvent( - 'added-rule-removed', {bubbles: true, composed: true})); - } - this._deleted = true; - this.rule.value.deleted = true; - this.dispatchEvent( - new CustomEvent('access-modified', {bubbles: true, composed: true})); - } - - _handleUndoRemove() { - this._deleted = false; - delete this.rule.value.deleted; - } - - _handleUndoChange() { - // gr-permission will take care of removing rules that were added but - // unsaved. We need to keep the added bit for the filter. - if (this.rule.value.added) { return; } - this.set('rule.value', Object.assign({}, this._originalRuleValues)); - this._deleted = false; - delete this.rule.value.deleted; - delete this.rule.value.modified; - } - - _handleValueChange() { - if (!this._originalRuleValues) { return; } - this.rule.value.modified = true; - // Allows overall access page to know a change has been made. - this.dispatchEvent( - new CustomEvent('access-modified', {bubbles: true, composed: true})); - } - - _setOriginalRuleValues(value) { - this._originalRuleValues = Object.assign({}, value); - } } - customElements.define(GrRuleEditor.is, GrRuleEditor); -})(); + _setupValues(rule) { + if (!rule.value) { + this._setDefaultRuleValues(); + } + } + + _computeForce(permission, action) { + if (this.permissionValues.push.id === permission && + action !== Action.DENY) { + return true; + } + + return this.permissionValues.editTopicName.id === permission; + } + + _computeForceClass(permission, action) { + return this._computeForce(permission, action) ? 'force' : ''; + } + + _computeGroupPath(group) { + return `${this.getBaseUrl()}/admin/groups/${this.encodeURL(group, true)}`; + } + + _handleAccessSaved() { + // Set a new 'original' value to keep track of after the value has been + // saved. + this._setOriginalRuleValues(this.rule.value); + } + + _handleEditingChanged(editing, editingOld) { + // Ignore when editing gets set initially. + if (!editingOld) { return; } + // Restore original values if no longer editing. + if (!editing) { + this._handleUndoChange(); + } + } + + _computeSectionClass(editing, deleted) { + const classList = []; + if (editing) { + classList.push('editing'); + } + if (deleted) { + classList.push('deleted'); + } + return classList.join(' '); + } + + _computeForceOptions(permission, action) { + if (permission === this.permissionValues.push.id) { + if (action === Action.ALLOW) { + return ForcePushOptions.ALLOW; + } else if (action === Action.BLOCK) { + return ForcePushOptions.BLOCK; + } else { + return []; + } + } else if (permission === this.permissionValues.editTopicName.id) { + return FORCE_EDIT_OPTIONS; + } + return []; + } + + _getDefaultRuleValues(permission, label) { + const ruleAction = Action.ALLOW; + const value = {}; + if (permission === 'priority') { + value.action = PRIORITY_OPTIONS[0]; + return value; + } else if (label) { + value.min = label.values[0].value; + value.max = label.values[label.values.length - 1].value; + } else if (this._computeForce(permission, ruleAction)) { + value.force = + this._computeForceOptions(permission, ruleAction)[0].value; + } + value.action = DROPDOWN_OPTIONS[0]; + return value; + } + + _setDefaultRuleValues() { + this.set('rule.value', this._getDefaultRuleValues(this.permission, + this.label)); + } + + _computeOptions(permission) { + if (permission === 'priority') { + return PRIORITY_OPTIONS; + } + return DROPDOWN_OPTIONS; + } + + _handleRemoveRule() { + if (this.rule.value.added) { + this.dispatchEvent(new CustomEvent( + 'added-rule-removed', {bubbles: true, composed: true})); + } + this._deleted = true; + this.rule.value.deleted = true; + this.dispatchEvent( + new CustomEvent('access-modified', {bubbles: true, composed: true})); + } + + _handleUndoRemove() { + this._deleted = false; + delete this.rule.value.deleted; + } + + _handleUndoChange() { + // gr-permission will take care of removing rules that were added but + // unsaved. We need to keep the added bit for the filter. + if (this.rule.value.added) { return; } + this.set('rule.value', Object.assign({}, this._originalRuleValues)); + this._deleted = false; + delete this.rule.value.deleted; + delete this.rule.value.modified; + } + + _handleValueChange() { + if (!this._originalRuleValues) { return; } + this.rule.value.modified = true; + // Allows overall access page to know a change has been made. + this.dispatchEvent( + new CustomEvent('access-modified', {bubbles: true, composed: true})); + } + + _setOriginalRuleValues(value) { + this._originalRuleValues = Object.assign({}, value); + } +} + +customElements.define(GrRuleEditor.is, GrRuleEditor);
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js index 9820e31..4ea13b1 100644 --- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js +++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_html.js
@@ -1,35 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-access-behavior/gr-access-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-rule-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { border-bottom: 1px solid var(--border-color); @@ -79,34 +66,25 @@ width: 14em; } </style> - <div id="mainContainer" - class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"> + <div id="mainContainer" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"> <div id="options"> - <gr-select id="action" - bind-value="{{rule.value.action}}" - on-change="_handleValueChange"> - <select disabled$="[[!editing]]"> + <gr-select id="action" bind-value="{{rule.value.action}}" on-change="_handleValueChange"> + <select disabled\$="[[!editing]]"> <template is="dom-repeat" items="[[_computeOptions(permission)]]"> <option value="[[item]]">[[item]]</option> </template> </select> </gr-select> <template is="dom-if" if="[[label]]"> - <gr-select - id="labelMin" - bind-value="{{rule.value.min}}" - on-change="_handleValueChange"> - <select disabled$="[[!editing]]"> + <gr-select id="labelMin" bind-value="{{rule.value.min}}" on-change="_handleValueChange"> + <select disabled\$="[[!editing]]"> <template is="dom-repeat" items="[[label.values]]"> <option value="[[item.value]]">[[item.value]]</option> </template> </select> </gr-select> - <gr-select - id="labelMax" - bind-value="{{rule.value.max}}" - on-change="_handleValueChange"> - <select disabled$="[[!editing]]"> + <gr-select id="labelMax" bind-value="{{rule.value.max}}" on-change="_handleValueChange"> + <select disabled\$="[[!editing]]"> <template is="dom-repeat" items="[[label.values]]"> <option value="[[item.value]]">[[item.value]]</option> </template> @@ -114,51 +92,25 @@ </gr-select> </template> <template is="dom-if" if="[[hasRange]]"> - <iron-autogrow-textarea - id="minInput" - class="min" - autocomplete="on" - placeholder="Min value" - bind-value="{{rule.value.min}}" - disabled$="[[!editing]]"></iron-autogrow-textarea> - <iron-autogrow-textarea - id="maxInput" - class="max" - autocomplete="on" - placeholder="Max value" - bind-value="{{rule.value.max}}" - disabled$="[[!editing]]"></iron-autogrow-textarea> + <iron-autogrow-textarea id="minInput" class="min" autocomplete="on" placeholder="Min value" bind-value="{{rule.value.min}}" disabled\$="[[!editing]]"></iron-autogrow-textarea> + <iron-autogrow-textarea id="maxInput" class="max" autocomplete="on" placeholder="Max value" bind-value="{{rule.value.max}}" disabled\$="[[!editing]]"></iron-autogrow-textarea> </template> - <a class="groupPath" href$="[[_computeGroupPath(groupId)]]"> + <a class="groupPath" href\$="[[_computeGroupPath(groupId)]]"> [[groupName]] </a> - <gr-select - id="force" - class$="[[_computeForceClass(permission, rule.value.action)]]" - bind-value="{{rule.value.force}}" - on-change="_handleValueChange"> - <select disabled$="[[!editing]]"> - <template - is="dom-repeat" - items="[[_computeForceOptions(permission, rule.value.action)]]"> + <gr-select id="force" class\$="[[_computeForceClass(permission, rule.value.action)]]" bind-value="{{rule.value.force}}" on-change="_handleValueChange"> + <select disabled\$="[[!editing]]"> + <template is="dom-repeat" items="[[_computeForceOptions(permission, rule.value.action)]]"> <option value="[[item.value]]">[[item.name]]</option> </template> </select> </gr-select> </div> - <gr-button - link - id="removeBtn" - on-click="_handleRemoveRule">Remove</gr-button> + <gr-button link="" id="removeBtn" on-click="_handleRemoveRule">Remove</gr-button> </div> - <div - id="deletedContainer" - class$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"> + <div id="deletedContainer" class\$="gr-form-styles [[_computeSectionClass(editing, _deleted)]]"> [[groupName]] was deleted - <gr-button link - id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button> + <gr-button link="" id="undoRemoveBtn" on-click="_handleUndoRemove">Undo</gr-button> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-rule-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html index 9f02ddc..42e5f32 100644 --- a/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html +++ b/polygerrit-ui/app/elements/admin/gr-rule-editor/gr-rule-editor_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rule-editor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-rule-editor.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-rule-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-rule-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,593 +41,596 @@ </template> </test-fixture> -<script> - suite('gr-rule-editor tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-rule-editor.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-rule-editor tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - suite('unit tests', () => { - test('_computeForce, _computeForceClass, and _computeForceOptions', - () => { - const ForcePushOptions = { - ALLOW: [ - {name: 'Allow pushing (but not force pushing)', value: false}, - {name: 'Allow pushing with or without force', value: true}, - ], - BLOCK: [ - {name: 'Block pushing with or without force', value: false}, - {name: 'Block force pushing', value: true}, - ], - }; + suite('unit tests', () => { + test('_computeForce, _computeForceClass, and _computeForceOptions', + () => { + const ForcePushOptions = { + ALLOW: [ + {name: 'Allow pushing (but not force pushing)', value: false}, + {name: 'Allow pushing with or without force', value: true}, + ], + BLOCK: [ + {name: 'Block pushing with or without force', value: false}, + {name: 'Block force pushing', value: true}, + ], + }; - const FORCE_EDIT_OPTIONS = [ - { - name: 'No Force Edit', - value: false, - }, - { - name: 'Force Edit', - value: true, - }, - ]; - let permission = 'push'; - let action = 'ALLOW'; - assert.isTrue(element._computeForce(permission, action)); - assert.equal(element._computeForceClass(permission, action), - 'force'); - assert.deepEqual(element._computeForceOptions(permission, action), - ForcePushOptions.ALLOW); + const FORCE_EDIT_OPTIONS = [ + { + name: 'No Force Edit', + value: false, + }, + { + name: 'Force Edit', + value: true, + }, + ]; + let permission = 'push'; + let action = 'ALLOW'; + assert.isTrue(element._computeForce(permission, action)); + assert.equal(element._computeForceClass(permission, action), + 'force'); + assert.deepEqual(element._computeForceOptions(permission, action), + ForcePushOptions.ALLOW); - action = 'BLOCK'; - assert.isTrue(element._computeForce(permission, action)); - assert.equal(element._computeForceClass(permission, action), - 'force'); - assert.deepEqual(element._computeForceOptions(permission, action), - ForcePushOptions.BLOCK); + action = 'BLOCK'; + assert.isTrue(element._computeForce(permission, action)); + assert.equal(element._computeForceClass(permission, action), + 'force'); + assert.deepEqual(element._computeForceOptions(permission, action), + ForcePushOptions.BLOCK); - action = 'DENY'; - assert.isFalse(element._computeForce(permission, action)); - assert.equal(element._computeForceClass(permission, action), ''); - assert.equal( - element._computeForceOptions(permission, action).length, 0); - - permission = 'editTopicName'; - assert.isTrue(element._computeForce(permission)); - assert.equal(element._computeForceClass(permission), 'force'); - assert.deepEqual(element._computeForceOptions(permission), - FORCE_EDIT_OPTIONS); - permission = 'submit'; - assert.isFalse(element._computeForce(permission)); - assert.equal(element._computeForceClass(permission), ''); - assert.deepEqual(element._computeForceOptions(permission), []); - }); - - test('_computeSectionClass', () => { - let deleted = true; - let editing = false; - assert.equal(element._computeSectionClass(editing, deleted), 'deleted'); - - deleted = false; - assert.equal(element._computeSectionClass(editing, deleted), ''); - - editing = true; - assert.equal(element._computeSectionClass(editing, deleted), 'editing'); - - deleted = true; - assert.equal(element._computeSectionClass(editing, deleted), - 'editing deleted'); - }); - - test('_getDefaultRuleValues', () => { - let permission = 'priority'; - let label; - assert.deepEqual(element._getDefaultRuleValues(permission, label), - {action: 'BATCH'}); - permission = 'label-Code-Review'; - label = {values: [ - {value: -2, text: 'This shall not be merged'}, - {value: -1, text: 'I would prefer this is not merged as is'}, - {value: -0, text: 'No score'}, - {value: 1, text: 'Looks good to me, but someone else must approve'}, - {value: 2, text: 'Looks good to me, approved'}, - ]}; - assert.deepEqual(element._getDefaultRuleValues(permission, label), - {action: 'ALLOW', max: 2, min: -2}); - permission = 'push'; - label = undefined; - assert.deepEqual(element._getDefaultRuleValues(permission, label), - {action: 'ALLOW', force: false}); - permission = 'submit'; - assert.deepEqual(element._getDefaultRuleValues(permission, label), - {action: 'ALLOW'}); - }); - - test('_setDefaultRuleValues', () => { - element.rule = {id: 123}; - const defaultValue = {action: 'ALLOW'}; - sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue); - element._setDefaultRuleValues(); - assert.isTrue(element._getDefaultRuleValues.called); - assert.equal(element.rule.value, defaultValue); - }); - - test('_computeOptions', () => { - const PRIORITY_OPTIONS = [ - 'BATCH', - 'INTERACTIVE', - ]; - const DROPDOWN_OPTIONS = [ - 'ALLOW', - 'DENY', - 'BLOCK', - ]; - let permission = 'priority'; - assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS); - permission = 'submit'; - assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS); - }); - - test('_handleValueChange', () => { - const modifiedHandler = sandbox.stub(); - element.rule = {value: {}}; - element.addEventListener('access-modified', modifiedHandler); - element._handleValueChange(); - assert.isNotOk(element.rule.value.modified); - element._originalRuleValues = {}; - element._handleValueChange(); - assert.isTrue(element.rule.value.modified); - assert.isTrue(modifiedHandler.called); - }); - - test('_handleAccessSaved', () => { - const originalValue = {action: 'DENY'}; - const newValue = {action: 'ALLOW'}; - element._originalRuleValues = originalValue; - element.rule = {value: newValue}; - element._handleAccessSaved(); - assert.deepEqual(element._originalRuleValues, newValue); - }); - - test('_setOriginalRuleValues', () => { - const value = { - action: 'ALLOW', - force: false, - }; - element._setOriginalRuleValues(value); - assert.deepEqual(element._originalRuleValues, value); - }); - }); - - suite('already existing generic rule', () => { - setup(done => { - element.group = 'Group Name'; - element.permission = 'submit'; - element.rule = { - id: '123', - value: { - action: 'ALLOW', - force: false, - }, - }; - element.section = 'refs/*'; - - // Typically called on ready since elements will have properies defined - // by the parent element. - element._setupValues(element.rule); - flushAsynchronousOperations(); - flush(() => { - element.attached(); - done(); - }); - }); - - test('_ruleValues and _originalRuleValues are set correctly', () => { - assert.deepEqual(element._originalRuleValues, element.rule.value); - }); - - test('values are set correctly', () => { - assert.equal(element.$.action.bindValue, element.rule.value.action); - assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin')); - assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax')); - assert.isFalse(element.$.force.classList.contains('force')); - }); - - test('modify and cancel restores original values', () => { - element.editing = true; - assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none'); - assert.isNotOk(element.rule.value.modified); - element.$.action.bindValue = 'DENY'; - assert.isTrue(element.rule.value.modified); - element.editing = false; - assert.equal(getComputedStyle(element.$.removeBtn).display, 'none'); - assert.deepEqual(element._originalRuleValues, element.rule.value); - assert.equal(element.$.action.bindValue, 'ALLOW'); - assert.isNotOk(element.rule.value.modified); - }); - - test('modify value', () => { - assert.isNotOk(element.rule.value.modified); - element.$.action.bindValue = 'DENY'; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); - - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); - - test('all selects are disabled when not in edit mode', () => { - const selects = Polymer.dom(element.root).querySelectorAll('select'); - for (const select of selects) { - assert.isTrue(select.disabled); - } - element.editing = true; - for (const select of selects) { - assert.isFalse(select.disabled); - } - }); - - test('remove rule and undo remove', () => { - element.editing = true; - element.rule = {id: 123, value: {action: 'ALLOW'}}; - assert.isFalse( - element.$.deletedContainer.classList.contains('deleted')); - MockInteractions.tap(element.$.removeBtn); - assert.isTrue(element.$.deletedContainer.classList.contains('deleted')); - assert.isTrue(element._deleted); - assert.isTrue(element.rule.value.deleted); - - MockInteractions.tap(element.$.undoRemoveBtn); - assert.isFalse(element._deleted); - assert.isNotOk(element.rule.value.deleted); - }); - - test('remove rule and cancel', () => { - element.editing = true; - assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none'); - assert.equal(getComputedStyle(element.$.deletedContainer).display, - 'none'); - - element.rule = {id: 123, value: {action: 'ALLOW'}}; - MockInteractions.tap(element.$.removeBtn); - assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none'); - assert.notEqual(getComputedStyle(element.$.deletedContainer).display, - 'none'); - assert.isTrue(element._deleted); - assert.isTrue(element.rule.value.deleted); - - element.editing = false; - assert.isFalse(element._deleted); - assert.isNotOk(element.rule.value.deleted); - assert.isNotOk(element.rule.value.modified); - - assert.deepEqual(element._originalRuleValues, element.rule.value); - assert.equal(getComputedStyle(element.$.removeBtn).display, 'none'); - assert.equal(getComputedStyle(element.$.deletedContainer).display, - 'none'); - }); - - test('_computeGroupPath', () => { - const group = '123'; - assert.equal(element._computeGroupPath(group), - `/admin/groups/123`); - }); - }); - - suite('new edit rule', () => { - setup(done => { - element.group = 'Group Name'; - element.permission = 'editTopicName'; - element.rule = { - id: '123', - }; - element.section = 'refs/*'; - element._setupValues(element.rule); - flushAsynchronousOperations(); - element.rule.value.added = true; - flush(() => { - element.attached(); - done(); - }); - }); - - test('_ruleValues and _originalRuleValues are set correctly', () => { - // Since the element does not already have default values, they should - // be set. The original values should be set to those too. - assert.isNotOk(element.rule.value.modified); - const expectedRuleValue = { - action: 'ALLOW', - force: false, - added: true, - }; - assert.deepEqual(element.rule.value, expectedRuleValue); - test('values are set correctly', () => { - assert.equal(element.$.action.bindValue, expectedRuleValue.action); - assert.equal(element.$.force.bindValue, expectedRuleValue.action); - }); - }); - - test('modify value', () => { - assert.isNotOk(element.rule.value.modified); - element.$.force.bindValue = true; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); - - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); - - test('remove value', () => { - element.editing = true; - const removeStub = sandbox.stub(); - element.addEventListener('added-rule-removed', removeStub); - MockInteractions.tap(element.$.removeBtn); - flushAsynchronousOperations(); - assert.isTrue(removeStub.called); - }); - }); - - suite('already existing rule with labels', () => { - setup(done => { - element.label = {values: [ - {value: -2, text: 'This shall not be merged'}, - {value: -1, text: 'I would prefer this is not merged as is'}, - {value: -0, text: 'No score'}, - {value: 1, text: 'Looks good to me, but someone else must approve'}, - {value: 2, text: 'Looks good to me, approved'}, - ]}; - element.group = 'Group Name'; - element.permission = 'label-Code-Review'; - element.rule = { - id: '123', - value: { - action: 'ALLOW', - force: false, - max: 2, - min: -2, - }, - }; - element.section = 'refs/*'; - element._setupValues(element.rule); - flushAsynchronousOperations(); - flush(() => { - element.attached(); - done(); - }); - }); - - test('_ruleValues and _originalRuleValues are set correctly', () => { - assert.deepEqual(element._originalRuleValues, element.rule.value); - }); - - test('values are set correctly', () => { - assert.equal(element.$.action.bindValue, element.rule.value.action); - assert.equal( - Polymer.dom(element.root).querySelector('#labelMin').bindValue, - element.rule.value.min); - assert.equal( - Polymer.dom(element.root).querySelector('#labelMax').bindValue, - element.rule.value.max); - assert.isFalse(element.$.force.classList.contains('force')); - }); - - test('modify value', () => { - const removeStub = sandbox.stub(); - element.addEventListener('added-rule-removed', removeStub); - assert.isNotOk(element.rule.value.modified); - Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); - assert.isFalse(removeStub.called); - - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); - }); - - suite('new rule with labels', () => { - setup(done => { - sandbox.spy(element, '_setDefaultRuleValues'); - element.label = {values: [ - {value: -2, text: 'This shall not be merged'}, - {value: -1, text: 'I would prefer this is not merged as is'}, - {value: -0, text: 'No score'}, - {value: 1, text: 'Looks good to me, but someone else must approve'}, - {value: 2, text: 'Looks good to me, approved'}, - ]}; - element.group = 'Group Name'; - element.permission = 'label-Code-Review'; - element.rule = { - id: '123', - }; - element.section = 'refs/*'; - element._setupValues(element.rule); - flushAsynchronousOperations(); - element.rule.value.added = true; - flush(() => { - element.attached(); - done(); - }); - }); - - test('_ruleValues and _originalRuleValues are set correctly', () => { - // Since the element does not already have default values, they should - // be set. The original values should be set to those too. - assert.isNotOk(element.rule.value.modified); - assert.isTrue(element._setDefaultRuleValues.called); - - const expectedRuleValue = { - max: element.label.values[element.label.values.length - 1].value, - min: element.label.values[0].value, - action: 'ALLOW', - added: true, - }; - assert.deepEqual(element.rule.value, expectedRuleValue); - test('values are set correctly', () => { + action = 'DENY'; + assert.isFalse(element._computeForce(permission, action)); + assert.equal(element._computeForceClass(permission, action), ''); assert.equal( - element.$.action.bindValue, - expectedRuleValue.action); - assert.equal( - Polymer.dom(element.root).querySelector('#labelMin').bindValue, - expectedRuleValue.min); - assert.equal( - Polymer.dom(element.root).querySelector('#labelMax').bindValue, - expectedRuleValue.max); + element._computeForceOptions(permission, action).length, 0); + + permission = 'editTopicName'; + assert.isTrue(element._computeForce(permission)); + assert.equal(element._computeForceClass(permission), 'force'); + assert.deepEqual(element._computeForceOptions(permission), + FORCE_EDIT_OPTIONS); + permission = 'submit'; + assert.isFalse(element._computeForce(permission)); + assert.equal(element._computeForceClass(permission), ''); + assert.deepEqual(element._computeForceOptions(permission), []); }); - }); - test('modify value', () => { - assert.isNotOk(element.rule.value.modified); - Polymer.dom(element.root).querySelector('#labelMin').bindValue = 1; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); + test('_computeSectionClass', () => { + let deleted = true; + let editing = false; + assert.equal(element._computeSectionClass(editing, deleted), 'deleted'); - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); + deleted = false; + assert.equal(element._computeSectionClass(editing, deleted), ''); + + editing = true; + assert.equal(element._computeSectionClass(editing, deleted), 'editing'); + + deleted = true; + assert.equal(element._computeSectionClass(editing, deleted), + 'editing deleted'); }); - suite('already existing push rule', () => { - setup(done => { - element.group = 'Group Name'; - element.permission = 'push'; - element.rule = { - id: '123', - value: { - action: 'ALLOW', - force: true, - }, - }; - element.section = 'refs/*'; - element._setupValues(element.rule); - flushAsynchronousOperations(); - flush(() => { - element.attached(); - done(); - }); - }); - - test('_ruleValues and _originalRuleValues are set correctly', () => { - assert.deepEqual(element._originalRuleValues, element.rule.value); - }); - - test('values are set correctly', () => { - assert.isTrue(element.$.force.classList.contains('force')); - assert.equal(element.$.action.bindValue, element.rule.value.action); - assert.equal( - Polymer.dom(element.root).querySelector('#force').bindValue, - element.rule.value.force); - assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin')); - assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax')); - }); - - test('modify value', () => { - assert.isNotOk(element.rule.value.modified); - element.$.action.bindValue = false; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); - - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); + test('_getDefaultRuleValues', () => { + let permission = 'priority'; + let label; + assert.deepEqual(element._getDefaultRuleValues(permission, label), + {action: 'BATCH'}); + permission = 'label-Code-Review'; + label = {values: [ + {value: -2, text: 'This shall not be merged'}, + {value: -1, text: 'I would prefer this is not merged as is'}, + {value: -0, text: 'No score'}, + {value: 1, text: 'Looks good to me, but someone else must approve'}, + {value: 2, text: 'Looks good to me, approved'}, + ]}; + assert.deepEqual(element._getDefaultRuleValues(permission, label), + {action: 'ALLOW', max: 2, min: -2}); + permission = 'push'; + label = undefined; + assert.deepEqual(element._getDefaultRuleValues(permission, label), + {action: 'ALLOW', force: false}); + permission = 'submit'; + assert.deepEqual(element._getDefaultRuleValues(permission, label), + {action: 'ALLOW'}); }); - suite('new push rule', () => { - setup(done => { - element.group = 'Group Name'; - element.permission = 'push'; - element.rule = { - id: '123', - }; - element.section = 'refs/*'; - element._setupValues(element.rule); - flushAsynchronousOperations(); - element.rule.value.added = true; - flush(() => { - element.attached(); - done(); - }); - }); - - test('_ruleValues and _originalRuleValues are set correctly', () => { - // Since the element does not already have default values, they should - // be set. The original values should be set to those too. - assert.isNotOk(element.rule.value.modified); - const expectedRuleValue = { - action: 'ALLOW', - force: false, - added: true, - }; - assert.deepEqual(element.rule.value, expectedRuleValue); - test('values are set correctly', () => { - assert.equal(element.$.action.bindValue, expectedRuleValue.action); - assert.equal(element.$.force.bindValue, expectedRuleValue.action); - }); - }); - - test('modify value', () => { - assert.isNotOk(element.rule.value.modified); - element.$.force.bindValue = true; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); - - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); + test('_setDefaultRuleValues', () => { + element.rule = {id: 123}; + const defaultValue = {action: 'ALLOW'}; + sandbox.stub(element, '_getDefaultRuleValues').returns(defaultValue); + element._setDefaultRuleValues(); + assert.isTrue(element._getDefaultRuleValues.called); + assert.equal(element.rule.value, defaultValue); }); - suite('already existing edit rule', () => { - setup(done => { - element.group = 'Group Name'; - element.permission = 'editTopicName'; - element.rule = { - id: '123', - value: { - action: 'ALLOW', - force: true, - }, - }; - element.section = 'refs/*'; - element._setupValues(element.rule); - flushAsynchronousOperations(); - flush(() => { - element.attached(); - done(); - }); - }); + test('_computeOptions', () => { + const PRIORITY_OPTIONS = [ + 'BATCH', + 'INTERACTIVE', + ]; + const DROPDOWN_OPTIONS = [ + 'ALLOW', + 'DENY', + 'BLOCK', + ]; + let permission = 'priority'; + assert.deepEqual(element._computeOptions(permission), PRIORITY_OPTIONS); + permission = 'submit'; + assert.deepEqual(element._computeOptions(permission), DROPDOWN_OPTIONS); + }); - test('_ruleValues and _originalRuleValues are set correctly', () => { - assert.deepEqual(element._originalRuleValues, element.rule.value); - }); + test('_handleValueChange', () => { + const modifiedHandler = sandbox.stub(); + element.rule = {value: {}}; + element.addEventListener('access-modified', modifiedHandler); + element._handleValueChange(); + assert.isNotOk(element.rule.value.modified); + element._originalRuleValues = {}; + element._handleValueChange(); + assert.isTrue(element.rule.value.modified); + assert.isTrue(modifiedHandler.called); + }); - test('values are set correctly', () => { - assert.isTrue(element.$.force.classList.contains('force')); - assert.equal(element.$.action.bindValue, element.rule.value.action); - assert.equal( - Polymer.dom(element.root).querySelector('#force').bindValue, - element.rule.value.force); - assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMin')); - assert.isNotOk(Polymer.dom(element.root).querySelector('#labelMax')); - }); + test('_handleAccessSaved', () => { + const originalValue = {action: 'DENY'}; + const newValue = {action: 'ALLOW'}; + element._originalRuleValues = originalValue; + element.rule = {value: newValue}; + element._handleAccessSaved(); + assert.deepEqual(element._originalRuleValues, newValue); + }); - test('modify value', () => { - assert.isNotOk(element.rule.value.modified); - element.$.action.bindValue = false; - flushAsynchronousOperations(); - assert.isTrue(element.rule.value.modified); - - // The original value should now differ from the rule values. - assert.notDeepEqual(element._originalRuleValues, element.rule.value); - }); + test('_setOriginalRuleValues', () => { + const value = { + action: 'ALLOW', + force: false, + }; + element._setOriginalRuleValues(value); + assert.deepEqual(element._originalRuleValues, value); }); }); + + suite('already existing generic rule', () => { + setup(done => { + element.group = 'Group Name'; + element.permission = 'submit'; + element.rule = { + id: '123', + value: { + action: 'ALLOW', + force: false, + }, + }; + element.section = 'refs/*'; + + // Typically called on ready since elements will have properies defined + // by the parent element. + element._setupValues(element.rule); + flushAsynchronousOperations(); + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + assert.deepEqual(element._originalRuleValues, element.rule.value); + }); + + test('values are set correctly', () => { + assert.equal(element.$.action.bindValue, element.rule.value.action); + assert.isNotOk(dom(element.root).querySelector('#labelMin')); + assert.isNotOk(dom(element.root).querySelector('#labelMax')); + assert.isFalse(element.$.force.classList.contains('force')); + }); + + test('modify and cancel restores original values', () => { + element.editing = true; + assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none'); + assert.isNotOk(element.rule.value.modified); + element.$.action.bindValue = 'DENY'; + assert.isTrue(element.rule.value.modified); + element.editing = false; + assert.equal(getComputedStyle(element.$.removeBtn).display, 'none'); + assert.deepEqual(element._originalRuleValues, element.rule.value); + assert.equal(element.$.action.bindValue, 'ALLOW'); + assert.isNotOk(element.rule.value.modified); + }); + + test('modify value', () => { + assert.isNotOk(element.rule.value.modified); + element.$.action.bindValue = 'DENY'; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + + test('all selects are disabled when not in edit mode', () => { + const selects = dom(element.root).querySelectorAll('select'); + for (const select of selects) { + assert.isTrue(select.disabled); + } + element.editing = true; + for (const select of selects) { + assert.isFalse(select.disabled); + } + }); + + test('remove rule and undo remove', () => { + element.editing = true; + element.rule = {id: 123, value: {action: 'ALLOW'}}; + assert.isFalse( + element.$.deletedContainer.classList.contains('deleted')); + MockInteractions.tap(element.$.removeBtn); + assert.isTrue(element.$.deletedContainer.classList.contains('deleted')); + assert.isTrue(element._deleted); + assert.isTrue(element.rule.value.deleted); + + MockInteractions.tap(element.$.undoRemoveBtn); + assert.isFalse(element._deleted); + assert.isNotOk(element.rule.value.deleted); + }); + + test('remove rule and cancel', () => { + element.editing = true; + assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none'); + assert.equal(getComputedStyle(element.$.deletedContainer).display, + 'none'); + + element.rule = {id: 123, value: {action: 'ALLOW'}}; + MockInteractions.tap(element.$.removeBtn); + assert.notEqual(getComputedStyle(element.$.removeBtn).display, 'none'); + assert.notEqual(getComputedStyle(element.$.deletedContainer).display, + 'none'); + assert.isTrue(element._deleted); + assert.isTrue(element.rule.value.deleted); + + element.editing = false; + assert.isFalse(element._deleted); + assert.isNotOk(element.rule.value.deleted); + assert.isNotOk(element.rule.value.modified); + + assert.deepEqual(element._originalRuleValues, element.rule.value); + assert.equal(getComputedStyle(element.$.removeBtn).display, 'none'); + assert.equal(getComputedStyle(element.$.deletedContainer).display, + 'none'); + }); + + test('_computeGroupPath', () => { + const group = '123'; + assert.equal(element._computeGroupPath(group), + `/admin/groups/123`); + }); + }); + + suite('new edit rule', () => { + setup(done => { + element.group = 'Group Name'; + element.permission = 'editTopicName'; + element.rule = { + id: '123', + }; + element.section = 'refs/*'; + element._setupValues(element.rule); + flushAsynchronousOperations(); + element.rule.value.added = true; + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + // Since the element does not already have default values, they should + // be set. The original values should be set to those too. + assert.isNotOk(element.rule.value.modified); + const expectedRuleValue = { + action: 'ALLOW', + force: false, + added: true, + }; + assert.deepEqual(element.rule.value, expectedRuleValue); + test('values are set correctly', () => { + assert.equal(element.$.action.bindValue, expectedRuleValue.action); + assert.equal(element.$.force.bindValue, expectedRuleValue.action); + }); + }); + + test('modify value', () => { + assert.isNotOk(element.rule.value.modified); + element.$.force.bindValue = true; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + + test('remove value', () => { + element.editing = true; + const removeStub = sandbox.stub(); + element.addEventListener('added-rule-removed', removeStub); + MockInteractions.tap(element.$.removeBtn); + flushAsynchronousOperations(); + assert.isTrue(removeStub.called); + }); + }); + + suite('already existing rule with labels', () => { + setup(done => { + element.label = {values: [ + {value: -2, text: 'This shall not be merged'}, + {value: -1, text: 'I would prefer this is not merged as is'}, + {value: -0, text: 'No score'}, + {value: 1, text: 'Looks good to me, but someone else must approve'}, + {value: 2, text: 'Looks good to me, approved'}, + ]}; + element.group = 'Group Name'; + element.permission = 'label-Code-Review'; + element.rule = { + id: '123', + value: { + action: 'ALLOW', + force: false, + max: 2, + min: -2, + }, + }; + element.section = 'refs/*'; + element._setupValues(element.rule); + flushAsynchronousOperations(); + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + assert.deepEqual(element._originalRuleValues, element.rule.value); + }); + + test('values are set correctly', () => { + assert.equal(element.$.action.bindValue, element.rule.value.action); + assert.equal( + dom(element.root).querySelector('#labelMin').bindValue, + element.rule.value.min); + assert.equal( + dom(element.root).querySelector('#labelMax').bindValue, + element.rule.value.max); + assert.isFalse(element.$.force.classList.contains('force')); + }); + + test('modify value', () => { + const removeStub = sandbox.stub(); + element.addEventListener('added-rule-removed', removeStub); + assert.isNotOk(element.rule.value.modified); + dom(element.root).querySelector('#labelMin').bindValue = 1; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + assert.isFalse(removeStub.called); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + }); + + suite('new rule with labels', () => { + setup(done => { + sandbox.spy(element, '_setDefaultRuleValues'); + element.label = {values: [ + {value: -2, text: 'This shall not be merged'}, + {value: -1, text: 'I would prefer this is not merged as is'}, + {value: -0, text: 'No score'}, + {value: 1, text: 'Looks good to me, but someone else must approve'}, + {value: 2, text: 'Looks good to me, approved'}, + ]}; + element.group = 'Group Name'; + element.permission = 'label-Code-Review'; + element.rule = { + id: '123', + }; + element.section = 'refs/*'; + element._setupValues(element.rule); + flushAsynchronousOperations(); + element.rule.value.added = true; + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + // Since the element does not already have default values, they should + // be set. The original values should be set to those too. + assert.isNotOk(element.rule.value.modified); + assert.isTrue(element._setDefaultRuleValues.called); + + const expectedRuleValue = { + max: element.label.values[element.label.values.length - 1].value, + min: element.label.values[0].value, + action: 'ALLOW', + added: true, + }; + assert.deepEqual(element.rule.value, expectedRuleValue); + test('values are set correctly', () => { + assert.equal( + element.$.action.bindValue, + expectedRuleValue.action); + assert.equal( + dom(element.root).querySelector('#labelMin').bindValue, + expectedRuleValue.min); + assert.equal( + dom(element.root).querySelector('#labelMax').bindValue, + expectedRuleValue.max); + }); + }); + + test('modify value', () => { + assert.isNotOk(element.rule.value.modified); + dom(element.root).querySelector('#labelMin').bindValue = 1; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + }); + + suite('already existing push rule', () => { + setup(done => { + element.group = 'Group Name'; + element.permission = 'push'; + element.rule = { + id: '123', + value: { + action: 'ALLOW', + force: true, + }, + }; + element.section = 'refs/*'; + element._setupValues(element.rule); + flushAsynchronousOperations(); + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + assert.deepEqual(element._originalRuleValues, element.rule.value); + }); + + test('values are set correctly', () => { + assert.isTrue(element.$.force.classList.contains('force')); + assert.equal(element.$.action.bindValue, element.rule.value.action); + assert.equal( + dom(element.root).querySelector('#force').bindValue, + element.rule.value.force); + assert.isNotOk(dom(element.root).querySelector('#labelMin')); + assert.isNotOk(dom(element.root).querySelector('#labelMax')); + }); + + test('modify value', () => { + assert.isNotOk(element.rule.value.modified); + element.$.action.bindValue = false; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + }); + + suite('new push rule', () => { + setup(done => { + element.group = 'Group Name'; + element.permission = 'push'; + element.rule = { + id: '123', + }; + element.section = 'refs/*'; + element._setupValues(element.rule); + flushAsynchronousOperations(); + element.rule.value.added = true; + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + // Since the element does not already have default values, they should + // be set. The original values should be set to those too. + assert.isNotOk(element.rule.value.modified); + const expectedRuleValue = { + action: 'ALLOW', + force: false, + added: true, + }; + assert.deepEqual(element.rule.value, expectedRuleValue); + test('values are set correctly', () => { + assert.equal(element.$.action.bindValue, expectedRuleValue.action); + assert.equal(element.$.force.bindValue, expectedRuleValue.action); + }); + }); + + test('modify value', () => { + assert.isNotOk(element.rule.value.modified); + element.$.force.bindValue = true; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + }); + + suite('already existing edit rule', () => { + setup(done => { + element.group = 'Group Name'; + element.permission = 'editTopicName'; + element.rule = { + id: '123', + value: { + action: 'ALLOW', + force: true, + }, + }; + element.section = 'refs/*'; + element._setupValues(element.rule); + flushAsynchronousOperations(); + flush(() => { + element.attached(); + done(); + }); + }); + + test('_ruleValues and _originalRuleValues are set correctly', () => { + assert.deepEqual(element._originalRuleValues, element.rule.value); + }); + + test('values are set correctly', () => { + assert.isTrue(element.$.force.classList.contains('force')); + assert.equal(element.$.action.bindValue, element.rule.value.action); + assert.equal( + dom(element.root).querySelector('#force').bindValue, + element.rule.value.force); + assert.isNotOk(dom(element.root).querySelector('#labelMin')); + assert.isNotOk(dom(element.root).querySelector('#labelMax')); + }); + + test('modify value', () => { + assert.isNotOk(element.rule.value.modified); + element.$.action.bindValue = false; + flushAsynchronousOperations(); + assert.isTrue(element.rule.value.modified); + + // The original value should now differ from the rule values. + assert.notDeepEqual(element._originalRuleValues, element.rule.value); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js index 013b44b..6e2f11c 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,210 +14,232 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const CHANGE_SIZE = { - XS: 10, - SMALL: 50, - MEDIUM: 250, - LARGE: 1000, - }; +import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js'; +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../styles/gr-change-list-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-account-link/gr-account-link.js'; +import '../../shared/gr-change-star/gr-change-star.js'; +import '../../shared/gr-change-status/gr-change-status.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-limited-text/gr-limited-text.js'; +import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; +import '../../../styles/shared-styles.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-list-item_html.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.ChangeTableMixin - * @appliesMixin Gerrit.PathListMixin - * @appliesMixin Gerrit.RESTClientMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrChangeListItem extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.ChangeTableBehavior, - Gerrit.PathListBehavior, - Gerrit.RESTClientBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-list-item'; } +const CHANGE_SIZE = { + XS: 10, + SMALL: 50, + MEDIUM: 250, + LARGE: 1000, +}; - static get properties() { - return { - visibleChangeTableColumns: Array, - labelNames: { - type: Array, - }, +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.ChangeTableMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.RESTClientMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrChangeListItem extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.ChangeTableBehavior, + Gerrit.PathListBehavior, + Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @type {?} */ - change: Object, - changeURL: { - type: String, - computed: '_computeChangeURL(change)', - }, - statuses: { - type: Array, - computed: 'changeStatuses(change)', - }, - showStar: { - type: Boolean, - value: false, - }, - showNumber: Boolean, - _changeSize: { - type: String, - computed: '_computeChangeSize(change)', - }, - _dynamicCellEndpoints: { - type: Array, - }, - }; + static get is() { return 'gr-change-list-item'; } + + static get properties() { + return { + visibleChangeTableColumns: Array, + labelNames: { + type: Array, + }, + + /** @type {?} */ + change: Object, + changeURL: { + type: String, + computed: '_computeChangeURL(change)', + }, + statuses: { + type: Array, + computed: 'changeStatuses(change)', + }, + showStar: { + type: Boolean, + value: false, + }, + showNumber: Boolean, + _changeSize: { + type: String, + computed: '_computeChangeSize(change)', + }, + _dynamicCellEndpoints: { + type: Array, + }, + }; + } + + /** @override */ + attached() { + super.attached(); + Gerrit.awaitPluginsLoaded().then(() => { + this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints( + 'change-list-item-cell'); + }); + } + + _computeChangeURL(change) { + return Gerrit.Nav.getUrlForChange(change); + } + + _computeLabelTitle(change, labelName) { + const label = change.labels[labelName]; + if (!label) { return 'Label not applicable'; } + const significantLabel = label.rejected || label.approved || + label.disliked || label.recommended; + if (significantLabel && significantLabel.name) { + return labelName + '\nby ' + significantLabel.name; } + return labelName; + } - /** @override */ - attached() { - super.attached(); - Gerrit.awaitPluginsLoaded().then(() => { - this._dynamicCellEndpoints = Gerrit._endpoints.getDynamicEndpoints( - 'change-list-item-cell'); - }); - } - - _computeChangeURL(change) { - return Gerrit.Nav.getUrlForChange(change); - } - - _computeLabelTitle(change, labelName) { - const label = change.labels[labelName]; - if (!label) { return 'Label not applicable'; } - const significantLabel = label.rejected || label.approved || - label.disliked || label.recommended; - if (significantLabel && significantLabel.name) { - return labelName + '\nby ' + significantLabel.name; - } - return labelName; - } - - _computeLabelClass(change, labelName) { - const label = change.labels[labelName]; - // Mimic a Set. - const classes = { - cell: true, - label: true, - }; - if (label) { - if (label.approved) { - classes['u-green'] = true; - } - if (label.value == 1) { - classes['u-monospace'] = true; - classes['u-green'] = true; - } else if (label.value == -1) { - classes['u-monospace'] = true; - classes['u-red'] = true; - } - if (label.rejected) { - classes['u-red'] = true; - } - } else { - classes['u-gray-background'] = true; - } - return Object.keys(classes).sort() - .join(' '); - } - - _computeLabelValue(change, labelName) { - const label = change.labels[labelName]; - if (!label) { return ''; } + _computeLabelClass(change, labelName) { + const label = change.labels[labelName]; + // Mimic a Set. + const classes = { + cell: true, + label: true, + }; + if (label) { if (label.approved) { - return '✓'; + classes['u-green'] = true; + } + if (label.value == 1) { + classes['u-monospace'] = true; + classes['u-green'] = true; + } else if (label.value == -1) { + classes['u-monospace'] = true; + classes['u-red'] = true; } if (label.rejected) { - return '✕'; + classes['u-red'] = true; } - if (label.value > 0) { - return '+' + label.value; - } - if (label.value < 0) { - return label.value; - } - return ''; + } else { + classes['u-gray-background'] = true; } + return Object.keys(classes).sort() + .join(' '); + } - _computeRepoUrl(change) { - return Gerrit.Nav.getUrlForProjectChanges(change.project, true, - change.internalHost); + _computeLabelValue(change, labelName) { + const label = change.labels[labelName]; + if (!label) { return ''; } + if (label.approved) { + return '✓'; } - - _computeRepoBranchURL(change) { - return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null, - change.internalHost); + if (label.rejected) { + return '✕'; } - - _computeTopicURL(change) { - if (!change.topic) { return ''; } - return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost); + if (label.value > 0) { + return '+' + label.value; } - - /** - * Computes the display string for the project column. If there is a host - * specified in the change detail, the string will be prefixed with it. - * - * @param {!Object} change - * @param {string=} truncate whether or not the project name should be - * truncated. If this value is truthy, the name will be truncated. - * @return {string} - */ - _computeRepoDisplay(change, truncate) { - if (!change || !change.project) { return ''; } - let str = ''; - if (change.internalHost) { str += change.internalHost + '/'; } - str += truncate ? this.truncatePath(change.project, 2) : change.project; - return str; + if (label.value < 0) { + return label.value; } + return ''; + } - _computeSizeTooltip(change) { - if (change.insertions + change.deletions === 0 || - isNaN(change.insertions + change.deletions)) { - return 'Size unknown'; - } else { - return `+${change.insertions}, -${change.deletions}`; - } - } + _computeRepoUrl(change) { + return Gerrit.Nav.getUrlForProjectChanges(change.project, true, + change.internalHost); + } - /** - * TShirt sizing is based on the following paper: - * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf - */ - _computeChangeSize(change) { - const delta = change.insertions + change.deletions; - if (isNaN(delta) || delta === 0) { - return null; // Unknown - } - if (delta < CHANGE_SIZE.XS) { - return 'XS'; - } else if (delta < CHANGE_SIZE.SMALL) { - return 'S'; - } else if (delta < CHANGE_SIZE.MEDIUM) { - return 'M'; - } else if (delta < CHANGE_SIZE.LARGE) { - return 'L'; - } else { - return 'XL'; - } - } + _computeRepoBranchURL(change) { + return Gerrit.Nav.getUrlForBranch(change.branch, change.project, null, + change.internalHost); + } - toggleReviewed() { - const newVal = !this.change.reviewed; - this.set('change.reviewed', newVal); - this.dispatchEvent(new CustomEvent('toggle-reviewed', { - bubbles: true, - composed: true, - detail: {change: this.change, reviewed: newVal}, - })); + _computeTopicURL(change) { + if (!change.topic) { return ''; } + return Gerrit.Nav.getUrlForTopic(change.topic, change.internalHost); + } + + /** + * Computes the display string for the project column. If there is a host + * specified in the change detail, the string will be prefixed with it. + * + * @param {!Object} change + * @param {string=} truncate whether or not the project name should be + * truncated. If this value is truthy, the name will be truncated. + * @return {string} + */ + _computeRepoDisplay(change, truncate) { + if (!change || !change.project) { return ''; } + let str = ''; + if (change.internalHost) { str += change.internalHost + '/'; } + str += truncate ? this.truncatePath(change.project, 2) : change.project; + return str; + } + + _computeSizeTooltip(change) { + if (change.insertions + change.deletions === 0 || + isNaN(change.insertions + change.deletions)) { + return 'Size unknown'; + } else { + return `+${change.insertions}, -${change.deletions}`; } } - customElements.define(GrChangeListItem.is, GrChangeListItem); -})(); + /** + * TShirt sizing is based on the following paper: + * http://dirkriehle.com/wp-content/uploads/2008/09/hicss-42-csdistr-final-web.pdf + */ + _computeChangeSize(change) { + const delta = change.insertions + change.deletions; + if (isNaN(delta) || delta === 0) { + return null; // Unknown + } + if (delta < CHANGE_SIZE.XS) { + return 'XS'; + } else if (delta < CHANGE_SIZE.SMALL) { + return 'S'; + } else if (delta < CHANGE_SIZE.MEDIUM) { + return 'M'; + } else if (delta < CHANGE_SIZE.LARGE) { + return 'L'; + } else { + return 'XL'; + } + } + + toggleReviewed() { + const newVal = !this.change.reviewed; + this.set('change.reviewed', newVal); + this.dispatchEvent(new CustomEvent('toggle-reviewed', { + bubbles: true, + composed: true, + detail: {change: this.change, reviewed: newVal}, + })); + } +} + +customElements.define(GrChangeListItem.is, GrChangeListItem);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js index 9022cf2..f76189c 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_html.js
@@ -1,39 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/gr-change-list-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> -<link rel="import" href="../../shared/gr-change-star/gr-change-star.html"> -<link rel="import" href="../../shared/gr-change-status/gr-change-status.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> - -<dom-module id="gr-change-list-item"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: table-row; @@ -128,17 +111,16 @@ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <td class="cell leftPadding"></td> - <td class="cell star" hidden$="[[!showStar]]" hidden> + <td class="cell star" hidden\$="[[!showStar]]" hidden=""> <gr-change-star change="{{change}}"></gr-change-star> </td> - <td class="cell number" hidden$="[[!showNumber]]" hidden> - <a href$="[[changeURL]]">[[change._number]]</a> + <td class="cell number" hidden\$="[[!showNumber]]" hidden=""> + <a href\$="[[changeURL]]">[[change._number]]</a> </td> - <td class="cell subject" - hidden$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]"> + <td class="cell subject" hidden\$="[[isColumnHidden('Subject', visibleChangeTableColumns)]]"> <div class="container"> <div class="content"> - <a title$="[[change.subject]]" href$="[[changeURL]]"> + <a title\$="[[change.subject]]" href\$="[[changeURL]]"> [[change.subject]] </a> </div> @@ -148,67 +130,50 @@ <span> </span> </div> </td> - <td class="cell status" - hidden$="[[isColumnHidden('Status', visibleChangeTableColumns)]]"> + <td class="cell status" hidden\$="[[isColumnHidden('Status', visibleChangeTableColumns)]]"> <template is="dom-repeat" items="[[statuses]]" as="status"> <div class="comma">,</div> - <gr-change-status flat status="[[status]]"></gr-change-status> + <gr-change-status flat="" status="[[status]]"></gr-change-status> </template> <template is="dom-if" if="[[!statuses.length]]"> <span class="placeholder">--</span> </template> </td> - <td class="cell owner" - hidden$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]"> - <gr-account-link - account="[[change.owner]]"></gr-account-link> + <td class="cell owner" hidden\$="[[isColumnHidden('Owner', visibleChangeTableColumns)]]"> + <gr-account-link account="[[change.owner]]"></gr-account-link> </td> - <td class="cell assignee" - hidden$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]"> + <td class="cell assignee" hidden\$="[[isColumnHidden('Assignee', visibleChangeTableColumns)]]"> <template is="dom-if" if="[[change.assignee]]"> - <gr-account-link - id="assigneeAccountLink" - account="[[change.assignee]]"></gr-account-link> + <gr-account-link id="assigneeAccountLink" account="[[change.assignee]]"></gr-account-link> </template> <template is="dom-if" if="[[!change.assignee]]"> <span class="placeholder">--</span> </template> </td> - <td class="cell repo" - hidden$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]"> - <a class="fullRepo" href$="[[_computeRepoUrl(change)]]"> + <td class="cell repo" hidden\$="[[isColumnHidden('Repo', visibleChangeTableColumns)]]"> + <a class="fullRepo" href\$="[[_computeRepoUrl(change)]]"> [[_computeRepoDisplay(change)]] </a> - <a - class="truncatedRepo" - href$="[[_computeRepoUrl(change)]]" - title$="[[_computeRepoDisplay(change)]]"> + <a class="truncatedRepo" href\$="[[_computeRepoUrl(change)]]" title\$="[[_computeRepoDisplay(change)]]"> [[_computeRepoDisplay(change, 'true')]] </a> </td> - <td class="cell branch" - hidden$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"> - <a href$="[[_computeRepoBranchURL(change)]]"> + <td class="cell branch" hidden\$="[[isColumnHidden('Branch', visibleChangeTableColumns)]]"> + <a href\$="[[_computeRepoBranchURL(change)]]"> [[change.branch]] </a> <template is="dom-if" if="[[change.topic]]"> - (<a href$="[[_computeTopicURL(change)]]"><!-- + (<a href\$="[[_computeTopicURL(change)]]"><!-- --><gr-limited-text limit="50" text="[[change.topic]]"> </gr-limited-text><!-- --></a>) </template> </td> - <td class="cell updated" - hidden$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]"> - <gr-date-formatter - has-tooltip - date-str="[[change.updated]]"></gr-date-formatter> + <td class="cell updated" hidden\$="[[isColumnHidden('Updated', visibleChangeTableColumns)]]"> + <gr-date-formatter has-tooltip="" date-str="[[change.updated]]"></gr-date-formatter> </td> - <td class="cell size" - hidden$="[[isColumnHidden('Size', visibleChangeTableColumns)]]"> - <gr-tooltip-content - has-tooltip - title="[[_computeSizeTooltip(change)]]"> + <td class="cell size" hidden\$="[[isColumnHidden('Size', visibleChangeTableColumns)]]"> + <gr-tooltip-content has-tooltip="" title="[[_computeSizeTooltip(change)]]"> <template is="dom-if" if="[[_changeSize]]"> <span>[[_changeSize]]</span> </template> @@ -218,20 +183,16 @@ </gr-tooltip-content> </td> <template is="dom-repeat" items="[[labelNames]]" as="labelName"> - <td title$="[[_computeLabelTitle(change, labelName)]]" - class$="[[_computeLabelClass(change, labelName)]]"> + <td title\$="[[_computeLabelTitle(change, labelName)]]" class\$="[[_computeLabelClass(change, labelName)]]"> [[_computeLabelValue(change, labelName)]] </td> </template> - <template is="dom-repeat" items="[[_dynamicCellEndpoints]]" - as="pluginEndpointName"> + <template is="dom-repeat" items="[[_dynamicCellEndpoints]]" as="pluginEndpointName"> <td class="cell endpoint"> - <gr-endpoint-decorator name$="[[pluginEndpointName]]"> + <gr-endpoint-decorator name\$="[[pluginEndpointName]]"> <gr-endpoint-param name="change" value="[[change]]"> </gr-endpoint-param> </gr-endpoint-decorator> </td> </template> - </template> - <script src="gr-change-list-item.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html index 076c478..9bfec19 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-list-item</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-change-list-item.html"> +<script type="module" src="./gr-change-list-item.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-change-list-item.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,244 +43,247 @@ </template> </test-fixture> -<script> - suite('gr-change-list-item tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-change-list-item.js'; +suite('gr-change-list-item tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getLoggedIn() { return Promise.resolve(false); }, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, }); + element = fixture('basic'); + }); - teardown(() => { sandbox.restore(); }); + teardown(() => { sandbox.restore(); }); - test('computed fields', () => { - assert.equal(element._computeLabelClass({labels: {}}), - 'cell label u-gray-background'); - assert.equal(element._computeLabelClass( - {labels: {}}, 'Verified'), 'cell label u-gray-background'); - assert.equal(element._computeLabelClass( - {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), - 'cell label u-green u-monospace'); - assert.equal(element._computeLabelClass( - {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'), - 'cell label u-monospace u-red'); - assert.equal(element._computeLabelClass( - {labels: {'Code-Review': {value: 1}}}, 'Code-Review'), - 'cell label u-green u-monospace'); - assert.equal(element._computeLabelClass( - {labels: {'Code-Review': {value: -1}}}, 'Code-Review'), - 'cell label u-monospace u-red'); - assert.equal(element._computeLabelClass( - {labels: {'Code-Review': {value: -1}}}, 'Verified'), - 'cell label u-gray-background'); + test('computed fields', () => { + assert.equal(element._computeLabelClass({labels: {}}), + 'cell label u-gray-background'); + assert.equal(element._computeLabelClass( + {labels: {}}, 'Verified'), 'cell label u-gray-background'); + assert.equal(element._computeLabelClass( + {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), + 'cell label u-green u-monospace'); + assert.equal(element._computeLabelClass( + {labels: {Verified: {rejected: true, value: -1}}}, 'Verified'), + 'cell label u-monospace u-red'); + assert.equal(element._computeLabelClass( + {labels: {'Code-Review': {value: 1}}}, 'Code-Review'), + 'cell label u-green u-monospace'); + assert.equal(element._computeLabelClass( + {labels: {'Code-Review': {value: -1}}}, 'Code-Review'), + 'cell label u-monospace u-red'); + assert.equal(element._computeLabelClass( + {labels: {'Code-Review': {value: -1}}}, 'Verified'), + 'cell label u-gray-background'); - assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'), - 'Label not applicable'); - assert.equal(element._computeLabelTitle( - {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'), - 'Verified\nby Diffy'); - assert.equal(element._computeLabelTitle( - {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'), - 'Label not applicable'); - assert.equal(element._computeLabelTitle( - {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'), - 'Verified\nby Diffy'); - assert.equal(element._computeLabelTitle( - {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}}, - 'Code-Review'), 'Code-Review\nby Diffy'); - assert.equal(element._computeLabelTitle( - {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}}, - 'Code-Review'), 'Code-Review\nby Diffy'); - assert.equal(element._computeLabelTitle( - {labels: {'Code-Review': {recommended: {name: 'Diffy'}, - rejected: {name: 'Admin'}}}}, 'Code-Review'), - 'Code-Review\nby Admin'); - assert.equal(element._computeLabelTitle( - {labels: {'Code-Review': {approved: {name: 'Diffy'}, - rejected: {name: 'Admin'}}}}, 'Code-Review'), - 'Code-Review\nby Admin'); - assert.equal(element._computeLabelTitle( - {labels: {'Code-Review': {recommended: {name: 'Diffy'}, - disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), - 'Code-Review\nby Admin'); - assert.equal(element._computeLabelTitle( - {labels: {'Code-Review': {approved: {name: 'Diffy'}, - disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), - 'Code-Review\nby Diffy'); + assert.equal(element._computeLabelTitle({labels: {}}, 'Verified'), + 'Label not applicable'); + assert.equal(element._computeLabelTitle( + {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Verified'), + 'Verified\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {Verified: {approved: {name: 'Diffy'}}}}, 'Code-Review'), + 'Label not applicable'); + assert.equal(element._computeLabelTitle( + {labels: {Verified: {rejected: {name: 'Diffy'}}}}, 'Verified'), + 'Verified\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {disliked: {name: 'Diffy'}, value: -1}}}, + 'Code-Review'), 'Code-Review\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {recommended: {name: 'Diffy'}, value: 1}}}, + 'Code-Review'), 'Code-Review\nby Diffy'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {recommended: {name: 'Diffy'}, + rejected: {name: 'Admin'}}}}, 'Code-Review'), + 'Code-Review\nby Admin'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {approved: {name: 'Diffy'}, + rejected: {name: 'Admin'}}}}, 'Code-Review'), + 'Code-Review\nby Admin'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {recommended: {name: 'Diffy'}, + disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), + 'Code-Review\nby Admin'); + assert.equal(element._computeLabelTitle( + {labels: {'Code-Review': {approved: {name: 'Diffy'}, + disliked: {name: 'Admin'}, value: -1}}}, 'Code-Review'), + 'Code-Review\nby Diffy'); - assert.equal(element._computeLabelValue({labels: {}}), ''); - assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), ''); - assert.equal(element._computeLabelValue( - {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓'); - assert.equal(element._computeLabelValue( - {labels: {Verified: {value: 1}}}, 'Verified'), '+1'); - assert.equal(element._computeLabelValue( - {labels: {Verified: {value: -1}}}, 'Verified'), '-1'); - assert.equal(element._computeLabelValue( - {labels: {Verified: {approved: true}}}, 'Verified'), '✓'); - assert.equal(element._computeLabelValue( - {labels: {Verified: {rejected: true}}}, 'Verified'), '✕'); - }); + assert.equal(element._computeLabelValue({labels: {}}), ''); + assert.equal(element._computeLabelValue({labels: {}}, 'Verified'), ''); + assert.equal(element._computeLabelValue( + {labels: {Verified: {approved: true, value: 1}}}, 'Verified'), '✓'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {value: 1}}}, 'Verified'), '+1'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {value: -1}}}, 'Verified'), '-1'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {approved: true}}}, 'Verified'), '✓'); + assert.equal(element._computeLabelValue( + {labels: {Verified: {rejected: true}}}, 'Verified'), '✕'); + }); - test('no hidden columns', () => { - element.visibleChangeTableColumns = [ - 'Subject', - 'Status', - 'Owner', - 'Assignee', - 'Repo', - 'Branch', - 'Updated', - 'Size', - ]; + test('no hidden columns', () => { + element.visibleChangeTableColumns = [ + 'Subject', + 'Status', + 'Owner', + 'Assignee', + 'Repo', + 'Branch', + 'Updated', + 'Size', + ]; - flushAsynchronousOperations(); + flushAsynchronousOperations(); - for (const column of element.columnNames) { - const elementClass = '.' + column.toLowerCase(); - assert.isOk(element.shadowRoot - .querySelector(elementClass), - `Expect ${elementClass} element to be found`); + for (const column of element.columnNames) { + const elementClass = '.' + column.toLowerCase(); + assert.isOk(element.shadowRoot + .querySelector(elementClass), + `Expect ${elementClass} element to be found`); + assert.isFalse(element.shadowRoot + .querySelector(elementClass).hidden); + } + }); + + test('repo column hidden', () => { + element.visibleChangeTableColumns = [ + 'Subject', + 'Status', + 'Owner', + 'Assignee', + 'Branch', + 'Updated', + 'Size', + ]; + + flushAsynchronousOperations(); + + for (const column of element.columnNames) { + const elementClass = '.' + column.toLowerCase(); + if (column === 'Repo') { + assert.isTrue(element.shadowRoot + .querySelector(elementClass).hidden); + } else { assert.isFalse(element.shadowRoot .querySelector(elementClass).hidden); } - }); - - test('repo column hidden', () => { - element.visibleChangeTableColumns = [ - 'Subject', - 'Status', - 'Owner', - 'Assignee', - 'Branch', - 'Updated', - 'Size', - ]; - - flushAsynchronousOperations(); - - for (const column of element.columnNames) { - const elementClass = '.' + column.toLowerCase(); - if (column === 'Repo') { - assert.isTrue(element.shadowRoot - .querySelector(elementClass).hidden); - } else { - assert.isFalse(element.shadowRoot - .querySelector(elementClass).hidden); - } - } - }); - - test('random column does not exist', () => { - element.visibleChangeTableColumns = [ - 'Bad', - ]; - - flushAsynchronousOperations(); - const elementClass = '.bad'; - assert.isNotOk(element.shadowRoot - .querySelector(elementClass)); - }); - - test('assignee only displayed if there is one', () => { - element.change = {}; - flushAsynchronousOperations(); - assert.isNotOk(element.shadowRoot - .querySelector('.assignee gr-account-link')); - assert.equal(element.shadowRoot - .querySelector('.assignee').textContent.trim(), '--'); - element.change = { - assignee: { - name: 'test', - status: 'test', - }, - }; - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('.assignee gr-account-link')); - }); - - test('TShirt sizing tooltip', () => { - assert.equal(element._computeSizeTooltip({ - insertions: 'foo', - deletions: 'bar', - }), 'Size unknown'); - assert.equal(element._computeSizeTooltip({ - insertions: 0, - deletions: 0, - }), 'Size unknown'); - assert.equal(element._computeSizeTooltip({ - insertions: 1, - deletions: 2, - }), '+1, -2'); - }); - - test('TShirt sizing', () => { - assert.equal(element._computeChangeSize({ - insertions: 'foo', - deletions: 'bar', - }), null); - assert.equal(element._computeChangeSize({ - insertions: 1, - deletions: 1, - }), 'XS'); - assert.equal(element._computeChangeSize({ - insertions: 9, - deletions: 1, - }), 'S'); - assert.equal(element._computeChangeSize({ - insertions: 10, - deletions: 200, - }), 'M'); - assert.equal(element._computeChangeSize({ - insertions: 99, - deletions: 900, - }), 'L'); - assert.equal(element._computeChangeSize({ - insertions: 99, - deletions: 999, - }), 'XL'); - }); - - test('change params passed to gr-navigation', () => { - sandbox.stub(Gerrit.Nav); - const change = { - internalHost: 'test-host', - project: 'test-repo', - topic: 'test-topic', - branch: 'test-branch', - }; - element.change = change; - flushAsynchronousOperations(); - - assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]); - assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args, - [change.project, true, change.internalHost]); - assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args, - [change.branch, change.project, null, change.internalHost]); - assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args, - [change.topic, change.internalHost]); - }); - - test('_computeRepoDisplay', () => { - const change = { - project: 'a/test/repo', - internalHost: 'host', - }; - assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo'); - assert.equal(element._computeRepoDisplay(change, true), - 'host/…/test/repo'); - delete change.internalHost; - assert.equal(element._computeRepoDisplay(change), 'a/test/repo'); - assert.equal(element._computeRepoDisplay(change, true), - '…/test/repo'); - }); + } }); + + test('random column does not exist', () => { + element.visibleChangeTableColumns = [ + 'Bad', + ]; + + flushAsynchronousOperations(); + const elementClass = '.bad'; + assert.isNotOk(element.shadowRoot + .querySelector(elementClass)); + }); + + test('assignee only displayed if there is one', () => { + element.change = {}; + flushAsynchronousOperations(); + assert.isNotOk(element.shadowRoot + .querySelector('.assignee gr-account-link')); + assert.equal(element.shadowRoot + .querySelector('.assignee').textContent.trim(), '--'); + element.change = { + assignee: { + name: 'test', + status: 'test', + }, + }; + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('.assignee gr-account-link')); + }); + + test('TShirt sizing tooltip', () => { + assert.equal(element._computeSizeTooltip({ + insertions: 'foo', + deletions: 'bar', + }), 'Size unknown'); + assert.equal(element._computeSizeTooltip({ + insertions: 0, + deletions: 0, + }), 'Size unknown'); + assert.equal(element._computeSizeTooltip({ + insertions: 1, + deletions: 2, + }), '+1, -2'); + }); + + test('TShirt sizing', () => { + assert.equal(element._computeChangeSize({ + insertions: 'foo', + deletions: 'bar', + }), null); + assert.equal(element._computeChangeSize({ + insertions: 1, + deletions: 1, + }), 'XS'); + assert.equal(element._computeChangeSize({ + insertions: 9, + deletions: 1, + }), 'S'); + assert.equal(element._computeChangeSize({ + insertions: 10, + deletions: 200, + }), 'M'); + assert.equal(element._computeChangeSize({ + insertions: 99, + deletions: 900, + }), 'L'); + assert.equal(element._computeChangeSize({ + insertions: 99, + deletions: 999, + }), 'XL'); + }); + + test('change params passed to gr-navigation', () => { + sandbox.stub(Gerrit.Nav); + const change = { + internalHost: 'test-host', + project: 'test-repo', + topic: 'test-topic', + branch: 'test-branch', + }; + element.change = change; + flushAsynchronousOperations(); + + assert.deepEqual(Gerrit.Nav.getUrlForChange.lastCall.args, [change]); + assert.deepEqual(Gerrit.Nav.getUrlForProjectChanges.lastCall.args, + [change.project, true, change.internalHost]); + assert.deepEqual(Gerrit.Nav.getUrlForBranch.lastCall.args, + [change.branch, change.project, null, change.internalHost]); + assert.deepEqual(Gerrit.Nav.getUrlForTopic.lastCall.args, + [change.topic, change.internalHost]); + }); + + test('_computeRepoDisplay', () => { + const change = { + project: 'a/test/repo', + internalHost: 'host', + }; + assert.equal(element._computeRepoDisplay(change), 'host/a/test/repo'); + assert.equal(element._computeRepoDisplay(change, true), + 'host/…/test/repo'); + delete change.internalHost; + assert.equal(element._computeRepoDisplay(change), 'a/test/repo'); + assert.equal(element._computeRepoDisplay(change, true), + '…/test/repo'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js index b82593d..383aafa 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,282 +14,298 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const LookupQueryPatterns = { - CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i, - CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g, - }; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-change-list/gr-change-list.js'; +import '../gr-repo-header/gr-repo-header.js'; +import '../gr-user-header/gr-user-header.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-list-view_html.js'; - const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/; +const LookupQueryPatterns = { + CHANGE_ID: /^\s*i?[0-9a-f]{7,40}\s*$/i, + CHANGE_NUM: /^\s*[1-9][0-9]*\s*$/g, +}; - const REPO_QUERY_PATTERN = - /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/; +const USER_QUERY_PATTERN = /^owner:\s?("[^"]+"|[^ ]+)$/; - const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i; +const REPO_QUERY_PATTERN = + /^project:\s?("[^"]+"|[^ ]+)(\sstatus\s?:(open|"open"))?$/; +const LIMIT_OPERATOR_PATTERN = /\blimit:(\d+)/i; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrChangeListView extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-change-list-view'; } /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * Fired when the title of the page should change. + * + * @event title-change */ - class GrChangeListView extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-list-view'; } + + static get properties() { + return { /** - * Fired when the title of the page should change. - * - * @event title-change + * URL params passed from the router. */ + params: { + type: Object, + observer: '_paramsChanged', + }, - static get properties() { - return { /** - * URL params passed from the router. + * True when user is logged in. */ - params: { - type: Object, - observer: '_paramsChanged', - }, + _loggedIn: { + type: Boolean, + computed: '_computeLoggedIn(account)', + }, - /** - * True when user is logged in. - */ - _loggedIn: { - type: Boolean, - computed: '_computeLoggedIn(account)', - }, + account: { + type: Object, + value: null, + }, - account: { - type: Object, - value: null, - }, + /** + * State persisted across restamps of the element. + * + * Need sub-property declaration since it is used in template before + * assignment. + * + * @type {{ selectedChangeIndex: (number|undefined) }} + * + */ + viewState: { + type: Object, + notify: true, + value() { return {}; }, + }, - /** - * State persisted across restamps of the element. - * - * Need sub-property declaration since it is used in template before - * assignment. - * - * @type {{ selectedChangeIndex: (number|undefined) }} - * - */ - viewState: { - type: Object, - notify: true, - value() { return {}; }, - }, + preferences: Object, - preferences: Object, + _changesPerPage: Number, - _changesPerPage: Number, + /** + * Currently active query. + */ + _query: { + type: String, + value: '', + }, - /** - * Currently active query. - */ - _query: { - type: String, - value: '', - }, + /** + * Offset of currently visible query results. + */ + _offset: Number, - /** - * Offset of currently visible query results. - */ - _offset: Number, + /** + * Change objects loaded from the server. + */ + _changes: { + type: Array, + observer: '_changesChanged', + }, - /** - * Change objects loaded from the server. - */ - _changes: { - type: Array, - observer: '_changesChanged', - }, + /** + * For showing a "loading..." string during ajax requests. + */ + _loading: { + type: Boolean, + value: true, + }, - /** - * For showing a "loading..." string during ajax requests. - */ - _loading: { - type: Boolean, - value: true, - }, + /** @type {?string} */ + _userId: { + type: String, + value: null, + }, - /** @type {?string} */ - _userId: { - type: String, - value: null, - }, + /** @type {?string} */ + _repo: { + type: String, + value: null, + }, + }; + } - /** @type {?string} */ - _repo: { - type: String, - value: null, - }, - }; + /** @override */ + created() { + super.created(); + this.addEventListener('next-page', + () => this._handleNextPage()); + this.addEventListener('previous-page', + () => this._handlePreviousPage()); + } + + /** @override */ + attached() { + super.attached(); + this._loadPreferences(); + } + + _paramsChanged(value) { + if (value.view !== Gerrit.Nav.View.SEARCH) { return; } + + this._loading = true; + this._query = value.query; + this._offset = value.offset || 0; + if (this.viewState.query != this._query || + this.viewState.offset != this._offset) { + this.set('viewState.selectedChangeIndex', 0); + this.set('viewState.query', this._query); + this.set('viewState.offset', this._offset); } - /** @override */ - created() { - super.created(); - this.addEventListener('next-page', - () => this._handleNextPage()); - this.addEventListener('previous-page', - () => this._handlePreviousPage()); - } + // NOTE: This method may be called before attachment. Fire title-change + // in an async so that attachment to the DOM can take place first. + this.async(() => this.fire('title-change', {title: this._query})); - /** @override */ - attached() { - super.attached(); - this._loadPreferences(); - } - - _paramsChanged(value) { - if (value.view !== Gerrit.Nav.View.SEARCH) { return; } - - this._loading = true; - this._query = value.query; - this._offset = value.offset || 0; - if (this.viewState.query != this._query || - this.viewState.offset != this._offset) { - this.set('viewState.selectedChangeIndex', 0); - this.set('viewState.query', this._query); - this.set('viewState.offset', this._offset); - } - - // NOTE: This method may be called before attachment. Fire title-change - // in an async so that attachment to the DOM can take place first. - this.async(() => this.fire('title-change', {title: this._query})); - - this._getPreferences() - .then(prefs => { - this._changesPerPage = prefs.changes_per_page; - return this._getChanges(); - }) - .then(changes => { - changes = changes || []; - if (this._query && changes.length === 1) { - for (const query in LookupQueryPatterns) { - if (LookupQueryPatterns.hasOwnProperty(query) && - this._query.match(LookupQueryPatterns[query])) { - Gerrit.Nav.navigateToChange(changes[0]); - return; - } + this._getPreferences() + .then(prefs => { + this._changesPerPage = prefs.changes_per_page; + return this._getChanges(); + }) + .then(changes => { + changes = changes || []; + if (this._query && changes.length === 1) { + for (const query in LookupQueryPatterns) { + if (LookupQueryPatterns.hasOwnProperty(query) && + this._query.match(LookupQueryPatterns[query])) { + Gerrit.Nav.navigateToChange(changes[0]); + return; } } - this._changes = changes; - this._loading = false; - }); - } + } + this._changes = changes; + this._loading = false; + }); + } - _loadPreferences() { - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (loggedIn) { - this._getPreferences().then(preferences => { - this.preferences = preferences; - }); - } else { - this.preferences = {}; - } - }); - } - - _getChanges() { - return this.$.restAPI.getChanges(this._changesPerPage, this._query, - this._offset); - } - - _getPreferences() { - return this.$.restAPI.getPreferences(); - } - - _limitFor(query, defaultLimit) { - const match = query.match(LIMIT_OPERATOR_PATTERN); - if (!match) { - return defaultLimit; + _loadPreferences() { + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + this._getPreferences().then(preferences => { + this.preferences = preferences; + }); + } else { + this.preferences = {}; } - return parseInt(match[1], 10); - } + }); + } - _computeNavLink(query, offset, direction, changesPerPage) { - // Offset could be a string when passed from the router. - offset = +(offset || 0); - const limit = this._limitFor(query, changesPerPage); - const newOffset = Math.max(0, offset + (limit * direction)); - return Gerrit.Nav.getUrlForSearchQuery(query, newOffset); - } + _getChanges() { + return this.$.restAPI.getChanges(this._changesPerPage, this._query, + this._offset); + } - _computePrevArrowClass(offset) { - return offset === 0 ? 'hide' : ''; - } + _getPreferences() { + return this.$.restAPI.getPreferences(); + } - _computeNextArrowClass(changes) { - const more = changes.length && changes[changes.length - 1]._more_changes; - return more ? '' : 'hide'; + _limitFor(query, defaultLimit) { + const match = query.match(LIMIT_OPERATOR_PATTERN); + if (!match) { + return defaultLimit; } + return parseInt(match[1], 10); + } - _computeNavClass(loading) { - return loading || !this._changes || !this._changes.length ? 'hide' : ''; + _computeNavLink(query, offset, direction, changesPerPage) { + // Offset could be a string when passed from the router. + offset = +(offset || 0); + const limit = this._limitFor(query, changesPerPage); + const newOffset = Math.max(0, offset + (limit * direction)); + return Gerrit.Nav.getUrlForSearchQuery(query, newOffset); + } + + _computePrevArrowClass(offset) { + return offset === 0 ? 'hide' : ''; + } + + _computeNextArrowClass(changes) { + const more = changes.length && changes[changes.length - 1]._more_changes; + return more ? '' : 'hide'; + } + + _computeNavClass(loading) { + return loading || !this._changes || !this._changes.length ? 'hide' : ''; + } + + _handleNextPage() { + if (this.$.nextArrow.hidden) { return; } + page.show(this._computeNavLink( + this._query, this._offset, 1, this._changesPerPage)); + } + + _handlePreviousPage() { + if (this.$.prevArrow.hidden) { return; } + page.show(this._computeNavLink( + this._query, this._offset, -1, this._changesPerPage)); + } + + _changesChanged(changes) { + this._userId = null; + this._repo = null; + if (!changes || !changes.length) { + return; } - - _handleNextPage() { - if (this.$.nextArrow.hidden) { return; } - page.show(this._computeNavLink( - this._query, this._offset, 1, this._changesPerPage)); - } - - _handlePreviousPage() { - if (this.$.prevArrow.hidden) { return; } - page.show(this._computeNavLink( - this._query, this._offset, -1, this._changesPerPage)); - } - - _changesChanged(changes) { - this._userId = null; - this._repo = null; - if (!changes || !changes.length) { + if (USER_QUERY_PATTERN.test(this._query)) { + const owner = changes[0].owner; + const userId = owner._account_id ? owner._account_id : owner.email; + if (userId) { + this._userId = userId; return; } - if (USER_QUERY_PATTERN.test(this._query)) { - const owner = changes[0].owner; - const userId = owner._account_id ? owner._account_id : owner.email; - if (userId) { - this._userId = userId; - return; - } - } - if (REPO_QUERY_PATTERN.test(this._query)) { - this._repo = changes[0].project; - } } - - _computeHeaderClass(id) { - return id ? '' : 'hide'; - } - - _computePage(offset, changesPerPage) { - return offset / changesPerPage + 1; - } - - _computeLoggedIn(account) { - return !!(account && Object.keys(account).length > 0); - } - - _handleToggleStar(e) { - this.$.restAPI.saveChangeStarred(e.detail.change._number, - e.detail.starred); - } - - _handleToggleReviewed(e) { - this.$.restAPI.saveChangeReviewed(e.detail.change._number, - e.detail.reviewed); + if (REPO_QUERY_PATTERN.test(this._query)) { + this._repo = changes[0].project; } } - customElements.define(GrChangeListView.is, GrChangeListView); -})(); + _computeHeaderClass(id) { + return id ? '' : 'hide'; + } + + _computePage(offset, changesPerPage) { + return offset / changesPerPage + 1; + } + + _computeLoggedIn(account) { + return !!(account && Object.keys(account).length > 0); + } + + _handleToggleStar(e) { + this.$.restAPI.saveChangeStarred(e.detail.change._number, + e.detail.starred); + } + + _handleToggleReviewed(e) { + this.$.restAPI.saveChangeReviewed(e.detail.change._number, + e.detail.reviewed); + } +} + +customElements.define(GrChangeListView.is, GrChangeListView);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js index 6c9d975..bed3985 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-change-list/gr-change-list.html"> -<link rel="import" href="../gr-repo-header/gr-repo-header.html"> -<link rel="import" href="../gr-user-header/gr-user-header.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-change-list-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -70,39 +58,20 @@ } } </style> - <div class="loading" hidden$="[[!_loading]]" hidden>Loading...</div> - <div hidden$="[[_loading]]" hidden> - <gr-repo-header - repo="[[_repo]]" - class$="[[_computeHeaderClass(_repo)]]"></gr-repo-header> - <gr-user-header - user-id="[[_userId]]" - show-dashboard-link - logged-in="[[_loggedIn]]" - class$="[[_computeHeaderClass(_userId)]]"></gr-user-header> - <gr-change-list - account="[[account]]" - changes="{{_changes}}" - preferences="[[preferences]]" - selected-index="{{viewState.selectedChangeIndex}}" - show-star="[[_loggedIn]]" - on-toggle-star="_handleToggleStar" - on-toggle-reviewed="_handleToggleReviewed"></gr-change-list> - <nav class$="[[_computeNavClass(_loading)]]"> + <div class="loading" hidden\$="[[!_loading]]" hidden="">Loading...</div> + <div hidden\$="[[_loading]]" hidden=""> + <gr-repo-header repo="[[_repo]]" class\$="[[_computeHeaderClass(_repo)]]"></gr-repo-header> + <gr-user-header user-id="[[_userId]]" show-dashboard-link="" logged-in="[[_loggedIn]]" class\$="[[_computeHeaderClass(_userId)]]"></gr-user-header> + <gr-change-list account="[[account]]" changes="{{_changes}}" preferences="[[preferences]]" selected-index="{{viewState.selectedChangeIndex}}" show-star="[[_loggedIn]]" on-toggle-star="_handleToggleStar" on-toggle-reviewed="_handleToggleReviewed"></gr-change-list> + <nav class\$="[[_computeNavClass(_loading)]]"> Page [[_computePage(_offset, _changesPerPage)]] - <a id="prevArrow" - href$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]" - class$="[[_computePrevArrowClass(_offset)]]"> + <a id="prevArrow" href\$="[[_computeNavLink(_query, _offset, -1, _changesPerPage)]]" class\$="[[_computePrevArrowClass(_offset)]]"> <iron-icon icon="gr-icons:chevron-left"></iron-icon> </a> - <a id="nextArrow" - href$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]" - class$="[[_computeNextArrowClass(_changes)]]"> + <a id="nextArrow" href\$="[[_computeNavLink(_query, _offset, 1, _changesPerPage)]]" class\$="[[_computeNextArrowClass(_changes)]]"> <iron-icon icon="gr-icons:chevron-right"></iron-icon> </a> </nav> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-change-list-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html index 1d81ab7..468cd41 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-list-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-change-list-view.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-change-list-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-list-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,26 +41,170 @@ </template> </test-fixture> -<script> - const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127'; - const COMMIT_HASH = '12345678'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-list-view.js'; +const CHANGE_ID = 'IcA3dAB3edAB9f60B8dcdA6ef71A75980e4B7127'; +const COMMIT_HASH = '12345678'; - suite('gr-change-list-view tests', async () => { - await readyToTest(); - let element; - let sandbox; +suite('gr-change-list-view tests', () => { + let element; + let sandbox; + setup(() => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + getChanges(num, query) { + return Promise.resolve([]); + }, + getAccountDetails() { return Promise.resolve({}); }, + getAccountStatus() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(done => { + flush(() => { + sandbox.restore(); + done(); + }); + }); + + test('_computePage', () => { + assert.equal(element._computePage(0, 25), 1); + assert.equal(element._computePage(50, 25), 3); + }); + + test('_limitFor', () => { + const defaultLimit = 25; + const _limitFor = q => element._limitFor(q, defaultLimit); + assert.equal(_limitFor(''), defaultLimit); + assert.equal(_limitFor('limit:10'), 10); + assert.equal(_limitFor('xlimit:10'), defaultLimit); + assert.equal(_limitFor('x(limit:10'), 10); + }); + + test('_computeNavLink', () => { + const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery') + .returns(''); + const query = 'status:open'; + let offset = 0; + let direction = 1; + const changesPerPage = 5; + + element._computeNavLink(query, offset, direction, changesPerPage); + assert.equal(getUrlStub.lastCall.args[1], 5); + + direction = -1; + element._computeNavLink(query, offset, direction, changesPerPage); + assert.equal(getUrlStub.lastCall.args[1], 0); + + offset = 5; + direction = 1; + element._computeNavLink(query, offset, direction, changesPerPage); + assert.equal(getUrlStub.lastCall.args[1], 10); + }); + + test('_computePrevArrowClass', () => { + let offset = 0; + assert.equal(element._computePrevArrowClass(offset), 'hide'); + offset = 5; + assert.equal(element._computePrevArrowClass(offset), ''); + }); + + test('_computeNextArrowClass', () => { + let changes = _.times(25, _.constant({_more_changes: true})); + assert.equal(element._computeNextArrowClass(changes), ''); + changes = _.times(25, _.constant({})); + assert.equal(element._computeNextArrowClass(changes), 'hide'); + }); + + test('_computeNavClass', () => { + let loading = true; + assert.equal(element._computeNavClass(loading), 'hide'); + loading = false; + assert.equal(element._computeNavClass(loading), 'hide'); + element._changes = []; + assert.equal(element._computeNavClass(loading), 'hide'); + element._changes = _.times(5, _.constant({})); + assert.equal(element._computeNavClass(loading), ''); + }); + + test('_handleNextPage', () => { + const showStub = sandbox.stub(page, 'show'); + element.$.nextArrow.hidden = true; + element._handleNextPage(); + assert.isFalse(showStub.called); + element.$.nextArrow.hidden = false; + element._handleNextPage(); + assert.isTrue(showStub.called); + }); + + test('_handlePreviousPage', () => { + const showStub = sandbox.stub(page, 'show'); + element.$.prevArrow.hidden = true; + element._handlePreviousPage(); + assert.isFalse(showStub.called); + element.$.prevArrow.hidden = false; + element._handlePreviousPage(); + assert.isTrue(showStub.called); + }); + + test('_userId query', done => { + assert.isNull(element._userId); + element._query = 'owner: foo@bar'; + element._changes = [{owner: {email: 'foo@bar'}}]; + flush(() => { + assert.equal(element._userId, 'foo@bar'); + + element._query = 'foo bar baz'; + element._changes = [{owner: {email: 'foo@bar'}}]; + assert.isNull(element._userId); + + done(); + }); + }); + + test('_userId query without email', done => { + assert.isNull(element._userId); + element._query = 'owner: foo@bar'; + element._changes = [{owner: {}}]; + flush(() => { + assert.isNull(element._userId); + done(); + }); + }); + + test('_repo query', done => { + assert.isNull(element._repo); + element._query = 'project: test-repo'; + element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}]; + flush(() => { + assert.equal(element._repo, 'test-repo'); + element._query = 'foo bar baz'; + element._changes = [{owner: {email: 'foo@bar'}}]; + assert.isNull(element._repo); + done(); + }); + }); + + test('_repo query with open status', done => { + assert.isNull(element._repo); + element._query = 'project:test-repo status:open'; + element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}]; + flush(() => { + assert.equal(element._repo, 'test-repo'); + element._query = 'foo bar baz'; + element._changes = [{owner: {email: 'foo@bar'}}]; + assert.isNull(element._repo); + done(); + }); + }); + + suite('query based navigation', () => { setup(() => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - getChanges(num, query) { - return Promise.resolve([]); - }, - getAccountDetails() { return Promise.resolve({}); }, - getAccountStatus() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - sandbox = sinon.sandbox.create(); }); teardown(done => { @@ -65,205 +214,63 @@ }); }); - test('_computePage', () => { - assert.equal(element._computePage(0, 25), 1); - assert.equal(element._computePage(50, 25), 3); - }); - - test('_limitFor', () => { - const defaultLimit = 25; - const _limitFor = q => element._limitFor(q, defaultLimit); - assert.equal(_limitFor(''), defaultLimit); - assert.equal(_limitFor('limit:10'), 10); - assert.equal(_limitFor('xlimit:10'), defaultLimit); - assert.equal(_limitFor('x(limit:10'), 10); - }); - - test('_computeNavLink', () => { - const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForSearchQuery') - .returns(''); - const query = 'status:open'; - let offset = 0; - let direction = 1; - const changesPerPage = 5; - - element._computeNavLink(query, offset, direction, changesPerPage); - assert.equal(getUrlStub.lastCall.args[1], 5); - - direction = -1; - element._computeNavLink(query, offset, direction, changesPerPage); - assert.equal(getUrlStub.lastCall.args[1], 0); - - offset = 5; - direction = 1; - element._computeNavLink(query, offset, direction, changesPerPage); - assert.equal(getUrlStub.lastCall.args[1], 10); - }); - - test('_computePrevArrowClass', () => { - let offset = 0; - assert.equal(element._computePrevArrowClass(offset), 'hide'); - offset = 5; - assert.equal(element._computePrevArrowClass(offset), ''); - }); - - test('_computeNextArrowClass', () => { - let changes = _.times(25, _.constant({_more_changes: true})); - assert.equal(element._computeNextArrowClass(changes), ''); - changes = _.times(25, _.constant({})); - assert.equal(element._computeNextArrowClass(changes), 'hide'); - }); - - test('_computeNavClass', () => { - let loading = true; - assert.equal(element._computeNavClass(loading), 'hide'); - loading = false; - assert.equal(element._computeNavClass(loading), 'hide'); - element._changes = []; - assert.equal(element._computeNavClass(loading), 'hide'); - element._changes = _.times(5, _.constant({})); - assert.equal(element._computeNavClass(loading), ''); - }); - - test('_handleNextPage', () => { - const showStub = sandbox.stub(page, 'show'); - element.$.nextArrow.hidden = true; - element._handleNextPage(); - assert.isFalse(showStub.called); - element.$.nextArrow.hidden = false; - element._handleNextPage(); - assert.isTrue(showStub.called); - }); - - test('_handlePreviousPage', () => { - const showStub = sandbox.stub(page, 'show'); - element.$.prevArrow.hidden = true; - element._handlePreviousPage(); - assert.isFalse(showStub.called); - element.$.prevArrow.hidden = false; - element._handlePreviousPage(); - assert.isTrue(showStub.called); - }); - - test('_userId query', done => { - assert.isNull(element._userId); - element._query = 'owner: foo@bar'; - element._changes = [{owner: {email: 'foo@bar'}}]; - flush(() => { - assert.equal(element._userId, 'foo@bar'); - - element._query = 'foo bar baz'; - element._changes = [{owner: {email: 'foo@bar'}}]; - assert.isNull(element._userId); - + test('Searching for a change ID redirects to change', done => { + const change = {_number: 1}; + sandbox.stub(element, '_getChanges') + .returns(Promise.resolve([change])); + sandbox.stub(Gerrit.Nav, 'navigateToChange', url => { + assert.equal(url, change); done(); }); + + element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID}; }); - test('_userId query without email', done => { - assert.isNull(element._userId); - element._query = 'owner: foo@bar'; - element._changes = [{owner: {}}]; - flush(() => { - assert.isNull(element._userId); + test('Searching for a change num redirects to change', done => { + const change = {_number: 1}; + sandbox.stub(element, '_getChanges') + .returns(Promise.resolve([change])); + sandbox.stub(Gerrit.Nav, 'navigateToChange', url => { + assert.equal(url, change); done(); }); + + element.params = {view: Gerrit.Nav.View.SEARCH, query: '1'}; }); - test('_repo query', done => { - assert.isNull(element._repo); - element._query = 'project: test-repo'; - element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}]; - flush(() => { - assert.equal(element._repo, 'test-repo'); - element._query = 'foo bar baz'; - element._changes = [{owner: {email: 'foo@bar'}}]; - assert.isNull(element._repo); + test('Commit hash redirects to change', done => { + const change = {_number: 1}; + sandbox.stub(element, '_getChanges') + .returns(Promise.resolve([change])); + sandbox.stub(Gerrit.Nav, 'navigateToChange', url => { + assert.equal(url, change); done(); }); + + element.params = {view: Gerrit.Nav.View.SEARCH, query: COMMIT_HASH}; }); - test('_repo query with open status', done => { - assert.isNull(element._repo); - element._query = 'project:test-repo status:open'; - element._changes = [{owner: {email: 'foo@bar'}, project: 'test-repo'}]; - flush(() => { - assert.equal(element._repo, 'test-repo'); - element._query = 'foo bar baz'; - element._changes = [{owner: {email: 'foo@bar'}}]; - assert.isNull(element._repo); - done(); - }); + test('Searching for an invalid change ID searches', () => { + sandbox.stub(element, '_getChanges') + .returns(Promise.resolve([])); + const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + + element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID}; + flushAsynchronousOperations(); + + assert.isFalse(stub.called); }); - suite('query based navigation', () => { - setup(() => { - }); + test('Change ID with multiple search results searches', () => { + sandbox.stub(element, '_getChanges') + .returns(Promise.resolve([{}, {}])); + const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - teardown(done => { - flush(() => { - sandbox.restore(); - done(); - }); - }); + element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID}; + flushAsynchronousOperations(); - test('Searching for a change ID redirects to change', done => { - const change = {_number: 1}; - sandbox.stub(element, '_getChanges') - .returns(Promise.resolve([change])); - sandbox.stub(Gerrit.Nav, 'navigateToChange', url => { - assert.equal(url, change); - done(); - }); - - element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID}; - }); - - test('Searching for a change num redirects to change', done => { - const change = {_number: 1}; - sandbox.stub(element, '_getChanges') - .returns(Promise.resolve([change])); - sandbox.stub(Gerrit.Nav, 'navigateToChange', url => { - assert.equal(url, change); - done(); - }); - - element.params = {view: Gerrit.Nav.View.SEARCH, query: '1'}; - }); - - test('Commit hash redirects to change', done => { - const change = {_number: 1}; - sandbox.stub(element, '_getChanges') - .returns(Promise.resolve([change])); - sandbox.stub(Gerrit.Nav, 'navigateToChange', url => { - assert.equal(url, change); - done(); - }); - - element.params = {view: Gerrit.Nav.View.SEARCH, query: COMMIT_HASH}; - }); - - test('Searching for an invalid change ID searches', () => { - sandbox.stub(element, '_getChanges') - .returns(Promise.resolve([])); - const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - - element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID}; - flushAsynchronousOperations(); - - assert.isFalse(stub.called); - }); - - test('Change ID with multiple search results searches', () => { - sandbox.stub(element, '_getChanges') - .returns(Promise.resolve([{}, {}])); - const stub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - - element.params = {view: Gerrit.Nav.View.SEARCH, query: CHANGE_ID}; - flushAsynchronousOperations(); - - assert.isFalse(stub.called); - }); + assert.isFalse(stub.called); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js index 30df8fd..33c2ea2 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,403 +14,423 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const NUMBER_FIXED_COLUMNS = 3; - const CLOSED_STATUS = ['MERGED', 'ABANDONED']; - const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--'; - const MAX_SHORTCUT_CHARS = 5; +import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/gr-change-list-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-cursor-manager/gr-cursor-manager.js'; +import '../gr-change-list-item/gr-change-list-item.js'; +import '../../../styles/shared-styles.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-list_html.js'; + +const NUMBER_FIXED_COLUMNS = 3; +const CLOSED_STATUS = ['MERGED', 'ABANDONED']; +const LABEL_PREFIX_INVALID_PROLOG = 'Invalid-Prolog-Rules-Label-Name--'; +const MAX_SHORTCUT_CHARS = 5; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.ChangeTableMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.RESTClientMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrChangeList extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.ChangeTableBehavior, + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.RESTClientBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-change-list'; } + /** + * Fired when next page key shortcut was pressed. + * + * @event next-page + */ /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.ChangeTableMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.RESTClientMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * Fired when previous page key shortcut was pressed. + * + * @event previous-page */ - class GrChangeList extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.ChangeTableBehavior, - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.RESTClientBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-list'; } - /** - * Fired when next page key shortcut was pressed. - * - * @event next-page - */ + static get properties() { + return { /** - * Fired when previous page key shortcut was pressed. - * - * @event previous-page + * The logged-in user's account, or an empty object if no user is logged + * in. */ - - static get properties() { - return { + account: { + type: Object, + value: null, + }, /** - * The logged-in user's account, or an empty object if no user is logged - * in. + * An array of ChangeInfo objects to render. + * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info */ - account: { - type: Object, - value: null, - }, - /** - * An array of ChangeInfo objects to render. - * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info - */ - changes: { - type: Array, - observer: '_changesChanged', - }, - /** - * ChangeInfo objects grouped into arrays. The sections and changes - * properties should not be used together. - * - * @type {!Array<{ - * name: string, - * query: string, - * results: !Array<!Object> - * }>} - */ - sections: { - type: Array, - value() { return []; }, - }, - labelNames: { - type: Array, - computed: '_computeLabelNames(sections)', - }, - _dynamicHeaderEndpoints: { - type: Array, - }, - selectedIndex: { - type: Number, - notify: true, - }, - showNumber: Boolean, // No default value to prevent flickering. - showStar: { - type: Boolean, - value: false, - }, - showReviewedState: { - type: Boolean, - value: false, - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - changeTableColumns: Array, - visibleChangeTableColumns: Array, - preferences: Object, - }; - } + changes: { + type: Array, + observer: '_changesChanged', + }, + /** + * ChangeInfo objects grouped into arrays. The sections and changes + * properties should not be used together. + * + * @type {!Array<{ + * name: string, + * query: string, + * results: !Array<!Object> + * }>} + */ + sections: { + type: Array, + value() { return []; }, + }, + labelNames: { + type: Array, + computed: '_computeLabelNames(sections)', + }, + _dynamicHeaderEndpoints: { + type: Array, + }, + selectedIndex: { + type: Number, + notify: true, + }, + showNumber: Boolean, // No default value to prevent flickering. + showStar: { + type: Boolean, + value: false, + }, + showReviewedState: { + type: Boolean, + value: false, + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + changeTableColumns: Array, + visibleChangeTableColumns: Array, + preferences: Object, + }; + } - static get observers() { - return [ - '_sectionsChanged(sections.*)', - '_computePreferences(account, preferences)', - ]; - } + static get observers() { + return [ + '_sectionsChanged(sections.*)', + '_computePreferences(account, preferences)', + ]; + } - keyboardShortcuts() { - return { - [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange', - [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange', - [this.Shortcut.NEXT_PAGE]: '_nextPage', - [this.Shortcut.PREV_PAGE]: '_prevPage', - [this.Shortcut.OPEN_CHANGE]: '_openChange', - [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed', - [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar', - [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList', - }; - } + keyboardShortcuts() { + return { + [this.Shortcut.CURSOR_NEXT_CHANGE]: '_nextChange', + [this.Shortcut.CURSOR_PREV_CHANGE]: '_prevChange', + [this.Shortcut.NEXT_PAGE]: '_nextPage', + [this.Shortcut.PREV_PAGE]: '_prevPage', + [this.Shortcut.OPEN_CHANGE]: '_openChange', + [this.Shortcut.TOGGLE_CHANGE_REVIEWED]: '_toggleChangeReviewed', + [this.Shortcut.TOGGLE_CHANGE_STAR]: '_toggleChangeStar', + [this.Shortcut.REFRESH_CHANGE_LIST]: '_refreshChangeList', + }; + } - /** @override */ - created() { - super.created(); - this.addEventListener('keydown', - e => this._scopedKeydownHandler(e)); - } + /** @override */ + created() { + super.created(); + this.addEventListener('keydown', + e => this._scopedKeydownHandler(e)); + } - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('tabindex', 0); - } + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('tabindex', 0); + } - /** @override */ - attached() { - super.attached(); - Gerrit.awaitPluginsLoaded().then(() => { - this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints( - 'change-list-header'); - }); - } + /** @override */ + attached() { + super.attached(); + Gerrit.awaitPluginsLoaded().then(() => { + this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints( + 'change-list-header'); + }); + } - /** - * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard - * events must be scoped to a component level (e.g. `enter`) in order to not - * override native browser functionality. - * - * Context: Issue 7294 - */ - _scopedKeydownHandler(e) { - if (e.keyCode === 13) { - // Enter. - this._openChange(e); - } - } - - _lowerCase(column) { - return column.toLowerCase(); - } - - _computePreferences(account, preferences) { - // Polymer 2: check for undefined - if ([account, preferences].some(arg => arg === undefined)) { - return; - } - - this.changeTableColumns = this.columnNames; - - if (account) { - this.showNumber = !!(preferences && - preferences.legacycid_in_change_table); - if (preferences.change_table && - preferences.change_table.length > 0) { - this.visibleChangeTableColumns = - this.getVisibleColumns(preferences.change_table); - } else { - this.visibleChangeTableColumns = this.columnNames; - } - } else { - // Not logged in. - this.showNumber = false; - this.visibleChangeTableColumns = this.columnNames; - } - } - - _computeColspan(changeTableColumns, labelNames) { - if (!changeTableColumns || !labelNames) return; - return changeTableColumns.length + labelNames.length + - NUMBER_FIXED_COLUMNS; - } - - _computeLabelNames(sections) { - if (!sections) { return []; } - let labels = []; - const nonExistingLabel = function(item) { - return !labels.includes(item); - }; - for (const section of sections) { - if (!section.results) { continue; } - for (const change of section.results) { - if (!change.labels) { continue; } - const currentLabels = Object.keys(change.labels); - labels = labels.concat(currentLabels.filter(nonExistingLabel)); - } - } - return labels.sort(); - } - - _computeLabelShortcut(labelName) { - if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) { - labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length); - } - return labelName.split('-') - .reduce((a, i) => { - if (!i) { return a; } - return a + i[0].toUpperCase(); - }, '') - .slice(0, MAX_SHORTCUT_CHARS); - } - - _changesChanged(changes) { - this.sections = changes ? [{results: changes}] : []; - } - - _processQuery(query) { - let tokens = query.split(' '); - const invalidTokens = ['limit:', 'age:', '-age:']; - tokens = tokens.filter(token => !invalidTokens - .some(invalidToken => token.startsWith(invalidToken))); - return tokens.join(' '); - } - - _sectionHref(query) { - return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query)); - } - - /** - * Maps an index local to a particular section to the absolute index - * across all the changes on the page. - * - * @param {number} sectionIndex index of section - * @param {number} localIndex index of row within section - * @return {number} absolute index of row in the aggregate dashboard - */ - _computeItemAbsoluteIndex(sectionIndex, localIndex) { - let idx = 0; - for (let i = 0; i < sectionIndex; i++) { - idx += this.sections[i].results.length; - } - return idx + localIndex; - } - - _computeItemSelected(sectionIndex, index, selectedIndex) { - const idx = this._computeItemAbsoluteIndex(sectionIndex, index); - return idx == selectedIndex; - } - - _computeItemNeedsReview(account, change, showReviewedState) { - return showReviewedState && !change.reviewed && - !change.work_in_progress && - this.changeIsOpen(change) && - (!account || account._account_id != change.owner._account_id); - } - - _computeItemHighlight(account, change) { - // Do not show the assignee highlight if the change is not open. - if (!change ||!change.assignee || - !account || - CLOSED_STATUS.indexOf(change.status) !== -1) { - return false; - } - return account._account_id === change.assignee._account_id; - } - - _nextChange(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.cursor.next(); - } - - _prevChange(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.cursor.previous(); - } - - _openChange(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex)); - } - - _nextPage(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { - return; - } - - e.preventDefault(); - this.fire('next-page'); - } - - _prevPage(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { - return; - } - - e.preventDefault(); - this.fire('previous-page'); - } - - _toggleChangeReviewed(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this._toggleReviewedForIndex(this.selectedIndex); - } - - _toggleReviewedForIndex(index) { - const changeEls = this._getListItems(); - if (index >= changeEls.length || !changeEls[index]) { - return; - } - - const changeEl = changeEls[index]; - changeEl.toggleReviewed(); - } - - _refreshChangeList(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - this._reloadWindow(); - } - - _reloadWindow() { - window.location.reload(); - } - - _toggleChangeStar(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this._toggleStarForIndex(this.selectedIndex); - } - - _toggleStarForIndex(index) { - const changeEls = this._getListItems(); - if (index >= changeEls.length || !changeEls[index]) { - return; - } - - const changeEl = changeEls[index]; - changeEl.shadowRoot - .querySelector('gr-change-star').toggleStar(); - } - - _changeForIndex(index) { - const changeEls = this._getListItems(); - if (index < changeEls.length && changeEls[index]) { - return changeEls[index].change; - } - return null; - } - - _getListItems() { - return Array.from( - Polymer.dom(this.root).querySelectorAll('gr-change-list-item')); - } - - _sectionsChanged() { - // Flush DOM operations so that the list item elements will be loaded. - Polymer.RenderStatus.afterNextRender(this, () => { - this.$.cursor.stops = this._getListItems(); - this.$.cursor.moveToStart(); - }); - } - - _isOutgoing(section) { - return !!section.isOutgoing; - } - - _isEmpty(section) { - return !section.results.length; + /** + * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard + * events must be scoped to a component level (e.g. `enter`) in order to not + * override native browser functionality. + * + * Context: Issue 7294 + */ + _scopedKeydownHandler(e) { + if (e.keyCode === 13) { + // Enter. + this._openChange(e); } } - customElements.define(GrChangeList.is, GrChangeList); -})(); + _lowerCase(column) { + return column.toLowerCase(); + } + + _computePreferences(account, preferences) { + // Polymer 2: check for undefined + if ([account, preferences].some(arg => arg === undefined)) { + return; + } + + this.changeTableColumns = this.columnNames; + + if (account) { + this.showNumber = !!(preferences && + preferences.legacycid_in_change_table); + if (preferences.change_table && + preferences.change_table.length > 0) { + this.visibleChangeTableColumns = + this.getVisibleColumns(preferences.change_table); + } else { + this.visibleChangeTableColumns = this.columnNames; + } + } else { + // Not logged in. + this.showNumber = false; + this.visibleChangeTableColumns = this.columnNames; + } + } + + _computeColspan(changeTableColumns, labelNames) { + if (!changeTableColumns || !labelNames) return; + return changeTableColumns.length + labelNames.length + + NUMBER_FIXED_COLUMNS; + } + + _computeLabelNames(sections) { + if (!sections) { return []; } + let labels = []; + const nonExistingLabel = function(item) { + return !labels.includes(item); + }; + for (const section of sections) { + if (!section.results) { continue; } + for (const change of section.results) { + if (!change.labels) { continue; } + const currentLabels = Object.keys(change.labels); + labels = labels.concat(currentLabels.filter(nonExistingLabel)); + } + } + return labels.sort(); + } + + _computeLabelShortcut(labelName) { + if (labelName.startsWith(LABEL_PREFIX_INVALID_PROLOG)) { + labelName = labelName.slice(LABEL_PREFIX_INVALID_PROLOG.length); + } + return labelName.split('-') + .reduce((a, i) => { + if (!i) { return a; } + return a + i[0].toUpperCase(); + }, '') + .slice(0, MAX_SHORTCUT_CHARS); + } + + _changesChanged(changes) { + this.sections = changes ? [{results: changes}] : []; + } + + _processQuery(query) { + let tokens = query.split(' '); + const invalidTokens = ['limit:', 'age:', '-age:']; + tokens = tokens.filter(token => !invalidTokens + .some(invalidToken => token.startsWith(invalidToken))); + return tokens.join(' '); + } + + _sectionHref(query) { + return Gerrit.Nav.getUrlForSearchQuery(this._processQuery(query)); + } + + /** + * Maps an index local to a particular section to the absolute index + * across all the changes on the page. + * + * @param {number} sectionIndex index of section + * @param {number} localIndex index of row within section + * @return {number} absolute index of row in the aggregate dashboard + */ + _computeItemAbsoluteIndex(sectionIndex, localIndex) { + let idx = 0; + for (let i = 0; i < sectionIndex; i++) { + idx += this.sections[i].results.length; + } + return idx + localIndex; + } + + _computeItemSelected(sectionIndex, index, selectedIndex) { + const idx = this._computeItemAbsoluteIndex(sectionIndex, index); + return idx == selectedIndex; + } + + _computeItemNeedsReview(account, change, showReviewedState) { + return showReviewedState && !change.reviewed && + !change.work_in_progress && + this.changeIsOpen(change) && + (!account || account._account_id != change.owner._account_id); + } + + _computeItemHighlight(account, change) { + // Do not show the assignee highlight if the change is not open. + if (!change ||!change.assignee || + !account || + CLOSED_STATUS.indexOf(change.status) !== -1) { + return false; + } + return account._account_id === change.assignee._account_id; + } + + _nextChange(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.cursor.next(); + } + + _prevChange(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.cursor.previous(); + } + + _openChange(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + Gerrit.Nav.navigateToChange(this._changeForIndex(this.selectedIndex)); + } + + _nextPage(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { + return; + } + + e.preventDefault(); + this.fire('next-page'); + } + + _prevPage(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) { + return; + } + + e.preventDefault(); + this.fire('previous-page'); + } + + _toggleChangeReviewed(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._toggleReviewedForIndex(this.selectedIndex); + } + + _toggleReviewedForIndex(index) { + const changeEls = this._getListItems(); + if (index >= changeEls.length || !changeEls[index]) { + return; + } + + const changeEl = changeEls[index]; + changeEl.toggleReviewed(); + } + + _refreshChangeList(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._reloadWindow(); + } + + _reloadWindow() { + window.location.reload(); + } + + _toggleChangeStar(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._toggleStarForIndex(this.selectedIndex); + } + + _toggleStarForIndex(index) { + const changeEls = this._getListItems(); + if (index >= changeEls.length || !changeEls[index]) { + return; + } + + const changeEl = changeEls[index]; + changeEl.shadowRoot + .querySelector('gr-change-star').toggleStar(); + } + + _changeForIndex(index) { + const changeEls = this._getListItems(); + if (index < changeEls.length && changeEls[index]) { + return changeEls[index].change; + } + return null; + } + + _getListItems() { + return Array.from( + dom(this.root).querySelectorAll('gr-change-list-item')); + } + + _sectionsChanged() { + // Flush DOM operations so that the list item elements will be loaded. + afterNextRender(this, () => { + this.$.cursor.stops = this._getListItems(); + this.$.cursor.moveToStart(); + }); + } + + _isOutgoing(section) { + return !!section.isOutgoing; + } + + _isEmpty(section) { + return !section.results.length; + } +} + +customElements.define(GrChangeList.is, GrChangeList);
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js index 61b9960..ac37827 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_html.js
@@ -1,36 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/gr-change-list-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> -<link rel="import" href="../gr-change-list-item/gr-change-list-item.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> - -<dom-module id="gr-change-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -57,16 +43,14 @@ } </style> <table id="changeList"> - <template is="dom-repeat" items="[[sections]]" as="changeSection" - index-as="sectionIndex"> + <template is="dom-repeat" items="[[sections]]" as="changeSection" index-as="sectionIndex"> <template is="dom-if" if="[[changeSection.name]]"> <tbody> <tr class="groupHeader"> <td class="leftPadding"></td> - <td class="star" hidden$="[[!showStar]]" hidden></td> - <td class="cell" - colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"> - <a href$="[[_sectionHref(changeSection.query)]]" class="section-title"> + <td class="star" hidden\$="[[!showStar]]" hidden=""></td> + <td class="cell" colspan\$="[[_computeColspan(changeTableColumns, labelNames)]]"> + <a href\$="[[_sectionHref(changeSection.query)]]" class="section-title"> <span class="section-name">[[changeSection.name]]</span> <span class="section-count-label">[[changeSection.countLabel]]</span> </a> @@ -78,9 +62,8 @@ <template is="dom-if" if="[[_isEmpty(changeSection)]]"> <tr class="noChanges"> <td class="leftPadding"></td> - <td class="star" hidden$="[[!showStar]]" hidden></td> - <td class="cell" - colspan$="[[_computeColspan(changeTableColumns, labelNames)]]"> + <td class="star" hidden\$="[[!showStar]]" hidden=""></td> + <td class="cell" colspan\$="[[_computeColspan(changeTableColumns, labelNames)]]"> <template is="dom-if" if="[[_isOutgoing(changeSection)]]"> <slot name="empty-outgoing"></slot> </template> @@ -93,48 +76,31 @@ <template is="dom-if" if="[[!_isEmpty(changeSection)]]"> <tr class="groupTitle"> <td class="leftPadding"></td> - <td class="star" hidden$="[[!showStar]]" hidden></td> - <td class="number" hidden$="[[!showNumber]]" hidden>#</td> + <td class="star" hidden\$="[[!showStar]]" hidden=""></td> + <td class="number" hidden\$="[[!showNumber]]" hidden="">#</td> <template is="dom-repeat" items="[[changeTableColumns]]" as="item"> - <td class$="[[_lowerCase(item)]]" - hidden$="[[isColumnHidden(item, visibleChangeTableColumns)]]"> + <td class\$="[[_lowerCase(item)]]" hidden\$="[[isColumnHidden(item, visibleChangeTableColumns)]]"> [[item]] </td> </template> <template is="dom-repeat" items="[[labelNames]]" as="labelName"> - <td class="label" title$="[[labelName]]"> + <td class="label" title\$="[[labelName]]"> [[_computeLabelShortcut(labelName)]] </td> </template> - <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" - as="pluginHeader"> + <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="pluginHeader"> <td class="endpoint"> - <gr-endpoint-decorator name$="[[pluginHeader]]"> + <gr-endpoint-decorator name\$="[[pluginHeader]]"> </gr-endpoint-decorator> </td> </template> </tr> </template> <template is="dom-repeat" items="[[changeSection.results]]" as="change"> - <gr-change-list-item - selected$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]" - highlight$="[[_computeItemHighlight(account, change)]]" - needs-review$="[[_computeItemNeedsReview(account, change, showReviewedState)]]" - change="[[change]]" - visible-change-table-columns="[[visibleChangeTableColumns]]" - show-number="[[showNumber]]" - show-star="[[showStar]]" - tabindex="0" - label-names="[[labelNames]]"></gr-change-list-item> + <gr-change-list-item selected\$="[[_computeItemSelected(sectionIndex, index, selectedIndex)]]" highlight\$="[[_computeItemHighlight(account, change)]]" needs-review\$="[[_computeItemNeedsReview(account, change, showReviewedState)]]" change="[[change]]" visible-change-table-columns="[[visibleChangeTableColumns]]" show-number="[[showNumber]]" show-star="[[showStar]]" tabindex="0" label-names="[[labelNames]]"></gr-change-list-item> </template> </tbody> </template> </table> - <gr-cursor-manager - id="cursor" - index="{{selectedIndex}}" - scroll-behavior="keep-visible" - focus-on-move></gr-cursor-manager> - </template> - <script src="gr-change-list.js"></script> -</dom-module> + <gr-cursor-manager id="cursor" index="{{selectedIndex}}" scroll-behavior="keep-visible" focus-on-move=""></gr-cursor-manager> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html index 23a5046..6329666 100644 --- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -19,17 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="gr-change-list.html"> +<script type="module" src="./gr-change-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -43,20 +48,420 @@ </template> </test-fixture> -<script> - suite('gr-change-list basic tests', async () => { - await readyToTest(); - // Define keybindings before attaching other fixtures. - const kb = window.Gerrit.KeyboardShortcutBinder; - kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j'); - kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k'); - kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o'); - kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r'); - kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r'); - kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's'); - kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n'); - kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p'); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js'; +suite('gr-change-list basic tests', () => { + // Define keybindings before attaching other fixtures. + const kb = window.Gerrit.KeyboardShortcutBinder; + kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_CHANGE, 'j'); + kb.bindShortcut(kb.Shortcut.CURSOR_PREV_CHANGE, 'k'); + kb.bindShortcut(kb.Shortcut.OPEN_CHANGE, 'o'); + kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE_LIST, 'shift+r'); + kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r'); + kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's'); + kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'n'); + kb.bindShortcut(kb.Shortcut.NEXT_PAGE, 'p'); + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + + suite('test show change number not logged in', () => { + setup(() => { + element = fixture('basic'); + element.account = null; + element.preferences = null; + }); + + test('show number disabled', () => { + assert.isFalse(element.showNumber); + }); + }); + + suite('test show change number preference enabled', () => { + setup(() => { + element = fixture('basic'); + element.preferences = { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [], + }; + element.account = {_account_id: 1001}; + flushAsynchronousOperations(); + }); + + test('show number enabled', () => { + assert.isTrue(element.showNumber); + }); + }); + + suite('test show change number preference disabled', () => { + setup(() => { + element = fixture('basic'); + // legacycid_in_change_table is not set when false. + element.preferences = { + time_format: 'HHMM_12', + change_table: [], + }; + element.account = {_account_id: 1001}; + flushAsynchronousOperations(); + }); + + test('show number disabled', () => { + assert.isFalse(element.showNumber); + }); + }); + + test('computed fields', () => { + assert.equal(element._computeLabelNames( + [{results: [{_number: 0, labels: {}}]}]).length, 0); + assert.equal(element._computeLabelNames([ + {results: [ + {_number: 0, labels: {Verified: {approved: {}}}}, + { + _number: 1, + labels: { + 'Verified': {approved: {}}, + 'Code-Review': {approved: {}}, + }, + }, + { + _number: 2, + labels: { + 'Verified': {approved: {}}, + 'Library-Compliance': {approved: {}}, + }, + }, + ]}, + ]).length, 3); + + assert.equal(element._computeLabelShortcut('Code-Review'), 'CR'); + assert.equal(element._computeLabelShortcut('Verified'), 'V'); + assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC'); + assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR'); + assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR'); + assert.equal(element._computeLabelShortcut( + 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V'); + assert.equal(element._computeLabelShortcut( + 'Some-Special-Label-7'), 'SSL7'); + assert.equal(element._computeLabelShortcut('--Too----many----dashes---'), + 'TMD'); + assert.equal(element._computeLabelShortcut( + 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL'); + }); + + test('colspans', () => { + element.sections = [ + {results: [{}]}, + ]; + flushAsynchronousOperations(); + const tdItemCount = dom(element.root).querySelectorAll( + 'td').length; + + const changeTableColumns = []; + const labelNames = []; + assert.equal(tdItemCount, element._computeColspan( + changeTableColumns, labelNames)); + }); + + test('keyboard shortcuts', done => { + sandbox.stub(element, '_computeLabelNames'); + element.sections = [ + {results: new Array(1)}, + {results: new Array(2)}, + ]; + element.selectedIndex = 0; + element.changes = [ + {_number: 0}, + {_number: 1}, + {_number: 2}, + ]; + flushAsynchronousOperations(); + afterNextRender(element, () => { + const elementItems = dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 3); + + assert.isTrue(elementItems[0].hasAttribute('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + assert.equal(element.selectedIndex, 1); + assert.isTrue(elementItems[1].hasAttribute('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + assert.equal(element.selectedIndex, 2); + assert.isTrue(elementItems[2].hasAttribute('selected')); + + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + assert.equal(element.selectedIndex, 2); + MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); + assert.deepEqual(navStub.lastCall.args[0], {_number: 2}, + 'Should navigate to /c/2/'); + + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); + assert.deepEqual(navStub.lastCall.args[0], {_number: 1}, + 'Should navigate to /c/1/'); + + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + assert.equal(element.selectedIndex, 0); + + const reloadStub = sandbox.stub(element, '_reloadWindow'); + MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); + assert.isTrue(reloadStub.called); + + done(); + }); + }); + + test('changes needing review', () => { + element.changes = [ + { + _number: 0, + status: 'NEW', + reviewed: true, + owner: {_account_id: 0}, + }, + { + _number: 1, + status: 'NEW', + owner: {_account_id: 0}, + }, + { + _number: 2, + status: 'MERGED', + owner: {_account_id: 0}, + }, + { + _number: 3, + status: 'ABANDONED', + owner: {_account_id: 0}, + }, + { + _number: 4, + status: 'NEW', + work_in_progress: true, + owner: {_account_id: 0}, + }, + ]; + flushAsynchronousOperations(); + let elementItems = dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 5); + for (let i = 0; i < elementItems.length; i++) { + assert.isFalse(elementItems[i].hasAttribute('needs-review')); + } + + element.showReviewedState = true; + elementItems = dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 5); + assert.isFalse(elementItems[0].hasAttribute('needs-review')); + assert.isTrue(elementItems[1].hasAttribute('needs-review')); + assert.isFalse(elementItems[2].hasAttribute('needs-review')); + assert.isFalse(elementItems[3].hasAttribute('needs-review')); + assert.isFalse(elementItems[4].hasAttribute('needs-review')); + + element.account = {_account_id: 42}; + elementItems = dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(elementItems.length, 5); + assert.isFalse(elementItems[0].hasAttribute('needs-review')); + assert.isTrue(elementItems[1].hasAttribute('needs-review')); + assert.isFalse(elementItems[2].hasAttribute('needs-review')); + assert.isFalse(elementItems[3].hasAttribute('needs-review')); + assert.isFalse(elementItems[4].hasAttribute('needs-review')); + }); + + test('no changes', () => { + element.changes = []; + flushAsynchronousOperations(); + const listItems = dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(listItems.length, 0); + const noChangesMsg = + dom(element.root).querySelector('.noChanges'); + assert.ok(noChangesMsg); + }); + + test('empty sections', () => { + element.sections = [{results: []}, {results: []}]; + flushAsynchronousOperations(); + const listItems = dom(element.root).querySelectorAll( + 'gr-change-list-item'); + assert.equal(listItems.length, 0); + const noChangesMsg = dom(element.root).querySelectorAll( + '.noChanges'); + assert.equal(noChangesMsg.length, 2); + }); + + suite('empty outgoing', () => { + test('not shown on empty non-outgoing sections', () => { + const section = {results: []}; + assert.isTrue(element._isEmpty(section)); + assert.isFalse(element._isOutgoing(section)); + }); + + test('shown on empty outgoing sections', () => { + const section = {results: [], isOutgoing: true}; + assert.isTrue(element._isEmpty(section)); + assert.isTrue(element._isOutgoing(section)); + }); + + test('not shown on non-empty outgoing sections', () => { + const section = {isOutgoing: true, results: [ + {_number: 0, labels: {Verified: {approved: {}}}}]}; + assert.isFalse(element._isEmpty(section)); + assert.isTrue(element._isOutgoing(section)); + }); + }); + + test('_isOutgoing', () => { + assert.isTrue(element._isOutgoing({results: [], isOutgoing: true})); + assert.isFalse(element._isOutgoing({results: []})); + }); + + suite('empty column preference', () => { + let element; + + setup(() => { + element = fixture('basic'); + element.sections = [ + {results: [{}]}, + ]; + element.account = {_account_id: 1001}; + element.preferences = { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [], + }; + flushAsynchronousOperations(); + }); + + test('show number enabled', () => { + assert.isTrue(element.showNumber); + }); + + test('all columns visible', () => { + for (const column of element.columnNames) { + const elementClass = '.' + element._lowerCase(column); + assert.isFalse(element.shadowRoot + .querySelector(elementClass).hidden); + } + }); + }); + + suite('full column preference', () => { + let element; + + setup(() => { + element = fixture('basic'); + element.sections = [ + {results: [{}]}, + ]; + element.account = {_account_id: 1001}; + element.preferences = { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Subject', + 'Status', + 'Owner', + 'Assignee', + 'Repo', + 'Branch', + 'Updated', + 'Size', + ], + }; + flushAsynchronousOperations(); + }); + + test('all columns visible', () => { + for (const column of element.changeTableColumns) { + const elementClass = '.' + element._lowerCase(column); + assert.isFalse(element.shadowRoot + .querySelector(elementClass).hidden); + } + }); + }); + + suite('partial column preference', () => { + let element; + + setup(() => { + element = fixture('basic'); + element.sections = [ + {results: [{}]}, + ]; + element.account = {_account_id: 1001}; + element.preferences = { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Subject', + 'Status', + 'Owner', + 'Assignee', + 'Branch', + 'Updated', + 'Size', + ], + }; + flushAsynchronousOperations(); + }); + + test('all columns except repo visible', () => { + for (const column of element.changeTableColumns) { + const elementClass = '.' + column.toLowerCase(); + if (column === 'Repo') { + assert.isTrue(element.shadowRoot + .querySelector(elementClass).hidden); + } else { + assert.isFalse(element.shadowRoot + .querySelector(elementClass).hidden); + } + } + }); + }); + + suite('random column does not exist', () => { + let element; + + /* This would only exist if somebody manually updated the config + file. */ + setup(() => { + element = fixture('basic'); + element.account = {_account_id: 1001}; + element.preferences = { + legacycid_in_change_table: true, + time_format: 'HHMM_12', + change_table: [ + 'Bad', + ], + }; + flushAsynchronousOperations(); + }); + + test('bad column does not exist', () => { + const elementClass = '.bad'; + assert.isNotOk(element.shadowRoot + .querySelector(elementClass)); + }); + }); + + suite('dashboard queries', () => { let element; let sandbox; @@ -67,576 +472,180 @@ teardown(() => { sandbox.restore(); }); - suite('test show change number not logged in', () => { - setup(() => { - element = fixture('basic'); - element.account = null; - element.preferences = null; - }); - - test('show number disabled', () => { - assert.isFalse(element.showNumber); - }); + test('query without age and limit unchanged', () => { + const query = 'status:closed owner:me'; + assert.deepEqual(element._processQuery(query), query); }); - suite('test show change number preference enabled', () => { - setup(() => { - element = fixture('basic'); - element.preferences = { - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [], - }; - element.account = {_account_id: 1001}; - flushAsynchronousOperations(); - }); - - test('show number enabled', () => { - assert.isTrue(element.showNumber); - }); + test('query with age and limit', () => { + const query = 'status:closed age:1week limit:10 owner:me'; + const expectedQuery = 'status:closed owner:me'; + assert.deepEqual(element._processQuery(query), expectedQuery); }); - suite('test show change number preference disabled', () => { - setup(() => { - element = fixture('basic'); - // legacycid_in_change_table is not set when false. - element.preferences = { - time_format: 'HHMM_12', - change_table: [], - }; - element.account = {_account_id: 1001}; - flushAsynchronousOperations(); - }); - - test('show number disabled', () => { - assert.isFalse(element.showNumber); - }); + test('query with age', () => { + const query = 'status:closed age:1week owner:me'; + const expectedQuery = 'status:closed owner:me'; + assert.deepEqual(element._processQuery(query), expectedQuery); }); - test('computed fields', () => { - assert.equal(element._computeLabelNames( - [{results: [{_number: 0, labels: {}}]}]).length, 0); - assert.equal(element._computeLabelNames([ - {results: [ - {_number: 0, labels: {Verified: {approved: {}}}}, - { - _number: 1, - labels: { - 'Verified': {approved: {}}, - 'Code-Review': {approved: {}}, - }, - }, - { - _number: 2, - labels: { - 'Verified': {approved: {}}, - 'Library-Compliance': {approved: {}}, - }, - }, - ]}, - ]).length, 3); - - assert.equal(element._computeLabelShortcut('Code-Review'), 'CR'); - assert.equal(element._computeLabelShortcut('Verified'), 'V'); - assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC'); - assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR'); - assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR'); - assert.equal(element._computeLabelShortcut( - 'Invalid-Prolog-Rules-Label-Name--Verified'), 'V'); - assert.equal(element._computeLabelShortcut( - 'Some-Special-Label-7'), 'SSL7'); - assert.equal(element._computeLabelShortcut('--Too----many----dashes---'), - 'TMD'); - assert.equal(element._computeLabelShortcut( - 'Really-rather-entirely-too-long-of-a-label-name'), 'RRETL'); + test('query with limit', () => { + const query = 'status:closed limit:10 owner:me'; + const expectedQuery = 'status:closed owner:me'; + assert.deepEqual(element._processQuery(query), expectedQuery); }); - test('colspans', () => { - element.sections = [ - {results: [{}]}, - ]; - flushAsynchronousOperations(); - const tdItemCount = Polymer.dom(element.root).querySelectorAll( - 'td').length; - - const changeTableColumns = []; - const labelNames = []; - assert.equal(tdItemCount, element._computeColspan( - changeTableColumns, labelNames)); + test('query with age as value and not key', () => { + const query = 'status:closed random:age'; + const expectedQuery = 'status:closed random:age'; + assert.deepEqual(element._processQuery(query), expectedQuery); }); + test('query with limit as value and not key', () => { + const query = 'status:closed random:limit'; + const expectedQuery = 'status:closed random:limit'; + assert.deepEqual(element._processQuery(query), expectedQuery); + }); + + test('query with -age key', () => { + const query = 'status:closed -age:1week'; + const expectedQuery = 'status:closed'; + assert.deepEqual(element._processQuery(query), expectedQuery); + }); + }); + + suite('gr-change-list sections', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + test('keyboard shortcuts', done => { - sandbox.stub(element, '_computeLabelNames'); - element.sections = [ - {results: new Array(1)}, - {results: new Array(2)}, - ]; element.selectedIndex = 0; - element.changes = [ - {_number: 0}, - {_number: 1}, - {_number: 2}, + element.sections = [ + { + results: [ + {_number: 0}, + {_number: 1}, + {_number: 2}, + ], + }, + { + results: [ + {_number: 3}, + {_number: 4}, + {_number: 5}, + ], + }, + { + results: [ + {_number: 6}, + {_number: 7}, + {_number: 8}, + ], + }, ]; flushAsynchronousOperations(); - Polymer.RenderStatus.afterNextRender(element, () => { - const elementItems = Polymer.dom(element.root).querySelectorAll( + afterNextRender(element, () => { + const elementItems = dom(element.root).querySelectorAll( 'gr-change-list-item'); - assert.equal(elementItems.length, 3); + assert.equal(elementItems.length, 9); - assert.isTrue(elementItems[0].hasAttribute('selected')); - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' assert.equal(element.selectedIndex, 1); - assert.isTrue(elementItems[1].hasAttribute('selected')); - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - assert.equal(element.selectedIndex, 2); - assert.isTrue(elementItems[2].hasAttribute('selected')); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); assert.equal(element.selectedIndex, 2); - MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); + + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' assert.deepEqual(navStub.lastCall.args[0], {_number: 2}, 'Should navigate to /c/2/'); - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' assert.deepEqual(navStub.lastCall.args[0], {_number: 1}, 'Should navigate to /c/1/'); - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - assert.equal(element.selectedIndex, 0); + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' + assert.equal(element.selectedIndex, 4); + MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' + assert.deepEqual(navStub.lastCall.args[0], {_number: 4}, + 'Should navigate to /c/4/'); - const reloadStub = sandbox.stub(element, '_reloadWindow'); - MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); - assert.isTrue(reloadStub.called); - + MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r' + const change = element._changeForIndex(element.selectedIndex); + assert.equal(change.reviewed, true, + 'Should mark change as reviewed'); + MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r' + assert.equal(change.reviewed, false, + 'Should mark change as unreviewed'); done(); }); }); - test('changes needing review', () => { + test('highlight attribute is updated correctly', () => { element.changes = [ { _number: 0, status: 'NEW', - reviewed: true, owner: {_account_id: 0}, }, { _number: 1, - status: 'NEW', - owner: {_account_id: 0}, - }, - { - _number: 2, - status: 'MERGED', - owner: {_account_id: 0}, - }, - { - _number: 3, status: 'ABANDONED', owner: {_account_id: 0}, }, - { - _number: 4, - status: 'NEW', - work_in_progress: true, - owner: {_account_id: 0}, - }, ]; - flushAsynchronousOperations(); - let elementItems = Polymer.dom(element.root).querySelectorAll( - 'gr-change-list-item'); - assert.equal(elementItems.length, 5); - for (let i = 0; i < elementItems.length; i++) { - assert.isFalse(elementItems[i].hasAttribute('needs-review')); - } - - element.showReviewedState = true; - elementItems = Polymer.dom(element.root).querySelectorAll( - 'gr-change-list-item'); - assert.equal(elementItems.length, 5); - assert.isFalse(elementItems[0].hasAttribute('needs-review')); - assert.isTrue(elementItems[1].hasAttribute('needs-review')); - assert.isFalse(elementItems[2].hasAttribute('needs-review')); - assert.isFalse(elementItems[3].hasAttribute('needs-review')); - assert.isFalse(elementItems[4].hasAttribute('needs-review')); - element.account = {_account_id: 42}; - elementItems = Polymer.dom(element.root).querySelectorAll( - 'gr-change-list-item'); - assert.equal(elementItems.length, 5); - assert.isFalse(elementItems[0].hasAttribute('needs-review')); - assert.isTrue(elementItems[1].hasAttribute('needs-review')); - assert.isFalse(elementItems[2].hasAttribute('needs-review')); - assert.isFalse(elementItems[3].hasAttribute('needs-review')); - assert.isFalse(elementItems[4].hasAttribute('needs-review')); - }); - - test('no changes', () => { - element.changes = []; flushAsynchronousOperations(); - const listItems = Polymer.dom(element.root).querySelectorAll( - 'gr-change-list-item'); - assert.equal(listItems.length, 0); - const noChangesMsg = - Polymer.dom(element.root).querySelector('.noChanges'); - assert.ok(noChangesMsg); - }); + let items = element._getListItems(); + assert.equal(items.length, 2); + assert.isFalse(items[0].hasAttribute('highlight')); + assert.isFalse(items[1].hasAttribute('highlight')); - test('empty sections', () => { - element.sections = [{results: []}, {results: []}]; + // Assign all issues to the user, but only the first one is highlighted + // because the second one is abandoned. + element.set(['changes', 0, 'assignee'], {_account_id: 12}); + element.set(['changes', 1, 'assignee'], {_account_id: 12}); + element.account = {_account_id: 12}; flushAsynchronousOperations(); - const listItems = Polymer.dom(element.root).querySelectorAll( - 'gr-change-list-item'); - assert.equal(listItems.length, 0); - const noChangesMsg = Polymer.dom(element.root).querySelectorAll( - '.noChanges'); - assert.equal(noChangesMsg.length, 2); + items = element._getListItems(); + assert.isTrue(items[0].hasAttribute('highlight')); + assert.isFalse(items[1].hasAttribute('highlight')); }); - suite('empty outgoing', () => { - test('not shown on empty non-outgoing sections', () => { - const section = {results: []}; - assert.isTrue(element._isEmpty(section)); - assert.isFalse(element._isOutgoing(section)); - }); - - test('shown on empty outgoing sections', () => { - const section = {results: [], isOutgoing: true}; - assert.isTrue(element._isEmpty(section)); - assert.isTrue(element._isOutgoing(section)); - }); - - test('not shown on non-empty outgoing sections', () => { - const section = {isOutgoing: true, results: [ - {_number: 0, labels: {Verified: {approved: {}}}}]}; - assert.isFalse(element._isEmpty(section)); - assert.isTrue(element._isOutgoing(section)); - }); + test('_computeItemHighlight gives false for null account', () => { + assert.isFalse( + element._computeItemHighlight(null, {assignee: {_account_id: 42}})); }); - test('_isOutgoing', () => { - assert.isTrue(element._isOutgoing({results: [], isOutgoing: true})); - assert.isFalse(element._isOutgoing({results: []})); - }); + test('_computeItemAbsoluteIndex', () => { + sandbox.stub(element, '_computeLabelNames'); + element.sections = [ + {results: new Array(1)}, + {results: new Array(2)}, + {results: new Array(3)}, + ]; - suite('empty column preference', () => { - let element; + assert.equal(element._computeItemAbsoluteIndex(0, 0), 0); + // Out of range but no matter. + assert.equal(element._computeItemAbsoluteIndex(0, 1), 1); - setup(() => { - element = fixture('basic'); - element.sections = [ - {results: [{}]}, - ]; - element.account = {_account_id: 1001}; - element.preferences = { - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [], - }; - flushAsynchronousOperations(); - }); - - test('show number enabled', () => { - assert.isTrue(element.showNumber); - }); - - test('all columns visible', () => { - for (const column of element.columnNames) { - const elementClass = '.' + element._lowerCase(column); - assert.isFalse(element.shadowRoot - .querySelector(elementClass).hidden); - } - }); - }); - - suite('full column preference', () => { - let element; - - setup(() => { - element = fixture('basic'); - element.sections = [ - {results: [{}]}, - ]; - element.account = {_account_id: 1001}; - element.preferences = { - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [ - 'Subject', - 'Status', - 'Owner', - 'Assignee', - 'Repo', - 'Branch', - 'Updated', - 'Size', - ], - }; - flushAsynchronousOperations(); - }); - - test('all columns visible', () => { - for (const column of element.changeTableColumns) { - const elementClass = '.' + element._lowerCase(column); - assert.isFalse(element.shadowRoot - .querySelector(elementClass).hidden); - } - }); - }); - - suite('partial column preference', () => { - let element; - - setup(() => { - element = fixture('basic'); - element.sections = [ - {results: [{}]}, - ]; - element.account = {_account_id: 1001}; - element.preferences = { - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [ - 'Subject', - 'Status', - 'Owner', - 'Assignee', - 'Branch', - 'Updated', - 'Size', - ], - }; - flushAsynchronousOperations(); - }); - - test('all columns except repo visible', () => { - for (const column of element.changeTableColumns) { - const elementClass = '.' + column.toLowerCase(); - if (column === 'Repo') { - assert.isTrue(element.shadowRoot - .querySelector(elementClass).hidden); - } else { - assert.isFalse(element.shadowRoot - .querySelector(elementClass).hidden); - } - } - }); - }); - - suite('random column does not exist', () => { - let element; - - /* This would only exist if somebody manually updated the config - file. */ - setup(() => { - element = fixture('basic'); - element.account = {_account_id: 1001}; - element.preferences = { - legacycid_in_change_table: true, - time_format: 'HHMM_12', - change_table: [ - 'Bad', - ], - }; - flushAsynchronousOperations(); - }); - - test('bad column does not exist', () => { - const elementClass = '.bad'; - assert.isNotOk(element.shadowRoot - .querySelector(elementClass)); - }); - }); - - suite('dashboard queries', () => { - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('query without age and limit unchanged', () => { - const query = 'status:closed owner:me'; - assert.deepEqual(element._processQuery(query), query); - }); - - test('query with age and limit', () => { - const query = 'status:closed age:1week limit:10 owner:me'; - const expectedQuery = 'status:closed owner:me'; - assert.deepEqual(element._processQuery(query), expectedQuery); - }); - - test('query with age', () => { - const query = 'status:closed age:1week owner:me'; - const expectedQuery = 'status:closed owner:me'; - assert.deepEqual(element._processQuery(query), expectedQuery); - }); - - test('query with limit', () => { - const query = 'status:closed limit:10 owner:me'; - const expectedQuery = 'status:closed owner:me'; - assert.deepEqual(element._processQuery(query), expectedQuery); - }); - - test('query with age as value and not key', () => { - const query = 'status:closed random:age'; - const expectedQuery = 'status:closed random:age'; - assert.deepEqual(element._processQuery(query), expectedQuery); - }); - - test('query with limit as value and not key', () => { - const query = 'status:closed random:limit'; - const expectedQuery = 'status:closed random:limit'; - assert.deepEqual(element._processQuery(query), expectedQuery); - }); - - test('query with -age key', () => { - const query = 'status:closed -age:1week'; - const expectedQuery = 'status:closed'; - assert.deepEqual(element._processQuery(query), expectedQuery); - }); - }); - - suite('gr-change-list sections', () => { - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('keyboard shortcuts', done => { - element.selectedIndex = 0; - element.sections = [ - { - results: [ - {_number: 0}, - {_number: 1}, - {_number: 2}, - ], - }, - { - results: [ - {_number: 3}, - {_number: 4}, - {_number: 5}, - ], - }, - { - results: [ - {_number: 6}, - {_number: 7}, - {_number: 8}, - ], - }, - ]; - flushAsynchronousOperations(); - Polymer.RenderStatus.afterNextRender(element, () => { - const elementItems = Polymer.dom(element.root).querySelectorAll( - 'gr-change-list-item'); - assert.equal(elementItems.length, 9); - - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' - assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' - - const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - assert.equal(element.selectedIndex, 2); - - MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' - assert.deepEqual(navStub.lastCall.args[0], {_number: 2}, - 'Should navigate to /c/2/'); - - MockInteractions.pressAndReleaseKeyOn(element, 75); // 'k' - assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' - assert.deepEqual(navStub.lastCall.args[0], {_number: 1}, - 'Should navigate to /c/1/'); - - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' - MockInteractions.pressAndReleaseKeyOn(element, 74); // 'j' - assert.equal(element.selectedIndex, 4); - MockInteractions.pressAndReleaseKeyOn(element, 13); // 'enter' - assert.deepEqual(navStub.lastCall.args[0], {_number: 4}, - 'Should navigate to /c/4/'); - - MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r' - const change = element._changeForIndex(element.selectedIndex); - assert.equal(change.reviewed, true, - 'Should mark change as reviewed'); - MockInteractions.pressAndReleaseKeyOn(element, 82); // 'r' - assert.equal(change.reviewed, false, - 'Should mark change as unreviewed'); - done(); - }); - }); - - test('highlight attribute is updated correctly', () => { - element.changes = [ - { - _number: 0, - status: 'NEW', - owner: {_account_id: 0}, - }, - { - _number: 1, - status: 'ABANDONED', - owner: {_account_id: 0}, - }, - ]; - element.account = {_account_id: 42}; - flushAsynchronousOperations(); - let items = element._getListItems(); - assert.equal(items.length, 2); - assert.isFalse(items[0].hasAttribute('highlight')); - assert.isFalse(items[1].hasAttribute('highlight')); - - // Assign all issues to the user, but only the first one is highlighted - // because the second one is abandoned. - element.set(['changes', 0, 'assignee'], {_account_id: 12}); - element.set(['changes', 1, 'assignee'], {_account_id: 12}); - element.account = {_account_id: 12}; - flushAsynchronousOperations(); - items = element._getListItems(); - assert.isTrue(items[0].hasAttribute('highlight')); - assert.isFalse(items[1].hasAttribute('highlight')); - }); - - test('_computeItemHighlight gives false for null account', () => { - assert.isFalse( - element._computeItemHighlight(null, {assignee: {_account_id: 42}})); - }); - - test('_computeItemAbsoluteIndex', () => { - sandbox.stub(element, '_computeLabelNames'); - element.sections = [ - {results: new Array(1)}, - {results: new Array(2)}, - {results: new Array(3)}, - ]; - - assert.equal(element._computeItemAbsoluteIndex(0, 0), 0); - // Out of range but no matter. - assert.equal(element._computeItemAbsoluteIndex(0, 1), 1); - - assert.equal(element._computeItemAbsoluteIndex(1, 0), 1); - assert.equal(element._computeItemAbsoluteIndex(1, 1), 2); - assert.equal(element._computeItemAbsoluteIndex(1, 2), 3); - assert.equal(element._computeItemAbsoluteIndex(2, 0), 3); - assert.equal(element._computeItemAbsoluteIndex(3, 0), 6); - }); + assert.equal(element._computeItemAbsoluteIndex(1, 0), 1); + assert.equal(element._computeItemAbsoluteIndex(1, 1), 2); + assert.equal(element._computeItemAbsoluteIndex(1, 2), 3); + assert.equal(element._computeItemAbsoluteIndex(2, 0), 3); + assert.equal(element._computeItemAbsoluteIndex(3, 0), 6); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js index 64d2486..3758a78 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help.js
@@ -14,27 +14,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrCreateChangeHelp extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-create-change-help'; } +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-icons/gr-icons.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-change-help_html.js'; - /** - * Fired when the "Create change" button is tapped. - * - * @event create-tap - */ +/** @extends Polymer.Element */ +class GrCreateChangeHelp extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _handleCreateTap(e) { - e.preventDefault(); - this.dispatchEvent( - new CustomEvent('create-tap', {bubbles: true, composed: true})); - } + static get is() { return 'gr-create-change-help'; } + + /** + * Fired when the "Create change" button is tapped. + * + * @event create-tap + */ + + _handleCreateTap(e) { + e.preventDefault(); + this.dispatchEvent( + new CustomEvent('create-tap', {bubbles: true, composed: true})); } +} - customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp); -})(); +customElements.define(GrCreateChangeHelp.is, GrCreateChangeHelp);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js index 842c402..f40bda7 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> - -<dom-module id="gr-create-change-help"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -82,11 +76,9 @@ <h1>Push your first change for code review</h1> <p> Pushing a change for review is easy, but a little different from - other git code review tools. Click on the `Create Change' button + other git code review tools. Click on the \`Create Change' button and follow the step by step instructions. </p> <gr-button on-click="_handleCreateTap">Create Change</gr-button> </div> - </template> - <script src="gr-create-change-help.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html index abc4a9b..3cbab30 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-create-change-help/gr-create-change-help_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-change-help</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-create-change-help.html"> +<script type="module" src="./gr-create-change-help.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-create-change-help.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,19 +43,22 @@ </template> </test-fixture> -<script> - suite('gr-create-change-help tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-create-change-help.js'; +suite('gr-create-change-help tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); - - test('Create change tap', done => { - element.addEventListener('create-tap', () => done()); - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button')); - }); + setup(() => { + element = fixture('basic'); }); + + test('Create change tap', done => { + element.addEventListener('create-tap', () => done()); + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js index 7abd784..7e5e749 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog.js
@@ -14,53 +14,61 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const Commands = { - CREATE: 'git commit', - AMEND: 'git commit --amend', - PUSH_PREFIX: 'git push origin HEAD:refs/for/', - }; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-shell-command/gr-shell-command.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-commands-dialog_html.js'; - /** @extends Polymer.Element */ - class GrCreateCommandsDialog extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-create-commands-dialog'; } +const Commands = { + CREATE: 'git commit', + AMEND: 'git commit --amend', + PUSH_PREFIX: 'git push origin HEAD:refs/for/', +}; - static get properties() { - return { - branch: String, - _createNewCommitCommand: { - type: String, - readonly: true, - value: Commands.CREATE, - }, - _amendExistingCommitCommand: { - type: String, - readonly: true, - value: Commands.AMEND, - }, - _pushCommand: { - type: String, - computed: '_computePushCommand(branch)', - }, - }; - } +/** @extends Polymer.Element */ +class GrCreateCommandsDialog extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - open() { - this.$.commandsOverlay.open(); - } + static get is() { return 'gr-create-commands-dialog'; } - _handleClose() { - this.$.commandsOverlay.close(); - } - - _computePushCommand(branch) { - return Commands.PUSH_PREFIX + branch; - } + static get properties() { + return { + branch: String, + _createNewCommitCommand: { + type: String, + readonly: true, + value: Commands.CREATE, + }, + _amendExistingCommitCommand: { + type: String, + readonly: true, + value: Commands.AMEND, + }, + _pushCommand: { + type: String, + computed: '_computePushCommand(branch)', + }, + }; } - customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog); -})(); + open() { + this.$.commandsOverlay.open(); + } + + _handleClose() { + this.$.commandsOverlay.close(); + } + + _computePushCommand(branch) { + return Commands.PUSH_PREFIX + branch; + } +} + +customElements.define(GrCreateCommandsDialog.is, GrCreateCommandsDialog);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js index 9e86058..aa13dca 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html"> - -<dom-module id="gr-create-commands-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> ol { list-style: decimal; @@ -34,13 +29,8 @@ max-width: 40em; } </style> - <gr-overlay id="commandsOverlay" with-backdrop> - <gr-dialog - id="commandsDialog" - confirm-label="Done" - cancel-label="" - confirm-on-enter - on-confirm="_handleClose"> + <gr-overlay id="commandsOverlay" with-backdrop=""> + <gr-dialog id="commandsDialog" confirm-label="Done" cancel-label="" confirm-on-enter="" on-confirm="_handleClose"> <div class="header" slot="header"> Create change commands </div> @@ -82,6 +72,4 @@ </div> </gr-dialog> </gr-overlay> - </template> - <script src="gr-create-commands-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html index 41709a6..f992b0b 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-create-commands-dialog/gr-create-commands-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-create-commands-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-create-commands-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-create-commands-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-commands-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,23 +40,25 @@ </template> </test-fixture> -<script> - suite('gr-create-commands-dialog tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-create-commands-dialog.js'; +suite('gr-create-commands-dialog tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); - - test('_computePushCommand', () => { - element.branch = 'master'; - assert.equal(element._pushCommand, - 'git push origin HEAD:refs/for/master'); - - element.branch = 'stable-2.15'; - assert.equal(element._pushCommand, - 'git push origin HEAD:refs/for/stable-2.15'); - }); + setup(() => { + element = fixture('basic'); }); + + test('_computePushCommand', () => { + element.branch = 'master'; + assert.equal(element._pushCommand, + 'git push origin HEAD:refs/for/master'); + + element.branch = 'stable-2.15'; + assert.equal(element._pushCommand, + 'git push origin HEAD:refs/for/stable-2.15'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js index 35f7450..f8757ba 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog.js
@@ -14,58 +14,66 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * Fired when a destination has been picked. Event details contain the repo - * name and the branch name. - * - * @event confirm - * @extends Polymer.Element - */ - class GrCreateDestinationDialog extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-create-destination-dialog'; } +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-repo-branch-picker/gr-repo-branch-picker.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-create-destination-dialog_html.js'; - static get properties() { - return { - _repo: String, - _branch: String, - _repoAndBranchSelected: { - type: Boolean, - value: false, - computed: '_computeRepoAndBranchSelected(_repo, _branch)', - }, - }; - } +/** + * Fired when a destination has been picked. Event details contain the repo + * name and the branch name. + * + * @event confirm + * @extends Polymer.Element + */ +class GrCreateDestinationDialog extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - open() { - this._repo = ''; - this._branch = ''; - this.$.createOverlay.open(); - } + static get is() { return 'gr-create-destination-dialog'; } - _handleClose() { - this.$.createOverlay.close(); - } - - _pickerConfirm(e) { - this.$.createOverlay.close(); - const detail = {repo: this._repo, branch: this._branch}; - // e is a 'confirm' event from gr-dialog. We want to fire a more detailed - // 'confirm' event here, so let's stop propagation of the bare event. - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false})); - } - - _computeRepoAndBranchSelected(repo, branch) { - return !!(repo && branch); - } + static get properties() { + return { + _repo: String, + _branch: String, + _repoAndBranchSelected: { + type: Boolean, + value: false, + computed: '_computeRepoAndBranchSelected(_repo, _branch)', + }, + }; } - customElements.define(GrCreateDestinationDialog.is, - GrCreateDestinationDialog); -})(); + open() { + this._repo = ''; + this._branch = ''; + this.$.createOverlay.open(); + } + + _handleClose() { + this.$.createOverlay.close(); + } + + _pickerConfirm(e) { + this.$.createOverlay.close(); + const detail = {repo: this._repo, branch: this._branch}; + // e is a 'confirm' event from gr-dialog. We want to fire a more detailed + // 'confirm' event here, so let's stop propagation of the bare event. + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('confirm', {detail, bubbles: false})); + } + + _computeRepoAndBranchSelected(repo, branch) { + return !!(repo && branch); + } +} + +customElements.define(GrCreateDestinationDialog.is, + GrCreateDestinationDialog);
diff --git a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js index def5228..73f6ec0 100644 --- a/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-create-destination-dialog/gr-create-destination-dialog_html.js
@@ -1,48 +1,35 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-repo-branch-picker/gr-repo-branch-picker.html"> - -<dom-module id="gr-create-destination-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> </style> - <gr-overlay id="createOverlay" with-backdrop> - <gr-dialog - confirm-label="View commands" - on-confirm="_pickerConfirm" - on-cancel="_handleClose" - disabled="[[!_repoAndBranchSelected]]"> + <gr-overlay id="createOverlay" with-backdrop=""> + <gr-dialog confirm-label="View commands" on-confirm="_pickerConfirm" on-cancel="_handleClose" disabled="[[!_repoAndBranchSelected]]"> <div class="header" slot="header"> Create change </div> <div class="main" slot="main"> - <gr-repo-branch-picker - repo="{{_repo}}" - branch="{{_branch}}"></gr-repo-branch-picker> + <gr-repo-branch-picker repo="{{_repo}}" branch="{{_branch}}"></gr-repo-branch-picker> <p> If you haven't done so, you will need to clone the repository. </p> </div> </gr-dialog> </gr-overlay> - </template> - <script src="gr-create-destination-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js index d0e1db2..a4ed814 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,306 +14,325 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-change-list/gr-change-list.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-create-commands-dialog/gr-create-commands-dialog.js'; +import '../gr-create-change-help/gr-create-change-help.js'; +import '../gr-create-destination-dialog/gr-create-destination-dialog.js'; +import '../gr-user-header/gr-user-header.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-dashboard-view_html.js'; +const PROJECT_PLACEHOLDER_PATTERN = /\$\{project\}/g; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrDashboardView extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-dashboard-view'; } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when the title of the page should change. + * + * @event title-change */ - class GrDashboardView extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-dashboard-view'; } - /** - * Fired when the title of the page should change. - * - * @event title-change - */ - static get properties() { - return { - account: { - type: Object, - value: null, + static get properties() { + return { + account: { + type: Object, + value: null, + }, + preferences: Object, + /** @type {{ selectedChangeIndex: number }} */ + viewState: Object, + + /** @type {{ project: string, user: string }} */ + params: { + type: Object, + }, + + createChangeTap: { + type: Function, + value() { + return this._createChangeTap.bind(this); }, - preferences: Object, - /** @type {{ selectedChangeIndex: number }} */ - viewState: Object, + }, - /** @type {{ project: string, user: string }} */ - params: { - type: Object, - }, + _results: Array, - createChangeTap: { - type: Function, - value() { - return this._createChangeTap.bind(this); - }, - }, + /** + * For showing a "loading..." string during ajax requests. + */ + _loading: { + type: Boolean, + value: true, + }, - _results: Array, + _showDraftsBanner: { + type: Boolean, + value: false, + }, - /** - * For showing a "loading..." string during ajax requests. - */ - _loading: { - type: Boolean, - value: true, - }, - - _showDraftsBanner: { - type: Boolean, - value: false, - }, - - _showNewUserHelp: { - type: Boolean, - value: false, - }, - }; - } - - static get observers() { - return [ - '_paramsChanged(params.*)', - ]; - } - - get options() { - return this.listChangesOptionsToHex( - this.ListChangesOption.LABELS, - this.ListChangesOption.DETAILED_ACCOUNTS, - this.ListChangesOption.REVIEWED - ); - } - - /** @override */ - attached() { - super.attached(); - this._loadPreferences(); - } - - _loadPreferences() { - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (loggedIn) { - this.$.restAPI.getPreferences().then(preferences => { - this.preferences = preferences; - }); - } else { - this.preferences = {}; - } - }); - } - - _getProjectDashboard(project, dashboard) { - const errFn = response => { - this.fire('page-error', {response}); - }; - return this.$.restAPI.getDashboard( - project, dashboard, errFn).then(response => { - if (!response) { - return; - } - return { - title: response.title, - sections: response.sections.map(section => { - const suffix = response.foreach ? ' ' + response.foreach : ''; - return { - name: section.name, - query: (section.query + suffix).replace( - PROJECT_PLACEHOLDER_PATTERN, project), - }; - }), - }; - }); - } - - _computeTitle(user) { - if (!user || user === 'self') { - return 'My Reviews'; - } - return 'Dashboard for ' + user; - } - - _isViewActive(params) { - return params.view === Gerrit.Nav.View.DASHBOARD; - } - - _paramsChanged(paramsChangeRecord) { - const params = paramsChangeRecord.base; - - if (!this._isViewActive(params)) { - return Promise.resolve(); - } - - return this._reload(); - } - - /** - * Reloads the element. - * - * @return {Promise<!Object>} - */ - _reload() { - this._loading = true; - const {project, dashboard, title, user, sections} = this.params; - const dashboardPromise = project ? - this._getProjectDashboard(project, dashboard) : - Promise.resolve(Gerrit.Nav.getUserDashboard( - user, - sections, - title || this._computeTitle(user))); - - const checkForNewUser = !project && user === 'self'; - return dashboardPromise - .then(res => { - if (res && res.title) { - this.fire('title-change', {title: res.title}); - } - return this._fetchDashboardChanges(res, checkForNewUser); - }) - .then(() => { - this._maybeShowDraftsBanner(); - this.$.reporting.dashboardDisplayed(); - }) - .catch(err => { - this.fire('title-change', { - title: title || this._computeTitle(user), - }); - console.warn(err); - }) - .then(() => { this._loading = false; }); - } - - /** - * Fetches the changes for each dashboard section and sets this._results - * with the response. - * - * @param {!Object} res - * @param {boolean} checkForNewUser - * @return {Promise} - */ - _fetchDashboardChanges(res, checkForNewUser) { - if (!res) { return Promise.resolve(); } - - const queries = res.sections - .map(section => (section.suffixForDashboard ? - section.query + ' ' + section.suffixForDashboard : - section.query)); - - if (checkForNewUser) { - queries.push('owner:self limit:1'); - } - - return this.$.restAPI.getChanges(null, queries, null, this.options) - .then(changes => { - if (checkForNewUser) { - // Last set of results is not meant for dashboard display. - const lastResultSet = changes.pop(); - this._showNewUserHelp = lastResultSet.length == 0; - } - this._results = changes.map((results, i) => { - return { - name: res.sections[i].name, - countLabel: this._computeSectionCountLabel(results), - query: res.sections[i].query, - results, - isOutgoing: res.sections[i].isOutgoing, - }; - }).filter((section, i) => i < res.sections.length && ( - !res.sections[i].hideIfEmpty || - section.results.length)); - }); - } - - _computeSectionCountLabel(changes) { - if (!changes || !changes.length || changes.length == 0) { - return ''; - } - const more = changes[changes.length - 1]._more_changes; - const numChanges = changes.length; - const andMore = more ? ' and more' : ''; - return `(${numChanges}${andMore})`; - } - - _computeUserHeaderClass(params) { - if (!params || !!params.project || !params.user || - params.user === 'self') { - return 'hide'; - } - return ''; - } - - _handleToggleStar(e) { - this.$.restAPI.saveChangeStarred(e.detail.change._number, - e.detail.starred); - } - - _handleToggleReviewed(e) { - this.$.restAPI.saveChangeReviewed(e.detail.change._number, - e.detail.reviewed); - } - - /** - * Banner is shown if a user is on their own dashboard and they have draft - * comments on closed changes. - */ - _maybeShowDraftsBanner() { - this._showDraftsBanner = false; - if (!(this.params.user === 'self')) { return; } - - const draftSection = this._results - .find(section => section.query === 'has:draft'); - if (!draftSection || !draftSection.results.length) { return; } - - const closedChanges = draftSection.results - .filter(change => !this.changeIsOpen(change)); - if (!closedChanges.length) { return; } - - this._showDraftsBanner = true; - } - - _computeBannerClass(show) { - return show ? '' : 'hide'; - } - - _handleOpenDeleteDialog() { - this.$.confirmDeleteOverlay.open(); - } - - _handleConfirmDelete() { - this.$.confirmDeleteDialog.disabled = true; - return this.$.restAPI.deleteDraftComments('-is:open').then(() => { - this._closeConfirmDeleteOverlay(); - this._reload(); - }); - } - - _closeConfirmDeleteOverlay() { - this.$.confirmDeleteOverlay.close(); - } - - _computeDraftsLink() { - return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open'); - } - - _createChangeTap(e) { - this.$.destinationDialog.open(); - } - - _handleDestinationConfirm(e) { - this.$.commandsDialog.branch = e.detail.branch; - this.$.commandsDialog.open(); - } + _showNewUserHelp: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrDashboardView.is, GrDashboardView); -})(); + static get observers() { + return [ + '_paramsChanged(params.*)', + ]; + } + + get options() { + return this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.DETAILED_ACCOUNTS, + this.ListChangesOption.REVIEWED + ); + } + + /** @override */ + attached() { + super.attached(); + this._loadPreferences(); + } + + _loadPreferences() { + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + this.$.restAPI.getPreferences().then(preferences => { + this.preferences = preferences; + }); + } else { + this.preferences = {}; + } + }); + } + + _getProjectDashboard(project, dashboard) { + const errFn = response => { + this.fire('page-error', {response}); + }; + return this.$.restAPI.getDashboard( + project, dashboard, errFn).then(response => { + if (!response) { + return; + } + return { + title: response.title, + sections: response.sections.map(section => { + const suffix = response.foreach ? ' ' + response.foreach : ''; + return { + name: section.name, + query: (section.query + suffix).replace( + PROJECT_PLACEHOLDER_PATTERN, project), + }; + }), + }; + }); + } + + _computeTitle(user) { + if (!user || user === 'self') { + return 'My Reviews'; + } + return 'Dashboard for ' + user; + } + + _isViewActive(params) { + return params.view === Gerrit.Nav.View.DASHBOARD; + } + + _paramsChanged(paramsChangeRecord) { + const params = paramsChangeRecord.base; + + if (!this._isViewActive(params)) { + return Promise.resolve(); + } + + return this._reload(); + } + + /** + * Reloads the element. + * + * @return {Promise<!Object>} + */ + _reload() { + this._loading = true; + const {project, dashboard, title, user, sections} = this.params; + const dashboardPromise = project ? + this._getProjectDashboard(project, dashboard) : + Promise.resolve(Gerrit.Nav.getUserDashboard( + user, + sections, + title || this._computeTitle(user))); + + const checkForNewUser = !project && user === 'self'; + return dashboardPromise + .then(res => { + if (res && res.title) { + this.fire('title-change', {title: res.title}); + } + return this._fetchDashboardChanges(res, checkForNewUser); + }) + .then(() => { + this._maybeShowDraftsBanner(); + this.$.reporting.dashboardDisplayed(); + }) + .catch(err => { + this.fire('title-change', { + title: title || this._computeTitle(user), + }); + console.warn(err); + }) + .then(() => { this._loading = false; }); + } + + /** + * Fetches the changes for each dashboard section and sets this._results + * with the response. + * + * @param {!Object} res + * @param {boolean} checkForNewUser + * @return {Promise} + */ + _fetchDashboardChanges(res, checkForNewUser) { + if (!res) { return Promise.resolve(); } + + const queries = res.sections + .map(section => (section.suffixForDashboard ? + section.query + ' ' + section.suffixForDashboard : + section.query)); + + if (checkForNewUser) { + queries.push('owner:self limit:1'); + } + + return this.$.restAPI.getChanges(null, queries, null, this.options) + .then(changes => { + if (checkForNewUser) { + // Last set of results is not meant for dashboard display. + const lastResultSet = changes.pop(); + this._showNewUserHelp = lastResultSet.length == 0; + } + this._results = changes.map((results, i) => { + return { + name: res.sections[i].name, + countLabel: this._computeSectionCountLabel(results), + query: res.sections[i].query, + results, + isOutgoing: res.sections[i].isOutgoing, + }; + }).filter((section, i) => i < res.sections.length && ( + !res.sections[i].hideIfEmpty || + section.results.length)); + }); + } + + _computeSectionCountLabel(changes) { + if (!changes || !changes.length || changes.length == 0) { + return ''; + } + const more = changes[changes.length - 1]._more_changes; + const numChanges = changes.length; + const andMore = more ? ' and more' : ''; + return `(${numChanges}${andMore})`; + } + + _computeUserHeaderClass(params) { + if (!params || !!params.project || !params.user || + params.user === 'self') { + return 'hide'; + } + return ''; + } + + _handleToggleStar(e) { + this.$.restAPI.saveChangeStarred(e.detail.change._number, + e.detail.starred); + } + + _handleToggleReviewed(e) { + this.$.restAPI.saveChangeReviewed(e.detail.change._number, + e.detail.reviewed); + } + + /** + * Banner is shown if a user is on their own dashboard and they have draft + * comments on closed changes. + */ + _maybeShowDraftsBanner() { + this._showDraftsBanner = false; + if (!(this.params.user === 'self')) { return; } + + const draftSection = this._results + .find(section => section.query === 'has:draft'); + if (!draftSection || !draftSection.results.length) { return; } + + const closedChanges = draftSection.results + .filter(change => !this.changeIsOpen(change)); + if (!closedChanges.length) { return; } + + this._showDraftsBanner = true; + } + + _computeBannerClass(show) { + return show ? '' : 'hide'; + } + + _handleOpenDeleteDialog() { + this.$.confirmDeleteOverlay.open(); + } + + _handleConfirmDelete() { + this.$.confirmDeleteDialog.disabled = true; + return this.$.restAPI.deleteDraftComments('-is:open').then(() => { + this._closeConfirmDeleteOverlay(); + this._reload(); + }); + } + + _closeConfirmDeleteOverlay() { + this.$.confirmDeleteOverlay.close(); + } + + _computeDraftsLink() { + return Gerrit.Nav.getUrlForSearchQuery('has:draft -is:open'); + } + + _createChangeTap(e) { + this.$.destinationDialog.open(); + } + + _handleDestinationConfirm(e) { + this.$.commandsDialog.branch = e.detail.branch; + this.$.commandsDialog.open(); + } +} + +customElements.define(GrDashboardView.is, GrDashboardView);
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js index f119e98..07d638c 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_html.js
@@ -1,37 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-create-commands-dialog/gr-create-commands-dialog.html"> -<link rel="import" href="../gr-create-change-help/gr-create-change-help.html"> -<link rel="import" href="../gr-create-destination-dialog/gr-create-destination-dialog.html"> -<link rel="import" href="../gr-user-header/gr-user-header.html"> - -<dom-module id="gr-dashboard-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -71,32 +56,19 @@ } } </style> - <div class$="banner [[_computeBannerClass(_showDraftsBanner)]]"> + <div class\$="banner [[_computeBannerClass(_showDraftsBanner)]]"> <div> You have draft comments on closed changes. - <a href$="[[_computeDraftsLink(_showDraftsBanner)]]" target="_blank">(view all)</a> + <a href\$="[[_computeDraftsLink(_showDraftsBanner)]]" target="_blank">(view all)</a> </div> <div> - <gr-button - class="delete" - link - on-click="_handleOpenDeleteDialog">Delete All</gr-button> + <gr-button class="delete" link="" on-click="_handleOpenDeleteDialog">Delete All</gr-button> </div> </div> - <div class="loading" hidden$="[[!_loading]]">Loading...</div> - <div hidden$="[[_loading]]" hidden> - <gr-user-header - user-id="[[params.user]]" - class$="[[_computeUserHeaderClass(params)]]"></gr-user-header> - <gr-change-list - show-star - show-reviewed-state - account="[[account]]" - preferences="[[preferences]]" - selected-index="{{viewState.selectedChangeIndex}}" - sections="[[_results]]" - on-toggle-star="_handleToggleStar" - on-toggle-reviewed="_handleToggleReviewed"> + <div class="loading" hidden\$="[[!_loading]]">Loading...</div> + <div hidden\$="[[_loading]]" hidden=""> + <gr-user-header user-id="[[params.user]]" class\$="[[_computeUserHeaderClass(params)]]"></gr-user-header> + <gr-change-list show-star="" show-reviewed-state="" account="[[account]]" preferences="[[preferences]]" selected-index="{{viewState.selectedChangeIndex}}" sections="[[_results]]" on-toggle-star="_handleToggleStar" on-toggle-reviewed="_handleToggleReviewed"> <div id="emptyOutgoing" slot="empty-outgoing"> <template is="dom-if" if="[[_showNewUserHelp]]"> <gr-create-change-help on-create-tap="createChangeTap"></gr-create-change-help> @@ -107,12 +79,8 @@ </div> </gr-change-list> </div> - <gr-overlay id="confirmDeleteOverlay" with-backdrop> - <gr-dialog - id="confirmDeleteDialog" - confirm-label="Delete" - on-confirm="_handleConfirmDelete" - on-cancel="_closeConfirmDeleteOverlay"> + <gr-overlay id="confirmDeleteOverlay" with-backdrop=""> + <gr-dialog id="confirmDeleteDialog" confirm-label="Delete" on-confirm="_handleConfirmDelete" on-cancel="_closeConfirmDeleteOverlay"> <div class="header" slot="header"> Delete comments </div> @@ -122,12 +90,8 @@ </div> </gr-dialog> </gr-overlay> - <gr-create-destination-dialog - id="destinationDialog" - on-confirm="_handleDestinationConfirm"></gr-create-destination-dialog> + <gr-create-destination-dialog id="destinationDialog" on-confirm="_handleDestinationConfirm"></gr-create-destination-dialog> <gr-create-commands-dialog id="commandsDialog"></gr-create-commands-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-dashboard-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html index 3d83e99..c879923 100644 --- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dashboard-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-dashboard-view.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-dashboard-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dashboard-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,351 +40,353 @@ </template> </test-fixture> -<script> - suite('gr-dashboard-view tests', async () => { - await readyToTest(); - let element; - let sandbox; - let paramsChangedPromise; - let getChangesStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dashboard-view.js'; +suite('gr-dashboard-view tests', () => { + let element; + let sandbox; + let paramsChangedPromise; + let getChangesStub; - setup(() => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - getAccountDetails() { return Promise.resolve({}); }, - getAccountStatus() { return Promise.resolve(false); }, - }); - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges', - (_, qs) => Promise.resolve(qs.map(() => []))); + setup(() => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + getAccountDetails() { return Promise.resolve({}); }, + getAccountStatus() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + getChangesStub = sandbox.stub(element.$.restAPI, 'getChanges', + (_, qs) => Promise.resolve(qs.map(() => []))); - let resolver; - paramsChangedPromise = new Promise(resolve => { - resolver = resolve; + let resolver; + paramsChangedPromise = new Promise(resolve => { + resolver = resolve; + }); + const paramsChanged = element._paramsChanged.bind(element); + sandbox.stub(element, '_paramsChanged', params => { + paramsChanged(params).then(() => resolver()); + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('drafts banner functionality', () => { + suite('_maybeShowDraftsBanner', () => { + test('not dashboard/self', () => { + element.params = {user: 'notself'}; + element._maybeShowDraftsBanner(); + assert.isFalse(element._showDraftsBanner); }); - const paramsChanged = element._paramsChanged.bind(element); - sandbox.stub(element, '_paramsChanged', params => { - paramsChanged(params).then(() => resolver()); + + test('no drafts at all', () => { + element.params = {user: 'self'}; + element._results = []; + element._maybeShowDraftsBanner(); + assert.isFalse(element._showDraftsBanner); + }); + + test('no drafts on open changes', () => { + element.params = {user: 'self'}; + element._results = [{query: 'has:draft', results: [{status: '_'}]}]; + sandbox.stub(element, 'changeIsOpen').returns(true); + element._maybeShowDraftsBanner(); + assert.isFalse(element._showDraftsBanner); + }); + + test('no drafts on open changes', () => { + element.params = {user: 'self'}; + element._results = [{query: 'has:draft', results: [{status: '_'}]}]; + sandbox.stub(element, 'changeIsOpen').returns(false); + element._maybeShowDraftsBanner(); + assert.isTrue(element._showDraftsBanner); }); }); - teardown(() => { - sandbox.restore(); + test('_showDraftsBanner', () => { + element._showDraftsBanner = false; + flushAsynchronousOperations(); + assert.isTrue(isHidden(element.shadowRoot + .querySelector('.banner'))); + + element._showDraftsBanner = true; + flushAsynchronousOperations(); + assert.isFalse(isHidden(element.shadowRoot + .querySelector('.banner'))); }); - suite('drafts banner functionality', () => { - suite('_maybeShowDraftsBanner', () => { - test('not dashboard/self', () => { - element.params = {user: 'notself'}; - element._maybeShowDraftsBanner(); - assert.isFalse(element._showDraftsBanner); - }); + test('delete tap opens dialog', () => { + sandbox.stub(element, '_handleOpenDeleteDialog'); + element._showDraftsBanner = true; + flushAsynchronousOperations(); - test('no drafts at all', () => { - element.params = {user: 'self'}; - element._results = []; - element._maybeShowDraftsBanner(); - assert.isFalse(element._showDraftsBanner); - }); - - test('no drafts on open changes', () => { - element.params = {user: 'self'}; - element._results = [{query: 'has:draft', results: [{status: '_'}]}]; - sandbox.stub(element, 'changeIsOpen').returns(true); - element._maybeShowDraftsBanner(); - assert.isFalse(element._showDraftsBanner); - }); - - test('no drafts on open changes', () => { - element.params = {user: 'self'}; - element._results = [{query: 'has:draft', results: [{status: '_'}]}]; - sandbox.stub(element, 'changeIsOpen').returns(false); - element._maybeShowDraftsBanner(); - assert.isTrue(element._showDraftsBanner); - }); - }); - - test('_showDraftsBanner', () => { - element._showDraftsBanner = false; - flushAsynchronousOperations(); - assert.isTrue(isHidden(element.shadowRoot - .querySelector('.banner'))); - - element._showDraftsBanner = true; - flushAsynchronousOperations(); - assert.isFalse(isHidden(element.shadowRoot - .querySelector('.banner'))); - }); - - test('delete tap opens dialog', () => { - sandbox.stub(element, '_handleOpenDeleteDialog'); - element._showDraftsBanner = true; - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('.banner .delete')); - assert.isTrue(element._handleOpenDeleteDialog.called); - }); - - test('delete comments flow', async () => { - sandbox.spy(element, '_handleConfirmDelete'); - sandbox.stub(element, '_reload'); - - // Set up control over timing of when RPC resolves. - let deleteDraftCommentsPromiseResolver; - const deleteDraftCommentsPromise = new Promise(resolve => { - deleteDraftCommentsPromiseResolver = resolve; - }); - sandbox.stub(element.$.restAPI, 'deleteDraftComments') - .returns(deleteDraftCommentsPromise); - - // Open confirmation dialog and tap confirm button. - await element.$.confirmDeleteOverlay.open(); - MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm); - flushAsynchronousOperations(); - assert.isTrue(element.$.restAPI.deleteDraftComments - .calledWithExactly('-is:open')); - assert.isTrue(element.$.confirmDeleteDialog.disabled); - assert.equal(element._reload.callCount, 0); - - // Verify state after RPC resolves. - deleteDraftCommentsPromiseResolver([]); - await deleteDraftCommentsPromise; - assert.equal(element._reload.callCount, 1); - }); + MockInteractions.tap(element.shadowRoot + .querySelector('.banner .delete')); + assert.isTrue(element._handleOpenDeleteDialog.called); }); - test('_computeTitle', () => { - assert.equal(element._computeTitle('self'), 'My Reviews'); - assert.equal(element._computeTitle('not self'), 'Dashboard for not self'); + test('delete comments flow', async () => { + sandbox.spy(element, '_handleConfirmDelete'); + sandbox.stub(element, '_reload'); + + // Set up control over timing of when RPC resolves. + let deleteDraftCommentsPromiseResolver; + const deleteDraftCommentsPromise = new Promise(resolve => { + deleteDraftCommentsPromiseResolver = resolve; + }); + sandbox.stub(element.$.restAPI, 'deleteDraftComments') + .returns(deleteDraftCommentsPromise); + + // Open confirmation dialog and tap confirm button. + await element.$.confirmDeleteOverlay.open(); + MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm); + flushAsynchronousOperations(); + assert.isTrue(element.$.restAPI.deleteDraftComments + .calledWithExactly('-is:open')); + assert.isTrue(element.$.confirmDeleteDialog.disabled); + assert.equal(element._reload.callCount, 0); + + // Verify state after RPC resolves. + deleteDraftCommentsPromiseResolver([]); + await deleteDraftCommentsPromise; + assert.equal(element._reload.callCount, 1); + }); + }); + + test('_computeTitle', () => { + assert.equal(element._computeTitle('self'), 'My Reviews'); + assert.equal(element._computeTitle('not self'), 'Dashboard for not self'); + }); + + suite('_computeSectionCountLabel', () => { + test('empty changes dont count label', () => { + assert.equal('', element._computeSectionCountLabel([])); }); - suite('_computeSectionCountLabel', () => { - test('empty changes dont count label', () => { - assert.equal('', element._computeSectionCountLabel([])); - }); - - test('1 change', () => { - assert.equal('(1)', - element._computeSectionCountLabel(['1'])); - }); - - test('2 changes', () => { - assert.equal('(2)', - element._computeSectionCountLabel(['1', '2'])); - }); - - test('1 change and more', () => { - assert.equal('(1 and more)', - element._computeSectionCountLabel([{_more_changes: true}])); - }); + test('1 change', () => { + assert.equal('(1)', + element._computeSectionCountLabel(['1'])); }); - suite('_isViewActive', () => { - test('nothing happens when user param is falsy', () => { - element.params = {}; - flushAsynchronousOperations(); - assert.equal(getChangesStub.callCount, 0); - - element.params = {user: ''}; - flushAsynchronousOperations(); - assert.equal(getChangesStub.callCount, 0); - }); - - test('content is refreshed when user param is updated', () => { - element.params = { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - }; - return paramsChangedPromise.then(() => { - assert.equal(getChangesStub.callCount, 1); - }); - }); + test('2 changes', () => { + assert.equal('(2)', + element._computeSectionCountLabel(['1', '2'])); }); - suite('selfOnly sections', () => { - test('viewing self dashboard includes selfOnly sections', () => { - element.params = { - view: Gerrit.Nav.View.DASHBOARD, - sections: [ - {query: '1'}, - {query: '2', selfOnly: true}, - ], - user: 'self', - }; - return paramsChangedPromise.then(() => { - assert.isTrue( - getChangesStub.calledWith( - null, ['1', '2', 'owner:self limit:1'], null, element.options)); - }); - }); + test('1 change and more', () => { + assert.equal('(1 and more)', + element._computeSectionCountLabel([{_more_changes: true}])); + }); + }); - test('viewing another user\'s dashboard omits selfOnly sections', () => { - element.params = { - view: Gerrit.Nav.View.DASHBOARD, - sections: [ - {query: '1'}, - {query: '2', selfOnly: true}, - ], - user: 'user', - }; - return paramsChangedPromise.then(() => { - assert.isTrue( - getChangesStub.calledWith( - null, ['1'], null, element.options)); - }); - }); + suite('_isViewActive', () => { + test('nothing happens when user param is falsy', () => { + element.params = {}; + flushAsynchronousOperations(); + assert.equal(getChangesStub.callCount, 0); + + element.params = {user: ''}; + flushAsynchronousOperations(); + assert.equal(getChangesStub.callCount, 0); }); - test('suffixForDashboard is included in getChanges query', () => { + test('content is refreshed when user param is updated', () => { + element.params = { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + }; + return paramsChangedPromise.then(() => { + assert.equal(getChangesStub.callCount, 1); + }); + }); + }); + + suite('selfOnly sections', () => { + test('viewing self dashboard includes selfOnly sections', () => { element.params = { view: Gerrit.Nav.View.DASHBOARD, sections: [ {query: '1'}, - {query: '2', suffixForDashboard: 'suffix'}, + {query: '2', selfOnly: true}, ], + user: 'self', }; return paramsChangedPromise.then(() => { - assert.isTrue(getChangesStub.calledOnce); - assert.deepEqual( - getChangesStub.firstCall.args, - [null, ['1', '2 suffix'], null, element.options]); + assert.isTrue( + getChangesStub.calledWith( + null, ['1', '2', 'owner:self limit:1'], null, element.options)); }); }); - suite('_getProjectDashboard', () => { - test('dashboard with foreach', () => { - sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({ - title: 'title', - foreach: 'foreach for ${project}', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: '${project} query 2'}, - ], - })); - return element._getProjectDashboard('project', '').then(dashboard => { - assert.deepEqual( - dashboard, - { - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1 foreach for project'}, - { - name: 'section 2', - query: 'project query 2 foreach for project', - }, - ], - }); - }); - }); - - test('dashboard without foreach', () => { - sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({ - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: '${project} query 2'}, - ], - })); - return element._getProjectDashboard('project', '').then(dashboard => { - assert.deepEqual( - dashboard, - { - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'project query 2'}, - ], - }); - }); - }); - }); - - test('hideIfEmpty sections', () => { - const sections = [ - {name: 'test1', query: 'test1', hideIfEmpty: true}, - {name: 'test2', query: 'test2', hideIfEmpty: true}, - ]; - getChangesStub.restore(); - sandbox.stub(element.$.restAPI, 'getChanges') - .returns(Promise.resolve([[], ['nonempty']])); - - return element._fetchDashboardChanges({sections}, false).then(() => { - assert.equal(element._results.length, 1); - assert.equal(element._results[0].name, 'test2'); - }); - }); - - test('preserve isOutgoing sections', () => { - const sections = [ - {name: 'test1', query: 'test1', isOutgoing: true}, - {name: 'test2', query: 'test2'}, - ]; - getChangesStub.restore(); - sandbox.stub(element.$.restAPI, 'getChanges') - .returns(Promise.resolve([[], []])); - - return element._fetchDashboardChanges({sections}, false).then(() => { - assert.equal(element._results.length, 2); - assert.isTrue(element._results[0].isOutgoing); - assert.isNotOk(element._results[1].isOutgoing); - }); - }); - - test('_showNewUserHelp', () => { - element._loading = false; - element._showNewUserHelp = false; - flushAsynchronousOperations(); - - assert.equal(element.$.emptyOutgoing.textContent.trim(), 'No changes'); - assert.isNotOk(element.shadowRoot - .querySelector('gr-create-change-help')); - element._showNewUserHelp = true; - flushAsynchronousOperations(); - - assert.notEqual(element.$.emptyOutgoing.textContent.trim(), 'No changes'); - assert.isOk(element.shadowRoot - .querySelector('gr-create-change-help')); - }); - - test('_computeUserHeaderClass', () => { - assert.equal(element._computeUserHeaderClass(undefined), 'hide'); - assert.equal(element._computeUserHeaderClass({}), 'hide'); - assert.equal(element._computeUserHeaderClass({user: 'self'}), 'hide'); - assert.equal(element._computeUserHeaderClass({user: 'user'}), ''); - assert.equal( - element._computeUserHeaderClass({project: 'p', user: 'user'}), - 'hide'); - }); - - test('404 page', done => { - const response = {status: 404}; - sandbox.stub(element.$.restAPI, 'getDashboard', - async (project, dashboard, errFn) => { - errFn(response); - }); - element.addEventListener('page-error', e => { - assert.strictEqual(e.detail.response, response); - done(); - }); + test('viewing another user\'s dashboard omits selfOnly sections', () => { element.params = { view: Gerrit.Nav.View.DASHBOARD, - project: 'project', - dashboard: 'dashboard', - }; - }); - - test('params change triggers dashboardDisplayed()', () => { - sandbox.stub(element.$.reporting, 'dashboardDisplayed'); - element.params = { - view: Gerrit.Nav.View.DASHBOARD, - project: 'project', - dashboard: 'dashboard', + sections: [ + {query: '1'}, + {query: '2', selfOnly: true}, + ], + user: 'user', }; return paramsChangedPromise.then(() => { - assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce); + assert.isTrue( + getChangesStub.calledWith( + null, ['1'], null, element.options)); }); }); }); + + test('suffixForDashboard is included in getChanges query', () => { + element.params = { + view: Gerrit.Nav.View.DASHBOARD, + sections: [ + {query: '1'}, + {query: '2', suffixForDashboard: 'suffix'}, + ], + }; + return paramsChangedPromise.then(() => { + assert.isTrue(getChangesStub.calledOnce); + assert.deepEqual( + getChangesStub.firstCall.args, + [null, ['1', '2 suffix'], null, element.options]); + }); + }); + + suite('_getProjectDashboard', () => { + test('dashboard with foreach', () => { + sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({ + title: 'title', + foreach: 'foreach for ${project}', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: '${project} query 2'}, + ], + })); + return element._getProjectDashboard('project', '').then(dashboard => { + assert.deepEqual( + dashboard, + { + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1 foreach for project'}, + { + name: 'section 2', + query: 'project query 2 foreach for project', + }, + ], + }); + }); + }); + + test('dashboard without foreach', () => { + sandbox.stub(element.$.restAPI, 'getDashboard', () => Promise.resolve({ + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: '${project} query 2'}, + ], + })); + return element._getProjectDashboard('project', '').then(dashboard => { + assert.deepEqual( + dashboard, + { + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'project query 2'}, + ], + }); + }); + }); + }); + + test('hideIfEmpty sections', () => { + const sections = [ + {name: 'test1', query: 'test1', hideIfEmpty: true}, + {name: 'test2', query: 'test2', hideIfEmpty: true}, + ]; + getChangesStub.restore(); + sandbox.stub(element.$.restAPI, 'getChanges') + .returns(Promise.resolve([[], ['nonempty']])); + + return element._fetchDashboardChanges({sections}, false).then(() => { + assert.equal(element._results.length, 1); + assert.equal(element._results[0].name, 'test2'); + }); + }); + + test('preserve isOutgoing sections', () => { + const sections = [ + {name: 'test1', query: 'test1', isOutgoing: true}, + {name: 'test2', query: 'test2'}, + ]; + getChangesStub.restore(); + sandbox.stub(element.$.restAPI, 'getChanges') + .returns(Promise.resolve([[], []])); + + return element._fetchDashboardChanges({sections}, false).then(() => { + assert.equal(element._results.length, 2); + assert.isTrue(element._results[0].isOutgoing); + assert.isNotOk(element._results[1].isOutgoing); + }); + }); + + test('_showNewUserHelp', () => { + element._loading = false; + element._showNewUserHelp = false; + flushAsynchronousOperations(); + + assert.equal(element.$.emptyOutgoing.textContent.trim(), 'No changes'); + assert.isNotOk(element.shadowRoot + .querySelector('gr-create-change-help')); + element._showNewUserHelp = true; + flushAsynchronousOperations(); + + assert.notEqual(element.$.emptyOutgoing.textContent.trim(), 'No changes'); + assert.isOk(element.shadowRoot + .querySelector('gr-create-change-help')); + }); + + test('_computeUserHeaderClass', () => { + assert.equal(element._computeUserHeaderClass(undefined), 'hide'); + assert.equal(element._computeUserHeaderClass({}), 'hide'); + assert.equal(element._computeUserHeaderClass({user: 'self'}), 'hide'); + assert.equal(element._computeUserHeaderClass({user: 'user'}), ''); + assert.equal( + element._computeUserHeaderClass({project: 'p', user: 'user'}), + 'hide'); + }); + + test('404 page', done => { + const response = {status: 404}; + sandbox.stub(element.$.restAPI, 'getDashboard', + async (project, dashboard, errFn) => { + errFn(response); + }); + element.addEventListener('page-error', e => { + assert.strictEqual(e.detail.response, response); + done(); + }); + element.params = { + view: Gerrit.Nav.View.DASHBOARD, + project: 'project', + dashboard: 'dashboard', + }; + }); + + test('params change triggers dashboardDisplayed()', () => { + sandbox.stub(element.$.reporting, 'dashboardDisplayed'); + element.params = { + view: Gerrit.Nav.View.DASHBOARD, + project: 'project', + dashboard: 'dashboard', + }; + return paramsChangedPromise.then(() => { + assert.isTrue(element.$.reporting.dashboardDisplayed.calledOnce); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js index de0a56e..2523700 100644 --- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js +++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard.js
@@ -14,24 +14,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrEmbedDashboard extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-embed-dashboard'; } +import '../gr-change-list/gr-change-list.js'; +import '../gr-create-change-help/gr-create-change-help.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-embed-dashboard_html.js'; - static get properties() { - return { - account: Object, - sections: Array, - preferences: Object, - showNewUserHelp: Boolean, - }; - } +/** @extends Polymer.Element */ +class GrEmbedDashboard extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-embed-dashboard'; } + + static get properties() { + return { + account: Object, + sections: Array, + preferences: Object, + showNewUserHelp: Boolean, + }; } +} - customElements.define(GrEmbedDashboard.is, GrEmbedDashboard); -})(); +customElements.define(GrEmbedDashboard.is, GrEmbedDashboard);
diff --git a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js index d445185..2c122f37 100644 --- a/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-embed-dashboard/gr-embed-dashboard_html.js
@@ -1,32 +1,23 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../change-list/gr-change-list/gr-change-list.html"> -<link rel="import" href="../gr-create-change-help/gr-create-change-help.html"> - -<dom-module id="gr-embed-dashboard"> - <template> - <gr-change-list - show-star - account="[[account]]" - preferences="[[preferences]]" - sections="[[sections]]"> +export const htmlTemplate = html` + <gr-change-list show-star="" account="[[account]]" preferences="[[preferences]]" sections="[[sections]]"> <div id="emptyOutgoing" slot="empty-outgoing"> <template is="dom-if" if="[[showNewUserHelp]]"> <gr-create-change-help></gr-create-change-help> @@ -36,6 +27,4 @@ </template> </div> </gr-change-list> - </template> - <script src="gr-embed-dashboard.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js index c0e472a..e9a0387 100644 --- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js +++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header.js
@@ -14,35 +14,45 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrRepoHeader extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-repo-header'; } +import '../../../styles/dashboard-header-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-header_html.js'; - static get properties() { - return { - /** @type {?string} */ - repo: { - type: String, - observer: '_repoChanged', - }, - /** @type {string|null} */ - _repoUrl: String, - }; - } +/** @extends Polymer.Element */ +class GrRepoHeader extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _repoChanged(repoName) { - if (!repoName) { - this._repoUrl = null; - return; - } - this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName); - } + static get is() { return 'gr-repo-header'; } + + static get properties() { + return { + /** @type {?string} */ + repo: { + type: String, + observer: '_repoChanged', + }, + /** @type {string|null} */ + _repoUrl: String, + }; } - customElements.define(GrRepoHeader.is, GrRepoHeader); -})(); + _repoChanged(repoName) { + if (!repoName) { + this._repoUrl = null; + return; + } + this._repoUrl = Gerrit.Nav.getUrlForRepo(repoName); + } +} + +customElements.define(GrRepoHeader.is, GrRepoHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js index 5d4b8a3..d1274ee 100644 --- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/dashboard-header-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-repo-header"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -31,15 +24,13 @@ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <div class="info"> - <h1 class$="name"> + <h1 class\$="name"> [[repo]] - <hr/> + <hr> </h1> <div> - <span>Detail:</span> <a href$="[[_repoUrl]]">Repo settings</a> + <span>Detail:</span> <a href\$="[[_repoUrl]]">Repo settings</a> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-header.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html index d9cd75e..02fb715 100644 --- a/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-repo-header/gr-repo-header_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-header</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-header.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-header.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-header.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,26 +40,28 @@ </template> </test-fixture> -<script> - suite('gr-repo-header tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-header.js'; +suite('gr-repo-header tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('repoUrl reset once repo changed', () => { - sandbox.stub(Gerrit.Nav, 'getUrlForRepo', - repoName => `http://test.com/${repoName}` - ); - assert.equal(element._repoUrl, undefined); - element.repo = 'test'; - assert.equal(element._repoUrl, 'http://test.com/test'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { sandbox.restore(); }); + + test('repoUrl reset once repo changed', () => { + sandbox.stub(Gerrit.Nav, 'getUrlForRepo', + repoName => `http://test.com/${repoName}` + ); + assert.equal(element._repoUrl, undefined); + element.repo = 'test'; + assert.equal(element._repoUrl, 'http://test.com/test'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js index 2fc8170..3fc4291 100644 --- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -14,91 +14,104 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @extends Polymer.Element - */ - class GrUserHeader extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-user-header'; } +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../shared/gr-avatar/gr-avatar.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/dashboard-header-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-user-header_html.js'; - static get properties() { - return { +/** + * @extends Polymer.Element + */ +class GrUserHeader extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-user-header'; } + + static get properties() { + return { + /** @type {?string} */ + userId: { + type: String, + observer: '_accountChanged', + }, + + showDashboardLink: { + type: Boolean, + value: false, + }, + + loggedIn: { + type: Boolean, + value: false, + }, + + /** + * @type {?{name: ?, email: ?, registered_on: ?}} + */ + _accountDetails: { + type: Object, + value: null, + }, + /** @type {?string} */ - userId: { - type: String, - observer: '_accountChanged', - }, - - showDashboardLink: { - type: Boolean, - value: false, - }, - - loggedIn: { - type: Boolean, - value: false, - }, - - /** - * @type {?{name: ?, email: ?, registered_on: ?}} - */ - _accountDetails: { - type: Object, - value: null, - }, - - /** @type {?string} */ - _status: { - type: String, - value: null, - }, - }; - } - - _accountChanged(userId) { - if (!userId) { - this._accountDetails = null; - this._status = null; - return; - } - - this.$.restAPI.getAccountDetails(userId).then(details => { - this._accountDetails = details; - }); - this.$.restAPI.getAccountStatus(userId).then(status => { - this._status = status; - }); - } - - _computeDisplayClass(status) { - return status ? ' ' : 'hide'; - } - - _computeDetail(accountDetails, name) { - return accountDetails ? accountDetails[name] : ''; - } - - _computeStatusClass(accountDetails) { - return this._computeDetail(accountDetails, 'status') ? '' : 'hide'; - } - - _computeDashboardUrl(accountDetails) { - if (!accountDetails) { return null; } - const id = accountDetails._account_id; - const email = accountDetails.email; - if (!id && !email ) { return null; } - return Gerrit.Nav.getUrlForUserDashboard(id ? id : email); - } - - _computeDashboardLinkClass(showDashboardLink, loggedIn) { - return showDashboardLink && loggedIn ? - 'dashboardLink' : 'dashboardLink hide'; - } + _status: { + type: String, + value: null, + }, + }; } - customElements.define(GrUserHeader.is, GrUserHeader); -})(); + _accountChanged(userId) { + if (!userId) { + this._accountDetails = null; + this._status = null; + return; + } + + this.$.restAPI.getAccountDetails(userId).then(details => { + this._accountDetails = details; + }); + this.$.restAPI.getAccountStatus(userId).then(status => { + this._status = status; + }); + } + + _computeDisplayClass(status) { + return status ? ' ' : 'hide'; + } + + _computeDetail(accountDetails, name) { + return accountDetails ? accountDetails[name] : ''; + } + + _computeStatusClass(accountDetails) { + return this._computeDetail(accountDetails, 'status') ? '' : 'hide'; + } + + _computeDashboardUrl(accountDetails) { + if (!accountDetails) { return null; } + const id = accountDetails._account_id; + const email = accountDetails.email; + if (!id && !email ) { return null; } + return Gerrit.Nav.getUrlForUserDashboard(id ? id : email); + } + + _computeDashboardLinkClass(showDashboardLink, loggedIn) { + return showDashboardLink && loggedIn ? + 'dashboardLink' : 'dashboardLink hide'; + } +} + +customElements.define(GrUserHeader.is, GrUserHeader);
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js index 8175849..3de4277 100644 --- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_html.js
@@ -1,32 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../shared/gr-avatar/gr-avatar.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/dashboard-header-styles.html"> - -<dom-module id="gr-user-header"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -43,16 +33,13 @@ display: none; } </style> - <gr-avatar - account="[[_accountDetails]]" - image-size="100" - aria-label="Account avatar"></gr-avatar> + <gr-avatar account="[[_accountDetails]]" image-size="100" aria-label="Account avatar"></gr-avatar> <div class="info"> <h1 class="name"> [[_computeDetail(_accountDetails, 'name')]] </h1> - <hr/> - <div class$="status [[_computeStatusClass(_accountDetails)]]"> + <hr> + <div class\$="status [[_computeStatusClass(_accountDetails)]]"> <span>Status:</span> [[_status]] </div> <div> @@ -62,8 +49,7 @@ </div> <div> <span>Joined:</span> - <gr-date-formatter - date-str="[[_computeDetail(_accountDetails, 'registered_on')]]"> + <gr-date-formatter date-str="[[_computeDetail(_accountDetails, 'registered_on')]]"> </gr-date-formatter> </div> <gr-endpoint-decorator name="user-header"> @@ -74,11 +60,9 @@ </gr-endpoint-decorator> </div> <div class="info"> - <div class$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]"> - <a href$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a> + <div class\$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]"> + <a href\$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-user-header.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html index 26ebeb2..169c028 100644 --- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html +++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-user-header</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-user-header.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-user-header.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-user-header.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,50 +40,52 @@ </template> </test-fixture> -<script> - suite('gr-user-header tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-user-header.js'; +suite('gr-user-header tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); - teardown(() => { sandbox.restore(); }); + teardown(() => { sandbox.restore(); }); - test('loads and clears account info', done => { - sandbox.stub(element.$.restAPI, 'getAccountDetails') - .returns(Promise.resolve({ - name: 'foo', - email: 'bar', - registered_on: '2015-03-12 18:32:08.000000000', - })); - sandbox.stub(element.$.restAPI, 'getAccountStatus') - .returns(Promise.resolve('baz')); + test('loads and clears account info', done => { + sandbox.stub(element.$.restAPI, 'getAccountDetails') + .returns(Promise.resolve({ + name: 'foo', + email: 'bar', + registered_on: '2015-03-12 18:32:08.000000000', + })); + sandbox.stub(element.$.restAPI, 'getAccountStatus') + .returns(Promise.resolve('baz')); - element.userId = 'foo.bar@baz'; + element.userId = 'foo.bar@baz'; + flush(() => { + assert.isOk(element._accountDetails); + assert.isOk(element._status); + + element.userId = null; flush(() => { - assert.isOk(element._accountDetails); - assert.isOk(element._status); + flushAsynchronousOperations(); + assert.isNull(element._accountDetails); + assert.isNull(element._status); - element.userId = null; - flush(() => { - flushAsynchronousOperations(); - assert.isNull(element._accountDetails); - assert.isNull(element._status); - - done(); - }); + done(); }); }); - - test('_computeDashboardLinkClass', () => { - assert.include(element._computeDashboardLinkClass(false, false), 'hide'); - assert.include(element._computeDashboardLinkClass(true, false), 'hide'); - assert.include(element._computeDashboardLinkClass(false, true), 'hide'); - assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide'); - }); }); + + test('_computeDashboardLinkClass', () => { + assert.include(element._computeDashboardLinkClass(false, false), 'hide'); + assert.include(element._computeDashboardLinkClass(true, false), 'hide'); + assert.include(element._computeDashboardLinkClass(false, true), 'hide'); + assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js index 12c6b58..51888d6 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -14,1613 +14,1642 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.'; - const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.'; - const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../admin/gr-create-change-dialog/gr-create-change-dialog.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js'; +import '../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js'; +import '../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js'; +import '../gr-confirm-move-dialog/gr-confirm-move-dialog.js'; +import '../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js'; +import '../gr-confirm-revert-dialog/gr-confirm-revert-dialog.js'; +import '../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js'; +import '../gr-confirm-submit-dialog/gr-confirm-submit-dialog.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-actions_html.js'; + +const ERR_BRANCH_EMPTY = 'The destination branch can’t be empty.'; +const ERR_COMMIT_EMPTY = 'The commit message can’t be empty.'; +const ERR_REVISION_ACTIONS = 'Couldn’t load revision actions.'; +/** + * @enum {string} + */ +const LabelStatus = { /** - * @enum {string} + * This label provides what is necessary for submission. */ - const LabelStatus = { - /** - * This label provides what is necessary for submission. - */ - OK: 'OK', - /** - * This label prevents the change from being submitted. - */ - REJECT: 'REJECT', - /** - * The label may be set, but it's neither necessary for submission - * nor does it block submission if set. - */ - MAY: 'MAY', - /** - * The label is required for submission, but has not been satisfied. - */ - NEED: 'NEED', - /** - * The label is required for submission, but is impossible to complete. - * The likely cause is access has not been granted correctly by the - * project owner or site administrator. - */ - IMPOSSIBLE: 'IMPOSSIBLE', - OPTIONAL: 'OPTIONAL', - }; + OK: 'OK', + /** + * This label prevents the change from being submitted. + */ + REJECT: 'REJECT', + /** + * The label may be set, but it's neither necessary for submission + * nor does it block submission if set. + */ + MAY: 'MAY', + /** + * The label is required for submission, but has not been satisfied. + */ + NEED: 'NEED', + /** + * The label is required for submission, but is impossible to complete. + * The likely cause is access has not been granted correctly by the + * project owner or site administrator. + */ + IMPOSSIBLE: 'IMPOSSIBLE', + OPTIONAL: 'OPTIONAL', +}; - const ChangeActions = { - ABANDON: 'abandon', - DELETE: '/', - DELETE_EDIT: 'deleteEdit', - EDIT: 'edit', - FOLLOW_UP: 'followup', - IGNORE: 'ignore', - MOVE: 'move', - PRIVATE: 'private', - PRIVATE_DELETE: 'private.delete', - PUBLISH_EDIT: 'publishEdit', - REBASE_EDIT: 'rebaseEdit', - RESTORE: 'restore', - REVERT: 'revert', - REVERT_SUBMISSION: 'revert_submission', - REVIEWED: 'reviewed', - STOP_EDIT: 'stopEdit', - UNIGNORE: 'unignore', - UNREVIEWED: 'unreviewed', - WIP: 'wip', - }; +const ChangeActions = { + ABANDON: 'abandon', + DELETE: '/', + DELETE_EDIT: 'deleteEdit', + EDIT: 'edit', + FOLLOW_UP: 'followup', + IGNORE: 'ignore', + MOVE: 'move', + PRIVATE: 'private', + PRIVATE_DELETE: 'private.delete', + PUBLISH_EDIT: 'publishEdit', + REBASE_EDIT: 'rebaseEdit', + RESTORE: 'restore', + REVERT: 'revert', + REVERT_SUBMISSION: 'revert_submission', + REVIEWED: 'reviewed', + STOP_EDIT: 'stopEdit', + UNIGNORE: 'unignore', + UNREVIEWED: 'unreviewed', + WIP: 'wip', +}; - const RevisionActions = { - CHERRYPICK: 'cherrypick', - REBASE: 'rebase', - SUBMIT: 'submit', - DOWNLOAD: 'download', - }; +const RevisionActions = { + CHERRYPICK: 'cherrypick', + REBASE: 'rebase', + SUBMIT: 'submit', + DOWNLOAD: 'download', +}; - const ActionLoadingLabels = { - abandon: 'Abandoning...', - cherrypick: 'Cherry-picking...', - delete: 'Deleting...', - move: 'Moving..', - rebase: 'Rebasing...', - restore: 'Restoring...', - revert: 'Reverting...', - revert_submission: 'Reverting Submission...', - submit: 'Submitting...', - }; +const ActionLoadingLabels = { + abandon: 'Abandoning...', + cherrypick: 'Cherry-picking...', + delete: 'Deleting...', + move: 'Moving..', + rebase: 'Rebasing...', + restore: 'Restoring...', + revert: 'Reverting...', + revert_submission: 'Reverting Submission...', + submit: 'Submitting...', +}; - const ActionType = { - CHANGE: 'change', - REVISION: 'revision', - }; +const ActionType = { + CHANGE: 'change', + REVISION: 'revision', +}; - const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; +const ADDITIONAL_ACTION_KEY_PREFIX = '__additionalAction_'; - const QUICK_APPROVE_ACTION = { - __key: 'review', - __type: 'change', - enabled: true, - key: 'review', - label: 'Quick approve', - method: 'POST', - }; +const QUICK_APPROVE_ACTION = { + __key: 'review', + __type: 'change', + enabled: true, + key: 'review', + label: 'Quick approve', + method: 'POST', +}; - const ActionPriority = { - CHANGE: 2, - DEFAULT: 0, - PRIMARY: 3, - REVIEW: -3, - REVISION: 1, - }; +const ActionPriority = { + CHANGE: 2, + DEFAULT: 0, + PRIMARY: 3, + REVIEW: -3, + REVISION: 1, +}; - const DOWNLOAD_ACTION = { - enabled: true, - label: 'Download patch', - title: 'Open download dialog', - __key: 'download', - __primary: false, - __type: 'revision', - }; +const DOWNLOAD_ACTION = { + enabled: true, + label: 'Download patch', + title: 'Open download dialog', + __key: 'download', + __primary: false, + __type: 'revision', +}; - const REBASE_EDIT = { - enabled: true, - label: 'Rebase edit', - title: 'Rebase change edit', - __key: 'rebaseEdit', - __primary: false, - __type: 'change', - method: 'POST', - }; +const REBASE_EDIT = { + enabled: true, + label: 'Rebase edit', + title: 'Rebase change edit', + __key: 'rebaseEdit', + __primary: false, + __type: 'change', + method: 'POST', +}; - const PUBLISH_EDIT = { - enabled: true, - label: 'Publish edit', - title: 'Publish change edit', - __key: 'publishEdit', - __primary: false, - __type: 'change', - method: 'POST', - }; +const PUBLISH_EDIT = { + enabled: true, + label: 'Publish edit', + title: 'Publish change edit', + __key: 'publishEdit', + __primary: false, + __type: 'change', + method: 'POST', +}; - const DELETE_EDIT = { - enabled: true, - label: 'Delete edit', - title: 'Delete change edit', - __key: 'deleteEdit', - __primary: false, - __type: 'change', - method: 'DELETE', - }; +const DELETE_EDIT = { + enabled: true, + label: 'Delete edit', + title: 'Delete change edit', + __key: 'deleteEdit', + __primary: false, + __type: 'change', + method: 'DELETE', +}; - const EDIT = { - enabled: true, - label: 'Edit', - title: 'Edit this change', - __key: 'edit', - __primary: false, - __type: 'change', - }; +const EDIT = { + enabled: true, + label: 'Edit', + title: 'Edit this change', + __key: 'edit', + __primary: false, + __type: 'change', +}; - const STOP_EDIT = { - enabled: true, - label: 'Stop editing', - title: 'Stop editing this change', - __key: 'stopEdit', - __primary: false, - __type: 'change', - }; +const STOP_EDIT = { + enabled: true, + label: 'Stop editing', + title: 'Stop editing this change', + __key: 'stopEdit', + __primary: false, + __type: 'change', +}; - // Set of keys that have icons. As more icons are added to gr-icons.html, this - // set should be expanded. - const ACTIONS_WITH_ICONS = new Set([ - ChangeActions.ABANDON, - ChangeActions.DELETE_EDIT, - ChangeActions.EDIT, - ChangeActions.PUBLISH_EDIT, - ChangeActions.REBASE_EDIT, - ChangeActions.RESTORE, - ChangeActions.REVERT, - ChangeActions.REVERT_SUBMISSION, - ChangeActions.STOP_EDIT, - QUICK_APPROVE_ACTION.key, - RevisionActions.REBASE, - RevisionActions.SUBMIT, - ]); +// Set of keys that have icons. As more icons are added to gr-icons.html, this +// set should be expanded. +const ACTIONS_WITH_ICONS = new Set([ + ChangeActions.ABANDON, + ChangeActions.DELETE_EDIT, + ChangeActions.EDIT, + ChangeActions.PUBLISH_EDIT, + ChangeActions.REBASE_EDIT, + ChangeActions.RESTORE, + ChangeActions.REVERT, + ChangeActions.REVERT_SUBMISSION, + ChangeActions.STOP_EDIT, + QUICK_APPROVE_ACTION.key, + RevisionActions.REBASE, + RevisionActions.SUBMIT, +]); - const AWAIT_CHANGE_ATTEMPTS = 5; - const AWAIT_CHANGE_TIMEOUT_MS = 1000; +const AWAIT_CHANGE_ATTEMPTS = 5; +const AWAIT_CHANGE_TIMEOUT_MS = 1000; - const REVERT_TYPES = { - REVERT_SINGLE_CHANGE: 1, - REVERT_SUBMISSION: 2, - }; +const REVERT_TYPES = { + REVERT_SINGLE_CHANGE: 1, + REVERT_SUBMISSION: 2, +}; - /* Revert submission is skipped as the normal revert dialog will now show - the user a choice between reverting single change or an entire submission. - Hence, a second button is not needed. - */ - const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION]; +/* Revert submission is skipped as the normal revert dialog will now show +the user a choice between reverting single change or an entire submission. +Hence, a second button is not needed. +*/ +const SKIP_ACTION_KEYS = [ChangeActions.REVERT_SUBMISSION]; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrChangeActions extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-change-actions'; } + /** + * Fired when the change should be reloaded. + * + * @event reload-change + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when an action is tapped. + * + * @event custom-tap - naming pattern: <action key>-tap */ - class GrChangeActions extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-actions'; } + + /** + * Fires to show an alert when a send is attempted on the non-latest patch. + * + * @event show-alert + */ + + /** + * Fires when a change action fails. + * + * @event show-error + */ + + constructor() { + super(); + this.ActionType = ActionType; + this.ChangeActions = ChangeActions; + this.RevisionActions = RevisionActions; + } + + static get properties() { + return { /** - * Fired when the change should be reloaded. - * - * @event reload-change + * @type {{ + * _number: number, + * branch: string, + * id: string, + * project: string, + * subject: string, + * }} */ + change: Object, + actions: { + type: Object, + value() { return {}; }, + }, + primaryActionKeys: { + type: Array, + value() { + return [ + RevisionActions.SUBMIT, + ]; + }, + }, + disableEdit: { + type: Boolean, + value: false, + }, + _hasKnownChainState: { + type: Boolean, + value: false, + }, + _hideQuickApproveAction: { + type: Boolean, + value: false, + }, + changeNum: String, + changeStatus: String, + commitNum: String, + hasParent: { + type: Boolean, + observer: '_computeChainState', + }, + latestPatchNum: String, + commitMessage: { + type: String, + value: '', + }, + /** @type {?} */ + revisionActions: { + type: Object, + notify: true, + value() { return {}; }, + }, + // If property binds directly to [[revisionActions.submit]] it is not + // updated when revisionActions doesn't contain submit action. + /** @type {?} */ + _revisionSubmitAction: { + type: Object, + computed: '_getSubmitAction(revisionActions)', + }, + // If property binds directly to [[revisionActions.rebase]] it is not + // updated when revisionActions doesn't contain rebase action. + /** @type {?} */ + _revisionRebaseAction: { + type: Object, + computed: '_getRebaseAction(revisionActions)', + }, + privateByDefault: String, - /** - * Fired when an action is tapped. - * - * @event custom-tap - naming pattern: <action key>-tap - */ + _loading: { + type: Boolean, + value: true, + }, + _actionLoadingMessage: { + type: String, + value: '', + }, + _allActionValues: { + type: Array, + computed: '_computeAllActions(actions.*, revisionActions.*,' + + 'primaryActionKeys.*, _additionalActions.*, change, ' + + '_actionPriorityOverrides.*)', + }, + _topLevelActions: { + type: Array, + computed: '_computeTopLevelActions(_allActionValues.*, ' + + '_hiddenActions.*, _overflowActions.*)', + observer: '_filterPrimaryActions', + }, + _topLevelPrimaryActions: Array, + _topLevelSecondaryActions: Array, + _menuActions: { + type: Array, + computed: '_computeMenuActions(_allActionValues.*, ' + + '_hiddenActions.*, _overflowActions.*)', + }, + _overflowActions: { + type: Array, + value() { + const value = [ + { + type: ActionType.CHANGE, + key: ChangeActions.WIP, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.DELETE, + }, + { + type: ActionType.REVISION, + key: RevisionActions.CHERRYPICK, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.MOVE, + }, + { + type: ActionType.REVISION, + key: RevisionActions.DOWNLOAD, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.IGNORE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.UNIGNORE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.REVIEWED, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.UNREVIEWED, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.PRIVATE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.PRIVATE_DELETE, + }, + { + type: ActionType.CHANGE, + key: ChangeActions.FOLLOW_UP, + }, + ]; + return value; + }, + }, + _actionPriorityOverrides: { + type: Array, + value() { return []; }, + }, + _additionalActions: { + type: Array, + value() { return []; }, + }, + _hiddenActions: { + type: Array, + value() { return []; }, + }, + _disabledMenuActions: { + type: Array, + value() { return []; }, + }, + // editPatchsetLoaded == "does the current selected patch range have + // 'edit' as one of either basePatchNum or patchNum". + editPatchsetLoaded: { + type: Boolean, + value: false, + }, + // editMode == "is edit mode enabled in the file list". + editMode: { + type: Boolean, + value: false, + }, + editBasedOnCurrentPatchSet: { + type: Boolean, + value: true, + }, + _revertChanges: Array, + }; + } - /** - * Fires to show an alert when a send is attempted on the non-latest patch. - * - * @event show-alert - */ + static get observers() { + return [ + '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', + '_changeChanged(change)', + '_editStatusChanged(editMode, editPatchsetLoaded, ' + + 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)', + ]; + } - /** - * Fires when a change action fails. - * - * @event show-error - */ + /** @override */ + created() { + super.created(); + this.addEventListener('fullscreen-overlay-opened', + () => this._handleHideBackgroundContent()); + this.addEventListener('fullscreen-overlay-closed', + () => this._handleShowBackgroundContent()); + } - constructor() { - super(); - this.ActionType = ActionType; - this.ChangeActions = ChangeActions; - this.RevisionActions = RevisionActions; + /** @override */ + ready() { + super.ready(); + this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); + this._handleLoadingComplete(); + } + + _getSubmitAction(revisionActions) { + return this._getRevisionAction(revisionActions, 'submit', null); + } + + _getRebaseAction(revisionActions) { + return this._getRevisionAction(revisionActions, 'rebase', + {rebaseOnCurrent: null} + ); + } + + _getRevisionAction(revisionActions, actionName, emptyActionValue) { + if (!revisionActions) { + return undefined; + } + if (revisionActions[actionName] === undefined) { + // Return null to fire an event when reveisionActions was loaded + // but doesn't contain actionName. undefined doesn't fire an event + return emptyActionValue; + } + return revisionActions[actionName]; + } + + reload() { + if (!this.changeNum || !this.latestPatchNum) { + return Promise.resolve(); } - static get properties() { - return { - /** - * @type {{ - * _number: number, - * branch: string, - * id: string, - * project: string, - * subject: string, - * }} - */ - change: Object, - actions: { - type: Object, - value() { return {}; }, - }, - primaryActionKeys: { - type: Array, - value() { - return [ - RevisionActions.SUBMIT, - ]; - }, - }, - disableEdit: { - type: Boolean, - value: false, - }, - _hasKnownChainState: { - type: Boolean, - value: false, - }, - _hideQuickApproveAction: { - type: Boolean, - value: false, - }, - changeNum: String, - changeStatus: String, - commitNum: String, - hasParent: { - type: Boolean, - observer: '_computeChainState', - }, - latestPatchNum: String, - commitMessage: { - type: String, - value: '', - }, - /** @type {?} */ - revisionActions: { - type: Object, - notify: true, - value() { return {}; }, - }, - // If property binds directly to [[revisionActions.submit]] it is not - // updated when revisionActions doesn't contain submit action. - /** @type {?} */ - _revisionSubmitAction: { - type: Object, - computed: '_getSubmitAction(revisionActions)', - }, - // If property binds directly to [[revisionActions.rebase]] it is not - // updated when revisionActions doesn't contain rebase action. - /** @type {?} */ - _revisionRebaseAction: { - type: Object, - computed: '_getRebaseAction(revisionActions)', - }, - privateByDefault: String, + this._loading = true; + return this._getRevisionActions() + .then(revisionActions => { + if (!revisionActions) { return; } - _loading: { - type: Boolean, - value: true, - }, - _actionLoadingMessage: { - type: String, - value: '', - }, - _allActionValues: { - type: Array, - computed: '_computeAllActions(actions.*, revisionActions.*,' + - 'primaryActionKeys.*, _additionalActions.*, change, ' + - '_actionPriorityOverrides.*)', - }, - _topLevelActions: { - type: Array, - computed: '_computeTopLevelActions(_allActionValues.*, ' + - '_hiddenActions.*, _overflowActions.*)', - observer: '_filterPrimaryActions', - }, - _topLevelPrimaryActions: Array, - _topLevelSecondaryActions: Array, - _menuActions: { - type: Array, - computed: '_computeMenuActions(_allActionValues.*, ' + - '_hiddenActions.*, _overflowActions.*)', - }, - _overflowActions: { - type: Array, - value() { - const value = [ - { - type: ActionType.CHANGE, - key: ChangeActions.WIP, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.DELETE, - }, - { - type: ActionType.REVISION, - key: RevisionActions.CHERRYPICK, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.MOVE, - }, - { - type: ActionType.REVISION, - key: RevisionActions.DOWNLOAD, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.IGNORE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.UNIGNORE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.REVIEWED, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.UNREVIEWED, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.PRIVATE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.PRIVATE_DELETE, - }, - { - type: ActionType.CHANGE, - key: ChangeActions.FOLLOW_UP, - }, - ]; - return value; - }, - }, - _actionPriorityOverrides: { - type: Array, - value() { return []; }, - }, - _additionalActions: { - type: Array, - value() { return []; }, - }, - _hiddenActions: { - type: Array, - value() { return []; }, - }, - _disabledMenuActions: { - type: Array, - value() { return []; }, - }, - // editPatchsetLoaded == "does the current selected patch range have - // 'edit' as one of either basePatchNum or patchNum". - editPatchsetLoaded: { - type: Boolean, - value: false, - }, - // editMode == "is edit mode enabled in the file list". - editMode: { - type: Boolean, - value: false, - }, - editBasedOnCurrentPatchSet: { - type: Boolean, - value: true, - }, - _revertChanges: Array, - }; - } - - static get observers() { - return [ - '_actionsChanged(actions.*, revisionActions.*, _additionalActions.*)', - '_changeChanged(change)', - '_editStatusChanged(editMode, editPatchsetLoaded, ' + - 'editBasedOnCurrentPatchSet, disableEdit, actions.*, change.*)', - ]; - } - - /** @override */ - created() { - super.created(); - this.addEventListener('fullscreen-overlay-opened', - () => this._handleHideBackgroundContent()); - this.addEventListener('fullscreen-overlay-closed', - () => this._handleShowBackgroundContent()); - } - - /** @override */ - ready() { - super.ready(); - this.$.jsAPI.addElement(this.$.jsAPI.Element.CHANGE_ACTIONS, this); - this._handleLoadingComplete(); - } - - _getSubmitAction(revisionActions) { - return this._getRevisionAction(revisionActions, 'submit', null); - } - - _getRebaseAction(revisionActions) { - return this._getRevisionAction(revisionActions, 'rebase', - {rebaseOnCurrent: null} - ); - } - - _getRevisionAction(revisionActions, actionName, emptyActionValue) { - if (!revisionActions) { - return undefined; - } - if (revisionActions[actionName] === undefined) { - // Return null to fire an event when reveisionActions was loaded - // but doesn't contain actionName. undefined doesn't fire an event - return emptyActionValue; - } - return revisionActions[actionName]; - } - - reload() { - if (!this.changeNum || !this.latestPatchNum) { - return Promise.resolve(); - } - - this._loading = true; - return this._getRevisionActions() - .then(revisionActions => { - if (!revisionActions) { return; } - - this.revisionActions = this._updateRebaseAction(revisionActions); - this._sendShowRevisionActions({ - change: this.change, - revisionActions, - }); - this._handleLoadingComplete(); - }) - .catch(err => { - this.fire('show-alert', {message: ERR_REVISION_ACTIONS}); - this._loading = false; - throw err; + this.revisionActions = this._updateRebaseAction(revisionActions); + this._sendShowRevisionActions({ + change: this.change, + revisionActions, }); - } + this._handleLoadingComplete(); + }) + .catch(err => { + this.fire('show-alert', {message: ERR_REVISION_ACTIONS}); + this._loading = false; + throw err; + }); + } - _handleLoadingComplete() { - Gerrit.awaitPluginsLoaded().then(() => this._loading = false); - } + _handleLoadingComplete() { + Gerrit.awaitPluginsLoaded().then(() => this._loading = false); + } - _sendShowRevisionActions(detail) { - this.$.jsAPI.handleEvent( - this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS, - detail - ); - } + _sendShowRevisionActions(detail) { + this.$.jsAPI.handleEvent( + this.$.jsAPI.EventType.SHOW_REVISION_ACTIONS, + detail + ); + } - _updateRebaseAction(revisionActions) { - if (revisionActions && revisionActions.rebase) { - revisionActions.rebase.rebaseOnCurrent = - !!revisionActions.rebase.enabled; - this._parentIsCurrent = !revisionActions.rebase.enabled; - revisionActions.rebase.enabled = true; - } else { - this._parentIsCurrent = true; - } - return revisionActions; + _updateRebaseAction(revisionActions) { + if (revisionActions && revisionActions.rebase) { + revisionActions.rebase.rebaseOnCurrent = + !!revisionActions.rebase.enabled; + this._parentIsCurrent = !revisionActions.rebase.enabled; + revisionActions.rebase.enabled = true; + } else { + this._parentIsCurrent = true; } + return revisionActions; + } - _changeChanged() { - this.reload(); - } + _changeChanged() { + this.reload(); + } - addActionButton(type, label) { - if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { - throw Error(`Invalid action type: ${type}`); - } - const action = { - enabled: true, - label, - __type: type, - __key: ADDITIONAL_ACTION_KEY_PREFIX + - Math.random().toString(36) - .substr(2), - }; - this.push('_additionalActions', action); - return action.__key; + addActionButton(type, label) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error(`Invalid action type: ${type}`); } + const action = { + enabled: true, + label, + __type: type, + __key: ADDITIONAL_ACTION_KEY_PREFIX + + Math.random().toString(36) + .substr(2), + }; + this.push('_additionalActions', action); + return action.__key; + } - removeActionButton(key) { - const idx = this._indexOfActionButtonWithKey(key); - if (idx === -1) { - return; - } - this.splice('_additionalActions', idx, 1); + removeActionButton(key) { + const idx = this._indexOfActionButtonWithKey(key); + if (idx === -1) { + return; } + this.splice('_additionalActions', idx, 1); + } - setActionButtonProp(key, prop, value) { - this.set([ - '_additionalActions', - this._indexOfActionButtonWithKey(key), - prop, - ], value); - } + setActionButtonProp(key, prop, value) { + this.set([ + '_additionalActions', + this._indexOfActionButtonWithKey(key), + prop, + ], value); + } - setActionOverflow(type, key, overflow) { - if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { - throw Error(`Invalid action type given: ${type}`); - } - const index = this._getActionOverflowIndex(type, key); - const action = { - type, - key, - overflow, - }; - if (!overflow && index !== -1) { - this.splice('_overflowActions', index, 1); - } else if (overflow) { - this.push('_overflowActions', action); - } + setActionOverflow(type, key, overflow) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error(`Invalid action type given: ${type}`); } - - setActionPriority(type, key, priority) { - if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { - throw Error(`Invalid action type given: ${type}`); - } - const index = this._actionPriorityOverrides - .findIndex(action => action.type === type && action.key === key); - const action = { - type, - key, - priority, - }; - if (index !== -1) { - this.set('_actionPriorityOverrides', index, action); - } else { - this.push('_actionPriorityOverrides', action); - } - } - - setActionHidden(type, key, hidden) { - if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { - throw Error(`Invalid action type given: ${type}`); - } - - const idx = this._hiddenActions.indexOf(key); - if (hidden && idx === -1) { - this.push('_hiddenActions', key); - } else if (!hidden && idx !== -1) { - this.splice('_hiddenActions', idx, 1); - } - } - - getActionDetails(action) { - if (this.revisionActions[action]) { - return this.revisionActions[action]; - } else if (this.actions[action]) { - return this.actions[action]; - } - } - - _indexOfActionButtonWithKey(key) { - for (let i = 0; i < this._additionalActions.length; i++) { - if (this._additionalActions[i].__key === key) { - return i; - } - } - return -1; - } - - _getRevisionActions() { - return this.$.restAPI.getChangeRevisionActions(this.changeNum, - this.latestPatchNum); - } - - _shouldHideActions(actions, loading) { - return loading || !actions || !actions.base || !actions.base.length; - } - - _keyCount(changeRecord) { - return Object.keys((changeRecord && changeRecord.base) || {}).length; - } - - _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, - additionalActionsChangeRecord) { - // Polymer 2: check for undefined - if ([ - actionsChangeRecord, - revisionActionsChangeRecord, - additionalActionsChangeRecord, - ].some(arg => arg === undefined)) { - return; - } - - const additionalActions = (additionalActionsChangeRecord && - additionalActionsChangeRecord.base) || []; - this.hidden = this._keyCount(actionsChangeRecord) === 0 && - this._keyCount(revisionActionsChangeRecord) === 0 && - additionalActions.length === 0; - this._actionLoadingMessage = ''; - this._disabledMenuActions = []; - - const revisionActions = revisionActionsChangeRecord.base || {}; - if (Object.keys(revisionActions).length !== 0) { - if (!revisionActions.download) { - this.set('revisionActions.download', DOWNLOAD_ACTION); - } - } - } - - /** - * @param {string=} actionName - */ - _deleteAndNotify(actionName) { - if (this.actions && this.actions[actionName]) { - delete this.actions[actionName]; - // We assign a fake value of 'false' to support Polymer 2 - // see https://github.com/Polymer/polymer/issues/2631 - this.notifyPath('actions.' + actionName, false); - } - } - - _editStatusChanged(editMode, editPatchsetLoaded, - editBasedOnCurrentPatchSet, disableEdit) { - // Polymer 2: check for undefined - if ([ - editMode, - editBasedOnCurrentPatchSet, - disableEdit, - ].some(arg => arg === undefined)) { - return; - } - - if (disableEdit) { - this._deleteAndNotify('publishEdit'); - this._deleteAndNotify('rebaseEdit'); - this._deleteAndNotify('deleteEdit'); - this._deleteAndNotify('stopEdit'); - this._deleteAndNotify('edit'); - return; - } - if (this.actions && editPatchsetLoaded) { - // Only show actions that mutate an edit if an actual edit patch set - // is loaded. - if (this.changeIsOpen(this.change)) { - if (editBasedOnCurrentPatchSet) { - if (!this.actions.publishEdit) { - this.set('actions.publishEdit', PUBLISH_EDIT); - } - this._deleteAndNotify('rebaseEdit'); - } else { - if (!this.actions.rebaseEdit) { - this.set('actions.rebaseEdit', REBASE_EDIT); - } - this._deleteAndNotify('publishEdit'); - } - } - if (!this.actions.deleteEdit) { - this.set('actions.deleteEdit', DELETE_EDIT); - } - } else { - this._deleteAndNotify('publishEdit'); - this._deleteAndNotify('rebaseEdit'); - this._deleteAndNotify('deleteEdit'); - } - - if (this.actions && this.changeIsOpen(this.change)) { - // Only show edit button if there is no edit patchset loaded and the - // file list is not in edit mode. - if (editPatchsetLoaded || editMode) { - this._deleteAndNotify('edit'); - } else { - if (!this.actions.edit) { this.set('actions.edit', EDIT); } - } - // Only show STOP_EDIT if edit mode is enabled, but no edit patch set - // is loaded. - if (editMode && !editPatchsetLoaded) { - if (!this.actions.stopEdit) { - this.set('actions.stopEdit', STOP_EDIT); - } - } else { - this._deleteAndNotify('stopEdit'); - } - } else { - // Remove edit button. - this._deleteAndNotify('edit'); - } - } - - _getValuesFor(obj) { - return Object.keys(obj).map(key => obj[key]); - } - - _getLabelStatus(label) { - if (label.approved) { - return LabelStatus.OK; - } else if (label.rejected) { - return LabelStatus.REJECT; - } else if (label.optional) { - return LabelStatus.OPTIONAL; - } else { - return LabelStatus.NEED; - } - } - - /** - * Get highest score for last missing permitted label for current change. - * Returns null if no labels permitted or more than one label missing. - * - * @return {{label: string, score: string}|null} - */ - _getTopMissingApproval() { - if (!this.change || - !this.change.labels || - !this.change.permitted_labels) { - return null; - } - let result; - for (const label in this.change.labels) { - if (!(label in this.change.permitted_labels)) { - continue; - } - if (this.change.permitted_labels[label].length === 0) { - continue; - } - const status = this._getLabelStatus(this.change.labels[label]); - if (status === LabelStatus.NEED) { - if (result) { - // More than one label is missing, so it's unclear which to quick - // approve, return null; - return null; - } - result = label; - } else if (status === LabelStatus.REJECT || - status === LabelStatus.IMPOSSIBLE) { - return null; - } - } - if (result) { - const score = this.change.permitted_labels[result].slice(-1)[0]; - const maxScore = - Object.keys(this.change.labels[result].values).slice(-1)[0]; - if (score === maxScore) { - // Allow quick approve only for maximal score. - return { - label: result, - score, - }; - } - } - return null; - } - - hideQuickApproveAction() { - this._topLevelSecondaryActions = - this._topLevelSecondaryActions - .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key); - this._hideQuickApproveAction = true; - } - - _getQuickApproveAction() { - if (this._hideQuickApproveAction) { - return null; - } - const approval = this._getTopMissingApproval(); - if (!approval) { - return null; - } - const action = Object.assign({}, QUICK_APPROVE_ACTION); - action.label = approval.label + approval.score; - const review = { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: {}, - }; - review.labels[approval.label] = approval.score; - action.payload = review; - return action; - } - - _getActionValues(actionsChangeRecord, primariesChangeRecord, - additionalActionsChangeRecord, type) { - if (!actionsChangeRecord || !primariesChangeRecord) { return []; } - - const actions = actionsChangeRecord.base || {}; - const primaryActionKeys = primariesChangeRecord.base || []; - const result = []; - const values = this._getValuesFor( - type === ActionType.CHANGE ? ChangeActions : RevisionActions); - const pluginActions = []; - Object.keys(actions).forEach(a => { - actions[a].__key = a; - actions[a].__type = type; - actions[a].__primary = primaryActionKeys.includes(a); - // Plugin actions always contain ~ in the key. - if (a.indexOf('~') !== -1) { - this._populateActionUrl(actions[a]); - pluginActions.push(actions[a]); - // Add server-side provided plugin actions to overflow menu. - this._overflowActions.push({ - type, - key: a, - }); - return; - } else if (!values.includes(a)) { - return; - } - actions[a].label = this._getActionLabel(actions[a]); - - // Triggers a re-render by ensuring object inequality. - result.push(Object.assign({}, actions[a])); - }); - - let additionalActions = (additionalActionsChangeRecord && - additionalActionsChangeRecord.base) || []; - additionalActions = additionalActions - .filter(a => a.__type === type) - .map(a => { - a.__primary = primaryActionKeys.includes(a.__key); - // Triggers a re-render by ensuring object inequality. - return Object.assign({}, a); - }); - return result.concat(additionalActions).concat(pluginActions); - } - - _populateActionUrl(action) { - const patchNum = - action.__type === ActionType.REVISION ? this.latestPatchNum : null; - this.$.restAPI.getChangeActionURL( - this.changeNum, patchNum, '/' + action.__key) - .then(url => action.__url = url); - } - - /** - * Given a change action, return a display label that uses the appropriate - * casing or includes explanatory details. - */ - _getActionLabel(action) { - if (action.label === 'Delete') { - // This label is common within change and revision actions. Make it more - // explicit to the user. - return 'Delete change'; - } else if (action.label === 'WIP') { - return 'Mark as work in progress'; - } - // Otherwise, just map the name to sentence case. - return this._toSentenceCase(action.label); - } - - /** - * Capitalize the first letter and lowecase all others. - * - * @param {string} s - * @return {string} - */ - _toSentenceCase(s) { - if (!s.length) { return ''; } - return s[0].toUpperCase() + s.slice(1).toLowerCase(); - } - - _computeLoadingLabel(action) { - return ActionLoadingLabels[action] || 'Working...'; - } - - _canSubmitChange() { - return this.$.jsAPI.canSubmitChange(this.change, - this._getRevision(this.change, this.latestPatchNum)); - } - - _getRevision(change, patchNum) { - for (const rev of Object.values(change.revisions)) { - if (this.patchNumEquals(rev._number, patchNum)) { - return rev; - } - } - return null; - } - - showRevertDialog() { - const query = 'submissionid:' + this.change.submission_id; - /* A chromium plugin expects that the modifyRevertMsg hook will only - be called after the revert button is pressed, hence we populate the - revert dialog after revert button is pressed. */ - this.$.restAPI.getChanges('', query) - .then(changes => { - this.$.confirmRevertDialog.populate(this.change, - this.commitMessage, changes); - this._showActionDialog(this.$.confirmRevertDialog); - }); - } - - showRevertSubmissionDialog() { - const query = 'submissionid:' + this.change.submission_id; - this.$.restAPI.getChanges('', query) - .then(changes => { - this.$.confirmRevertSubmissionDialog. - _populateRevertSubmissionMessage( - this.commitMessage, this.change, changes); - this._showActionDialog(this.$.confirmRevertSubmissionDialog); - }); - } - - _handleActionTap(e) { - e.preventDefault(); - let el = Polymer.dom(e).localTarget; - while (el.tagName.toLowerCase() !== 'gr-button') { - if (!el.parentElement) { return; } - el = el.parentElement; - } - - const key = el.getAttribute('data-action-key'); - if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || - key.indexOf('~') !== -1) { - this.fire(`${key}-tap`, {node: el}); - return; - } - const type = el.getAttribute('data-action-type'); - this._handleAction(type, key); - } - - _handleOveflowItemTap(e) { - e.preventDefault(); - const el = Polymer.dom(e).localTarget; - const key = e.detail.action.__key; - if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || - key.indexOf('~') !== -1) { - this.fire(`${key}-tap`, {node: el}); - return; - } - this._handleAction(e.detail.action.__type, e.detail.action.__key); - } - - _handleAction(type, key) { - this.$.reporting.reportInteraction(`${type}-${key}`); - switch (type) { - case ActionType.REVISION: - this._handleRevisionAction(key); - break; - case ActionType.CHANGE: - this._handleChangeAction(key); - break; - default: - this._fireAction(this._prependSlash(key), this.actions[key], false); - } - } - - _handleChangeAction(key) { - let action; - switch (key) { - case ChangeActions.REVERT: - this.showRevertDialog(); - break; - case ChangeActions.REVERT_SUBMISSION: - this.showRevertSubmissionDialog(); - break; - case ChangeActions.ABANDON: - this._showActionDialog(this.$.confirmAbandonDialog); - break; - case QUICK_APPROVE_ACTION.key: - action = this._allActionValues.find(o => o.key === key); - this._fireAction( - this._prependSlash(key), action, true, action.payload); - break; - case ChangeActions.EDIT: - this._handleEditTap(); - break; - case ChangeActions.STOP_EDIT: - this._handleStopEditTap(); - break; - case ChangeActions.DELETE: - this._handleDeleteTap(); - break; - case ChangeActions.DELETE_EDIT: - this._handleDeleteEditTap(); - break; - case ChangeActions.FOLLOW_UP: - this._handleFollowUpTap(); - break; - case ChangeActions.WIP: - this._handleWipTap(); - break; - case ChangeActions.MOVE: - this._handleMoveTap(); - break; - case ChangeActions.PUBLISH_EDIT: - this._handlePublishEditTap(); - break; - case ChangeActions.REBASE_EDIT: - this._handleRebaseEditTap(); - break; - default: - this._fireAction(this._prependSlash(key), this.actions[key], false); - } - } - - _handleRevisionAction(key) { - switch (key) { - case RevisionActions.REBASE: - this._showActionDialog(this.$.confirmRebase); - this.$.confirmRebase.fetchRecentChanges(); - break; - case RevisionActions.CHERRYPICK: - this._handleCherrypickTap(); - break; - case RevisionActions.DOWNLOAD: - this._handleDownloadTap(); - break; - case RevisionActions.SUBMIT: - if (!this._canSubmitChange()) { return; } - this._showActionDialog(this.$.confirmSubmitDialog); - break; - default: - this._fireAction(this._prependSlash(key), - this.revisionActions[key], true); - } - } - - _prependSlash(key) { - return key === '/' ? key : `/${key}`; - } - - /** - * _hasKnownChainState set to true true if hasParent is defined (can be - * either true or false). set to false otherwise. - */ - _computeChainState(hasParent) { - this._hasKnownChainState = true; - } - - _calculateDisabled(action, hasKnownChainState) { - if (action.__key === 'rebase' && hasKnownChainState === false) { - return true; - } - return !action.enabled; - } - - _handleConfirmDialogCancel() { - this._hideAllDialogs(); - } - - _hideAllDialogs() { - const dialogEls = - Polymer.dom(this.root).querySelectorAll('.confirmDialog'); - for (const dialogEl of dialogEls) { dialogEl.hidden = true; } - this.$.overlay.close(); - } - - _handleRebaseConfirm(e) { - const el = this.$.confirmRebase; - const payload = {base: e.detail.base}; - this.$.overlay.close(); - el.hidden = true; - this._fireAction('/rebase', this.revisionActions.rebase, true, payload); - } - - _handleCherrypickConfirm() { - this._handleCherryPickRestApi(false); - } - - _handleCherrypickConflictConfirm() { - this._handleCherryPickRestApi(true); - } - - _handleCherryPickRestApi(conflicts) { - const el = this.$.confirmCherrypick; - if (!el.branch) { - this.fire('show-alert', {message: ERR_BRANCH_EMPTY}); - return; - } - if (!el.message) { - this.fire('show-alert', {message: ERR_COMMIT_EMPTY}); - return; - } - this.$.overlay.close(); - el.hidden = true; - this._fireAction( - '/cherrypick', - this.revisionActions.cherrypick, - true, - { - destination: el.branch, - base: el.baseCommit ? el.baseCommit : null, - message: el.message, - allow_conflicts: conflicts, - } - ); - } - - _handleMoveConfirm() { - const el = this.$.confirmMove; - if (!el.branch) { - this.fire('show-alert', {message: ERR_BRANCH_EMPTY}); - return; - } - this.$.overlay.close(); - el.hidden = true; - this._fireAction( - '/move', - this.actions.move, - false, - { - destination_branch: el.branch, - message: el.message, - } - ); - } - - _handleRevertDialogConfirm(e) { - const revertType = e.detail.revertType; - const message = e.detail.message; - const el = this.$.confirmRevertDialog; - this.$.overlay.close(); - el.hidden = true; - switch (revertType) { - case REVERT_TYPES.REVERT_SINGLE_CHANGE: - this._fireAction('/revert', this.actions.revert, false, - {message}); - break; - case REVERT_TYPES.REVERT_SUBMISSION: - this._fireAction('/revert_submission', this.actions.revert_submission, - false, {message}); - break; - default: - console.error('invalid revert type'); - } - } - - _handleRevertSubmissionDialogConfirm() { - const el = this.$.confirmRevertSubmissionDialog; - this.$.overlay.close(); - el.hidden = true; - this._fireAction('/revert_submission', this.actions.revert_submission, - false, {message: el.message}); - } - - _handleAbandonDialogConfirm() { - const el = this.$.confirmAbandonDialog; - this.$.overlay.close(); - el.hidden = true; - this._fireAction('/abandon', this.actions.abandon, false, - {message: el.message}); - } - - _handleCreateFollowUpChange() { - this.$.createFollowUpChange.handleCreateChange(); - this._handleCloseCreateFollowUpChange(); - } - - _handleCloseCreateFollowUpChange() { - this.$.overlay.close(); - } - - _handleDeleteConfirm() { - this._fireAction('/', this.actions[ChangeActions.DELETE], false); - } - - _handleDeleteEditConfirm() { - this._hideAllDialogs(); - - this._fireAction('/edit', this.actions.deleteEdit, false); - } - - _handleSubmitConfirm() { - if (!this._canSubmitChange()) { return; } - this._hideAllDialogs(); - this._fireAction('/submit', this.revisionActions.submit, true); - } - - _getActionOverflowIndex(type, key) { - return this._overflowActions - .findIndex(action => action.type === type && action.key === key); - } - - _setLoadingOnButtonWithKey(type, key) { - this._actionLoadingMessage = this._computeLoadingLabel(key); - let buttonKey = key; - // TODO(dhruvsri): clean this up later - // If key is revert-submission, then button key should be 'revert' - if (buttonKey === ChangeActions.REVERT_SUBMISSION) { - // Revert submission button no longer exists - buttonKey = ChangeActions.REVERT; - } - - // If the action appears in the overflow menu. - if (this._getActionOverflowIndex(type, buttonKey) !== -1) { - this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' : - buttonKey); - return function() { - this._actionLoadingMessage = ''; - this._disabledMenuActions = []; - }.bind(this); - } - - // Otherwise it's a top-level action. - const buttonEl = this.shadowRoot - .querySelector(`[data-action-key="${buttonKey}"]`); - buttonEl.setAttribute('loading', true); - buttonEl.disabled = true; - return function() { - this._actionLoadingMessage = ''; - buttonEl.removeAttribute('loading'); - buttonEl.disabled = false; - }.bind(this); - } - - /** - * @param {string} endpoint - * @param {!Object|undefined} action - * @param {boolean} revAction - * @param {!Object|string=} opt_payload - */ - _fireAction(endpoint, action, revAction, opt_payload) { - const cleanupFn = - this._setLoadingOnButtonWithKey(action.__type, action.__key); - - this._send(action.method, opt_payload, endpoint, revAction, cleanupFn, - action).then(this._handleResponse.bind(this, action)); - } - - _showActionDialog(dialog) { - this._hideAllDialogs(); - - dialog.hidden = false; - this.$.overlay.open().then(() => { - if (dialog.resetFocus) { - dialog.resetFocus(); - } - }); - } - - // TODO(rmistry): Redo this after - // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved. - _setLabelValuesOnRevert(newChangeId) { - const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); - if (!labels) { return Promise.resolve(); } - return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels}); - } - - _handleResponse(action, response) { - if (!response) { return; } - return this.$.restAPI.getResponseObject(response).then(obj => { - switch (action.__key) { - case ChangeActions.REVERT: - this._waitForChangeReachable(obj._number) - .then(() => this._setLabelValuesOnRevert(obj._number)) - .then(() => { - Gerrit.Nav.navigateToChange(obj); - }); - break; - case RevisionActions.CHERRYPICK: - this._waitForChangeReachable(obj._number).then(() => { - Gerrit.Nav.navigateToChange(obj); - }); - break; - case ChangeActions.DELETE: - if (action.__type === ActionType.CHANGE) { - Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot()); - } - break; - case ChangeActions.WIP: - case ChangeActions.DELETE_EDIT: - case ChangeActions.PUBLISH_EDIT: - case ChangeActions.REBASE_EDIT: - Gerrit.Nav.navigateToChange(this.change); - break; - case ChangeActions.REVERT_SUBMISSION: - if (!obj.revert_changes || !obj.revert_changes.length) return; - /* If there is only 1 change then gerrit will automatically - redirect to that change */ - Gerrit.Nav.navigateToSearchQuery('topic: ' + - obj.revert_changes[0].topic); - break; - default: - this.dispatchEvent(new CustomEvent('reload-change', - {detail: {action: action.__key}, bubbles: false})); - break; - } - }); - } - - _handleShowRevertSubmissionChangesConfirm() { - this._hideAllDialogs(); - } - - _handleResponseError(action, response, body) { - if (action && action.__key === RevisionActions.CHERRYPICK) { - if (response && response.status === 409 && - body && !body.allow_conflicts) { - return this._showActionDialog( - this.$.confirmCherrypickConflict); - } - } - return response.text().then(errText => { - this.fire('show-error', - {message: `Could not perform action: ${errText}`}); - if (!errText.startsWith('Change is already up to date')) { - throw Error(errText); - } - }); - } - - /** - * @param {string} method - * @param {string|!Object|undefined} payload - * @param {string} actionEndpoint - * @param {boolean} revisionAction - * @param {?Function} cleanupFn - * @param {!Object|undefined} action - */ - _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) { - const handleError = response => { - cleanupFn.call(this); - this._handleResponseError(action, response, payload); - }; - - return this.fetchChangeUpdates(this.change, this.$.restAPI) - .then(result => { - if (!result.isLatest) { - this.fire('show-alert', { - message: 'Cannot set label: a newer patch has been ' + - 'uploaded to this change.', - action: 'Reload', - callback: () => { - // Load the current change without any patch range. - Gerrit.Nav.navigateToChange(this.change); - }, - }); - - // Because this is not a network error, call the cleanup function - // but not the error handler. - cleanupFn(); - - return Promise.resolve(); - } - const patchNum = revisionAction ? this.latestPatchNum : null; - return this.$.restAPI.executeChangeAction(this.changeNum, method, - actionEndpoint, patchNum, payload, handleError) - .then(response => { - cleanupFn.call(this); - return response; - }); - }); - } - - _handleAbandonTap() { - this._showActionDialog(this.$.confirmAbandonDialog); - } - - _handleCherrypickTap() { - this.$.confirmCherrypick.branch = ''; - this._showActionDialog(this.$.confirmCherrypick); - } - - _handleMoveTap() { - this.$.confirmMove.branch = ''; - this.$.confirmMove.message = ''; - this._showActionDialog(this.$.confirmMove); - } - - _handleDownloadTap() { - this.fire('download-tap', null, {bubbles: false}); - } - - _handleDeleteTap() { - this._showActionDialog(this.$.confirmDeleteDialog); - } - - _handleDeleteEditTap() { - this._showActionDialog(this.$.confirmDeleteEditDialog); - } - - _handleFollowUpTap() { - this._showActionDialog(this.$.createFollowUpDialog); - } - - _handleWipTap() { - this._fireAction('/wip', this.actions.wip, false); - } - - _handlePublishEditTap() { - this._fireAction('/edit:publish', this.actions.publishEdit, false); - } - - _handleRebaseEditTap() { - this._fireAction('/edit:rebase', this.actions.rebaseEdit, false); - } - - _handleHideBackgroundContent() { - this.$.mainContent.classList.add('overlayOpen'); - } - - _handleShowBackgroundContent() { - this.$.mainContent.classList.remove('overlayOpen'); - } - - /** - * Merge sources of change actions into a single ordered array of action - * values. - * - * @param {!Array} changeActionsRecord - * @param {!Array} revisionActionsRecord - * @param {!Array} primariesRecord - * @param {!Array} additionalActionsRecord - * @param {!Object} change The change object. - * @return {!Array} - */ - _computeAllActions(changeActionsRecord, revisionActionsRecord, - primariesRecord, additionalActionsRecord, change) { - // Polymer 2: check for undefined - if ([ - changeActionsRecord, - revisionActionsRecord, - primariesRecord, - additionalActionsRecord, - change, - ].some(arg => arg === undefined)) { - return []; - } - - const revisionActionValues = this._getActionValues(revisionActionsRecord, - primariesRecord, additionalActionsRecord, ActionType.REVISION); - const changeActionValues = this._getActionValues(changeActionsRecord, - primariesRecord, additionalActionsRecord, ActionType.CHANGE); - const quickApprove = this._getQuickApproveAction(); - if (quickApprove) { - changeActionValues.unshift(quickApprove); - } - - return revisionActionValues - .concat(changeActionValues) - .sort(this._actionComparator.bind(this)) - .map(action => { - if (ACTIONS_WITH_ICONS.has(action.__key)) { - action.icon = action.__key; - } - return action; - }) - .filter(action => !this._shouldSkipAction(action)); - } - - _getActionPriority(action) { - if (action.__type && action.__key) { - const overrideAction = this._actionPriorityOverrides - .find(i => i.type === action.__type && i.key === action.__key); - - if (overrideAction !== undefined) { - return overrideAction.priority; - } - } - if (action.__key === 'review') { - return ActionPriority.REVIEW; - } else if (action.__primary) { - return ActionPriority.PRIMARY; - } else if (action.__type === ActionType.CHANGE) { - return ActionPriority.CHANGE; - } else if (action.__type === ActionType.REVISION) { - return ActionPriority.REVISION; - } - return ActionPriority.DEFAULT; - } - - /** - * Sort comparator to define the order of change actions. - */ - _actionComparator(actionA, actionB) { - const priorityDelta = this._getActionPriority(actionA) - - this._getActionPriority(actionB); - // Sort by the button label if same priority. - if (priorityDelta === 0) { - return actionA.label > actionB.label ? 1 : -1; - } else { - return priorityDelta; - } - } - - _shouldSkipAction(action) { - return SKIP_ACTION_KEYS.includes(action.__key); - } - - _computeTopLevelActions(actionRecord, hiddenActionsRecord) { - const hiddenActions = hiddenActionsRecord.base || []; - return actionRecord.base.filter(a => { - const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; - return !(overflow || hiddenActions.includes(a.__key)); - }); - } - - _filterPrimaryActions(_topLevelActions) { - this._topLevelPrimaryActions = _topLevelActions.filter(action => - action.__primary); - this._topLevelSecondaryActions = _topLevelActions.filter(action => - !action.__primary); - } - - _computeMenuActions(actionRecord, hiddenActionsRecord) { - const hiddenActions = hiddenActionsRecord.base || []; - return actionRecord.base.filter(a => { - const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; - return overflow && !hiddenActions.includes(a.__key); - }).map(action => { - let key = action.__key; - if (key === '/') { key = 'delete'; } - return { - name: action.label, - id: `${key}-${action.__type}`, - action, - tooltip: action.title, - }; - }); - } - - /** - * Occasionally, a change created by a change action is not yet knwon to the - * API for a brief time. Wait for the given change number to be recognized. - * - * Returns a promise that resolves with true if a request is recognized, or - * false if the change was never recognized after all attempts. - * - * @param {number} changeNum - * @return {Promise<boolean>} - */ - _waitForChangeReachable(changeNum) { - let attempsRemaining = AWAIT_CHANGE_ATTEMPTS; - return new Promise(resolve => { - const check = () => { - attempsRemaining--; - // Pass a no-op error handler to avoid the "not found" error toast. - this.$.restAPI.getChange(changeNum, () => {}).then(response => { - // If the response is 404, the response will be undefined. - if (response) { - resolve(true); - return; - } - - if (attempsRemaining) { - this.async(check, AWAIT_CHANGE_TIMEOUT_MS); - } else { - resolve(false); - } - }); - }; - check(); - }); - } - - _handleEditTap() { - this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false})); - } - - _handleStopEditTap() { - this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false})); - } - - _computeHasTooltip(title) { - return !!title; - } - - _computeHasIcon(action) { - return action.icon ? '' : 'hidden'; + const index = this._getActionOverflowIndex(type, key); + const action = { + type, + key, + overflow, + }; + if (!overflow && index !== -1) { + this.splice('_overflowActions', index, 1); + } else if (overflow) { + this.push('_overflowActions', action); } } - customElements.define(GrChangeActions.is, GrChangeActions); -})(); + setActionPriority(type, key, priority) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error(`Invalid action type given: ${type}`); + } + const index = this._actionPriorityOverrides + .findIndex(action => action.type === type && action.key === key); + const action = { + type, + key, + priority, + }; + if (index !== -1) { + this.set('_actionPriorityOverrides', index, action); + } else { + this.push('_actionPriorityOverrides', action); + } + } + + setActionHidden(type, key, hidden) { + if (type !== ActionType.CHANGE && type !== ActionType.REVISION) { + throw Error(`Invalid action type given: ${type}`); + } + + const idx = this._hiddenActions.indexOf(key); + if (hidden && idx === -1) { + this.push('_hiddenActions', key); + } else if (!hidden && idx !== -1) { + this.splice('_hiddenActions', idx, 1); + } + } + + getActionDetails(action) { + if (this.revisionActions[action]) { + return this.revisionActions[action]; + } else if (this.actions[action]) { + return this.actions[action]; + } + } + + _indexOfActionButtonWithKey(key) { + for (let i = 0; i < this._additionalActions.length; i++) { + if (this._additionalActions[i].__key === key) { + return i; + } + } + return -1; + } + + _getRevisionActions() { + return this.$.restAPI.getChangeRevisionActions(this.changeNum, + this.latestPatchNum); + } + + _shouldHideActions(actions, loading) { + return loading || !actions || !actions.base || !actions.base.length; + } + + _keyCount(changeRecord) { + return Object.keys((changeRecord && changeRecord.base) || {}).length; + } + + _actionsChanged(actionsChangeRecord, revisionActionsChangeRecord, + additionalActionsChangeRecord) { + // Polymer 2: check for undefined + if ([ + actionsChangeRecord, + revisionActionsChangeRecord, + additionalActionsChangeRecord, + ].some(arg => arg === undefined)) { + return; + } + + const additionalActions = (additionalActionsChangeRecord && + additionalActionsChangeRecord.base) || []; + this.hidden = this._keyCount(actionsChangeRecord) === 0 && + this._keyCount(revisionActionsChangeRecord) === 0 && + additionalActions.length === 0; + this._actionLoadingMessage = ''; + this._disabledMenuActions = []; + + const revisionActions = revisionActionsChangeRecord.base || {}; + if (Object.keys(revisionActions).length !== 0) { + if (!revisionActions.download) { + this.set('revisionActions.download', DOWNLOAD_ACTION); + } + } + } + + /** + * @param {string=} actionName + */ + _deleteAndNotify(actionName) { + if (this.actions && this.actions[actionName]) { + delete this.actions[actionName]; + // We assign a fake value of 'false' to support Polymer 2 + // see https://github.com/Polymer/polymer/issues/2631 + this.notifyPath('actions.' + actionName, false); + } + } + + _editStatusChanged(editMode, editPatchsetLoaded, + editBasedOnCurrentPatchSet, disableEdit) { + // Polymer 2: check for undefined + if ([ + editMode, + editBasedOnCurrentPatchSet, + disableEdit, + ].some(arg => arg === undefined)) { + return; + } + + if (disableEdit) { + this._deleteAndNotify('publishEdit'); + this._deleteAndNotify('rebaseEdit'); + this._deleteAndNotify('deleteEdit'); + this._deleteAndNotify('stopEdit'); + this._deleteAndNotify('edit'); + return; + } + if (this.actions && editPatchsetLoaded) { + // Only show actions that mutate an edit if an actual edit patch set + // is loaded. + if (this.changeIsOpen(this.change)) { + if (editBasedOnCurrentPatchSet) { + if (!this.actions.publishEdit) { + this.set('actions.publishEdit', PUBLISH_EDIT); + } + this._deleteAndNotify('rebaseEdit'); + } else { + if (!this.actions.rebaseEdit) { + this.set('actions.rebaseEdit', REBASE_EDIT); + } + this._deleteAndNotify('publishEdit'); + } + } + if (!this.actions.deleteEdit) { + this.set('actions.deleteEdit', DELETE_EDIT); + } + } else { + this._deleteAndNotify('publishEdit'); + this._deleteAndNotify('rebaseEdit'); + this._deleteAndNotify('deleteEdit'); + } + + if (this.actions && this.changeIsOpen(this.change)) { + // Only show edit button if there is no edit patchset loaded and the + // file list is not in edit mode. + if (editPatchsetLoaded || editMode) { + this._deleteAndNotify('edit'); + } else { + if (!this.actions.edit) { this.set('actions.edit', EDIT); } + } + // Only show STOP_EDIT if edit mode is enabled, but no edit patch set + // is loaded. + if (editMode && !editPatchsetLoaded) { + if (!this.actions.stopEdit) { + this.set('actions.stopEdit', STOP_EDIT); + } + } else { + this._deleteAndNotify('stopEdit'); + } + } else { + // Remove edit button. + this._deleteAndNotify('edit'); + } + } + + _getValuesFor(obj) { + return Object.keys(obj).map(key => obj[key]); + } + + _getLabelStatus(label) { + if (label.approved) { + return LabelStatus.OK; + } else if (label.rejected) { + return LabelStatus.REJECT; + } else if (label.optional) { + return LabelStatus.OPTIONAL; + } else { + return LabelStatus.NEED; + } + } + + /** + * Get highest score for last missing permitted label for current change. + * Returns null if no labels permitted or more than one label missing. + * + * @return {{label: string, score: string}|null} + */ + _getTopMissingApproval() { + if (!this.change || + !this.change.labels || + !this.change.permitted_labels) { + return null; + } + let result; + for (const label in this.change.labels) { + if (!(label in this.change.permitted_labels)) { + continue; + } + if (this.change.permitted_labels[label].length === 0) { + continue; + } + const status = this._getLabelStatus(this.change.labels[label]); + if (status === LabelStatus.NEED) { + if (result) { + // More than one label is missing, so it's unclear which to quick + // approve, return null; + return null; + } + result = label; + } else if (status === LabelStatus.REJECT || + status === LabelStatus.IMPOSSIBLE) { + return null; + } + } + if (result) { + const score = this.change.permitted_labels[result].slice(-1)[0]; + const maxScore = + Object.keys(this.change.labels[result].values).slice(-1)[0]; + if (score === maxScore) { + // Allow quick approve only for maximal score. + return { + label: result, + score, + }; + } + } + return null; + } + + hideQuickApproveAction() { + this._topLevelSecondaryActions = + this._topLevelSecondaryActions + .filter(sa => sa.key !== QUICK_APPROVE_ACTION.key); + this._hideQuickApproveAction = true; + } + + _getQuickApproveAction() { + if (this._hideQuickApproveAction) { + return null; + } + const approval = this._getTopMissingApproval(); + if (!approval) { + return null; + } + const action = Object.assign({}, QUICK_APPROVE_ACTION); + action.label = approval.label + approval.score; + const review = { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: {}, + }; + review.labels[approval.label] = approval.score; + action.payload = review; + return action; + } + + _getActionValues(actionsChangeRecord, primariesChangeRecord, + additionalActionsChangeRecord, type) { + if (!actionsChangeRecord || !primariesChangeRecord) { return []; } + + const actions = actionsChangeRecord.base || {}; + const primaryActionKeys = primariesChangeRecord.base || []; + const result = []; + const values = this._getValuesFor( + type === ActionType.CHANGE ? ChangeActions : RevisionActions); + const pluginActions = []; + Object.keys(actions).forEach(a => { + actions[a].__key = a; + actions[a].__type = type; + actions[a].__primary = primaryActionKeys.includes(a); + // Plugin actions always contain ~ in the key. + if (a.indexOf('~') !== -1) { + this._populateActionUrl(actions[a]); + pluginActions.push(actions[a]); + // Add server-side provided plugin actions to overflow menu. + this._overflowActions.push({ + type, + key: a, + }); + return; + } else if (!values.includes(a)) { + return; + } + actions[a].label = this._getActionLabel(actions[a]); + + // Triggers a re-render by ensuring object inequality. + result.push(Object.assign({}, actions[a])); + }); + + let additionalActions = (additionalActionsChangeRecord && + additionalActionsChangeRecord.base) || []; + additionalActions = additionalActions + .filter(a => a.__type === type) + .map(a => { + a.__primary = primaryActionKeys.includes(a.__key); + // Triggers a re-render by ensuring object inequality. + return Object.assign({}, a); + }); + return result.concat(additionalActions).concat(pluginActions); + } + + _populateActionUrl(action) { + const patchNum = + action.__type === ActionType.REVISION ? this.latestPatchNum : null; + this.$.restAPI.getChangeActionURL( + this.changeNum, patchNum, '/' + action.__key) + .then(url => action.__url = url); + } + + /** + * Given a change action, return a display label that uses the appropriate + * casing or includes explanatory details. + */ + _getActionLabel(action) { + if (action.label === 'Delete') { + // This label is common within change and revision actions. Make it more + // explicit to the user. + return 'Delete change'; + } else if (action.label === 'WIP') { + return 'Mark as work in progress'; + } + // Otherwise, just map the name to sentence case. + return this._toSentenceCase(action.label); + } + + /** + * Capitalize the first letter and lowecase all others. + * + * @param {string} s + * @return {string} + */ + _toSentenceCase(s) { + if (!s.length) { return ''; } + return s[0].toUpperCase() + s.slice(1).toLowerCase(); + } + + _computeLoadingLabel(action) { + return ActionLoadingLabels[action] || 'Working...'; + } + + _canSubmitChange() { + return this.$.jsAPI.canSubmitChange(this.change, + this._getRevision(this.change, this.latestPatchNum)); + } + + _getRevision(change, patchNum) { + for (const rev of Object.values(change.revisions)) { + if (this.patchNumEquals(rev._number, patchNum)) { + return rev; + } + } + return null; + } + + showRevertDialog() { + const query = 'submissionid:' + this.change.submission_id; + /* A chromium plugin expects that the modifyRevertMsg hook will only + be called after the revert button is pressed, hence we populate the + revert dialog after revert button is pressed. */ + this.$.restAPI.getChanges('', query) + .then(changes => { + this.$.confirmRevertDialog.populate(this.change, + this.commitMessage, changes); + this._showActionDialog(this.$.confirmRevertDialog); + }); + } + + showRevertSubmissionDialog() { + const query = 'submissionid:' + this.change.submission_id; + this.$.restAPI.getChanges('', query) + .then(changes => { + this.$.confirmRevertSubmissionDialog. + _populateRevertSubmissionMessage( + this.commitMessage, this.change, changes); + this._showActionDialog(this.$.confirmRevertSubmissionDialog); + }); + } + + _handleActionTap(e) { + e.preventDefault(); + let el = dom(e).localTarget; + while (el.tagName.toLowerCase() !== 'gr-button') { + if (!el.parentElement) { return; } + el = el.parentElement; + } + + const key = el.getAttribute('data-action-key'); + if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || + key.indexOf('~') !== -1) { + this.fire(`${key}-tap`, {node: el}); + return; + } + const type = el.getAttribute('data-action-type'); + this._handleAction(type, key); + } + + _handleOveflowItemTap(e) { + e.preventDefault(); + const el = dom(e).localTarget; + const key = e.detail.action.__key; + if (key.startsWith(ADDITIONAL_ACTION_KEY_PREFIX) || + key.indexOf('~') !== -1) { + this.fire(`${key}-tap`, {node: el}); + return; + } + this._handleAction(e.detail.action.__type, e.detail.action.__key); + } + + _handleAction(type, key) { + this.$.reporting.reportInteraction(`${type}-${key}`); + switch (type) { + case ActionType.REVISION: + this._handleRevisionAction(key); + break; + case ActionType.CHANGE: + this._handleChangeAction(key); + break; + default: + this._fireAction(this._prependSlash(key), this.actions[key], false); + } + } + + _handleChangeAction(key) { + let action; + switch (key) { + case ChangeActions.REVERT: + this.showRevertDialog(); + break; + case ChangeActions.REVERT_SUBMISSION: + this.showRevertSubmissionDialog(); + break; + case ChangeActions.ABANDON: + this._showActionDialog(this.$.confirmAbandonDialog); + break; + case QUICK_APPROVE_ACTION.key: + action = this._allActionValues.find(o => o.key === key); + this._fireAction( + this._prependSlash(key), action, true, action.payload); + break; + case ChangeActions.EDIT: + this._handleEditTap(); + break; + case ChangeActions.STOP_EDIT: + this._handleStopEditTap(); + break; + case ChangeActions.DELETE: + this._handleDeleteTap(); + break; + case ChangeActions.DELETE_EDIT: + this._handleDeleteEditTap(); + break; + case ChangeActions.FOLLOW_UP: + this._handleFollowUpTap(); + break; + case ChangeActions.WIP: + this._handleWipTap(); + break; + case ChangeActions.MOVE: + this._handleMoveTap(); + break; + case ChangeActions.PUBLISH_EDIT: + this._handlePublishEditTap(); + break; + case ChangeActions.REBASE_EDIT: + this._handleRebaseEditTap(); + break; + default: + this._fireAction(this._prependSlash(key), this.actions[key], false); + } + } + + _handleRevisionAction(key) { + switch (key) { + case RevisionActions.REBASE: + this._showActionDialog(this.$.confirmRebase); + this.$.confirmRebase.fetchRecentChanges(); + break; + case RevisionActions.CHERRYPICK: + this._handleCherrypickTap(); + break; + case RevisionActions.DOWNLOAD: + this._handleDownloadTap(); + break; + case RevisionActions.SUBMIT: + if (!this._canSubmitChange()) { return; } + this._showActionDialog(this.$.confirmSubmitDialog); + break; + default: + this._fireAction(this._prependSlash(key), + this.revisionActions[key], true); + } + } + + _prependSlash(key) { + return key === '/' ? key : `/${key}`; + } + + /** + * _hasKnownChainState set to true true if hasParent is defined (can be + * either true or false). set to false otherwise. + */ + _computeChainState(hasParent) { + this._hasKnownChainState = true; + } + + _calculateDisabled(action, hasKnownChainState) { + if (action.__key === 'rebase' && hasKnownChainState === false) { + return true; + } + return !action.enabled; + } + + _handleConfirmDialogCancel() { + this._hideAllDialogs(); + } + + _hideAllDialogs() { + const dialogEls = + dom(this.root).querySelectorAll('.confirmDialog'); + for (const dialogEl of dialogEls) { dialogEl.hidden = true; } + this.$.overlay.close(); + } + + _handleRebaseConfirm(e) { + const el = this.$.confirmRebase; + const payload = {base: e.detail.base}; + this.$.overlay.close(); + el.hidden = true; + this._fireAction('/rebase', this.revisionActions.rebase, true, payload); + } + + _handleCherrypickConfirm() { + this._handleCherryPickRestApi(false); + } + + _handleCherrypickConflictConfirm() { + this._handleCherryPickRestApi(true); + } + + _handleCherryPickRestApi(conflicts) { + const el = this.$.confirmCherrypick; + if (!el.branch) { + this.fire('show-alert', {message: ERR_BRANCH_EMPTY}); + return; + } + if (!el.message) { + this.fire('show-alert', {message: ERR_COMMIT_EMPTY}); + return; + } + this.$.overlay.close(); + el.hidden = true; + this._fireAction( + '/cherrypick', + this.revisionActions.cherrypick, + true, + { + destination: el.branch, + base: el.baseCommit ? el.baseCommit : null, + message: el.message, + allow_conflicts: conflicts, + } + ); + } + + _handleMoveConfirm() { + const el = this.$.confirmMove; + if (!el.branch) { + this.fire('show-alert', {message: ERR_BRANCH_EMPTY}); + return; + } + this.$.overlay.close(); + el.hidden = true; + this._fireAction( + '/move', + this.actions.move, + false, + { + destination_branch: el.branch, + message: el.message, + } + ); + } + + _handleRevertDialogConfirm(e) { + const revertType = e.detail.revertType; + const message = e.detail.message; + const el = this.$.confirmRevertDialog; + this.$.overlay.close(); + el.hidden = true; + switch (revertType) { + case REVERT_TYPES.REVERT_SINGLE_CHANGE: + this._fireAction('/revert', this.actions.revert, false, + {message}); + break; + case REVERT_TYPES.REVERT_SUBMISSION: + this._fireAction('/revert_submission', this.actions.revert_submission, + false, {message}); + break; + default: + console.error('invalid revert type'); + } + } + + _handleRevertSubmissionDialogConfirm() { + const el = this.$.confirmRevertSubmissionDialog; + this.$.overlay.close(); + el.hidden = true; + this._fireAction('/revert_submission', this.actions.revert_submission, + false, {message: el.message}); + } + + _handleAbandonDialogConfirm() { + const el = this.$.confirmAbandonDialog; + this.$.overlay.close(); + el.hidden = true; + this._fireAction('/abandon', this.actions.abandon, false, + {message: el.message}); + } + + _handleCreateFollowUpChange() { + this.$.createFollowUpChange.handleCreateChange(); + this._handleCloseCreateFollowUpChange(); + } + + _handleCloseCreateFollowUpChange() { + this.$.overlay.close(); + } + + _handleDeleteConfirm() { + this._fireAction('/', this.actions[ChangeActions.DELETE], false); + } + + _handleDeleteEditConfirm() { + this._hideAllDialogs(); + + this._fireAction('/edit', this.actions.deleteEdit, false); + } + + _handleSubmitConfirm() { + if (!this._canSubmitChange()) { return; } + this._hideAllDialogs(); + this._fireAction('/submit', this.revisionActions.submit, true); + } + + _getActionOverflowIndex(type, key) { + return this._overflowActions + .findIndex(action => action.type === type && action.key === key); + } + + _setLoadingOnButtonWithKey(type, key) { + this._actionLoadingMessage = this._computeLoadingLabel(key); + let buttonKey = key; + // TODO(dhruvsri): clean this up later + // If key is revert-submission, then button key should be 'revert' + if (buttonKey === ChangeActions.REVERT_SUBMISSION) { + // Revert submission button no longer exists + buttonKey = ChangeActions.REVERT; + } + + // If the action appears in the overflow menu. + if (this._getActionOverflowIndex(type, buttonKey) !== -1) { + this.push('_disabledMenuActions', buttonKey === '/' ? 'delete' : + buttonKey); + return function() { + this._actionLoadingMessage = ''; + this._disabledMenuActions = []; + }.bind(this); + } + + // Otherwise it's a top-level action. + const buttonEl = this.shadowRoot + .querySelector(`[data-action-key="${buttonKey}"]`); + buttonEl.setAttribute('loading', true); + buttonEl.disabled = true; + return function() { + this._actionLoadingMessage = ''; + buttonEl.removeAttribute('loading'); + buttonEl.disabled = false; + }.bind(this); + } + + /** + * @param {string} endpoint + * @param {!Object|undefined} action + * @param {boolean} revAction + * @param {!Object|string=} opt_payload + */ + _fireAction(endpoint, action, revAction, opt_payload) { + const cleanupFn = + this._setLoadingOnButtonWithKey(action.__type, action.__key); + + this._send(action.method, opt_payload, endpoint, revAction, cleanupFn, + action).then(this._handleResponse.bind(this, action)); + } + + _showActionDialog(dialog) { + this._hideAllDialogs(); + + dialog.hidden = false; + this.$.overlay.open().then(() => { + if (dialog.resetFocus) { + dialog.resetFocus(); + } + }); + } + + // TODO(rmistry): Redo this after + // https://bugs.chromium.org/p/gerrit/issues/detail?id=4671 is resolved. + _setLabelValuesOnRevert(newChangeId) { + const labels = this.$.jsAPI.getLabelValuesPostRevert(this.change); + if (!labels) { return Promise.resolve(); } + return this.$.restAPI.saveChangeReview(newChangeId, 'current', {labels}); + } + + _handleResponse(action, response) { + if (!response) { return; } + return this.$.restAPI.getResponseObject(response).then(obj => { + switch (action.__key) { + case ChangeActions.REVERT: + this._waitForChangeReachable(obj._number) + .then(() => this._setLabelValuesOnRevert(obj._number)) + .then(() => { + Gerrit.Nav.navigateToChange(obj); + }); + break; + case RevisionActions.CHERRYPICK: + this._waitForChangeReachable(obj._number).then(() => { + Gerrit.Nav.navigateToChange(obj); + }); + break; + case ChangeActions.DELETE: + if (action.__type === ActionType.CHANGE) { + Gerrit.Nav.navigateToRelativeUrl(Gerrit.Nav.getUrlForRoot()); + } + break; + case ChangeActions.WIP: + case ChangeActions.DELETE_EDIT: + case ChangeActions.PUBLISH_EDIT: + case ChangeActions.REBASE_EDIT: + Gerrit.Nav.navigateToChange(this.change); + break; + case ChangeActions.REVERT_SUBMISSION: + if (!obj.revert_changes || !obj.revert_changes.length) return; + /* If there is only 1 change then gerrit will automatically + redirect to that change */ + Gerrit.Nav.navigateToSearchQuery('topic: ' + + obj.revert_changes[0].topic); + break; + default: + this.dispatchEvent(new CustomEvent('reload-change', + {detail: {action: action.__key}, bubbles: false})); + break; + } + }); + } + + _handleShowRevertSubmissionChangesConfirm() { + this._hideAllDialogs(); + } + + _handleResponseError(action, response, body) { + if (action && action.__key === RevisionActions.CHERRYPICK) { + if (response && response.status === 409 && + body && !body.allow_conflicts) { + return this._showActionDialog( + this.$.confirmCherrypickConflict); + } + } + return response.text().then(errText => { + this.fire('show-error', + {message: `Could not perform action: ${errText}`}); + if (!errText.startsWith('Change is already up to date')) { + throw Error(errText); + } + }); + } + + /** + * @param {string} method + * @param {string|!Object|undefined} payload + * @param {string} actionEndpoint + * @param {boolean} revisionAction + * @param {?Function} cleanupFn + * @param {!Object|undefined} action + */ + _send(method, payload, actionEndpoint, revisionAction, cleanupFn, action) { + const handleError = response => { + cleanupFn.call(this); + this._handleResponseError(action, response, payload); + }; + + return this.fetchChangeUpdates(this.change, this.$.restAPI) + .then(result => { + if (!result.isLatest) { + this.fire('show-alert', { + message: 'Cannot set label: a newer patch has been ' + + 'uploaded to this change.', + action: 'Reload', + callback: () => { + // Load the current change without any patch range. + Gerrit.Nav.navigateToChange(this.change); + }, + }); + + // Because this is not a network error, call the cleanup function + // but not the error handler. + cleanupFn(); + + return Promise.resolve(); + } + const patchNum = revisionAction ? this.latestPatchNum : null; + return this.$.restAPI.executeChangeAction(this.changeNum, method, + actionEndpoint, patchNum, payload, handleError) + .then(response => { + cleanupFn.call(this); + return response; + }); + }); + } + + _handleAbandonTap() { + this._showActionDialog(this.$.confirmAbandonDialog); + } + + _handleCherrypickTap() { + this.$.confirmCherrypick.branch = ''; + this._showActionDialog(this.$.confirmCherrypick); + } + + _handleMoveTap() { + this.$.confirmMove.branch = ''; + this.$.confirmMove.message = ''; + this._showActionDialog(this.$.confirmMove); + } + + _handleDownloadTap() { + this.fire('download-tap', null, {bubbles: false}); + } + + _handleDeleteTap() { + this._showActionDialog(this.$.confirmDeleteDialog); + } + + _handleDeleteEditTap() { + this._showActionDialog(this.$.confirmDeleteEditDialog); + } + + _handleFollowUpTap() { + this._showActionDialog(this.$.createFollowUpDialog); + } + + _handleWipTap() { + this._fireAction('/wip', this.actions.wip, false); + } + + _handlePublishEditTap() { + this._fireAction('/edit:publish', this.actions.publishEdit, false); + } + + _handleRebaseEditTap() { + this._fireAction('/edit:rebase', this.actions.rebaseEdit, false); + } + + _handleHideBackgroundContent() { + this.$.mainContent.classList.add('overlayOpen'); + } + + _handleShowBackgroundContent() { + this.$.mainContent.classList.remove('overlayOpen'); + } + + /** + * Merge sources of change actions into a single ordered array of action + * values. + * + * @param {!Array} changeActionsRecord + * @param {!Array} revisionActionsRecord + * @param {!Array} primariesRecord + * @param {!Array} additionalActionsRecord + * @param {!Object} change The change object. + * @return {!Array} + */ + _computeAllActions(changeActionsRecord, revisionActionsRecord, + primariesRecord, additionalActionsRecord, change) { + // Polymer 2: check for undefined + if ([ + changeActionsRecord, + revisionActionsRecord, + primariesRecord, + additionalActionsRecord, + change, + ].some(arg => arg === undefined)) { + return []; + } + + const revisionActionValues = this._getActionValues(revisionActionsRecord, + primariesRecord, additionalActionsRecord, ActionType.REVISION); + const changeActionValues = this._getActionValues(changeActionsRecord, + primariesRecord, additionalActionsRecord, ActionType.CHANGE); + const quickApprove = this._getQuickApproveAction(); + if (quickApprove) { + changeActionValues.unshift(quickApprove); + } + + return revisionActionValues + .concat(changeActionValues) + .sort(this._actionComparator.bind(this)) + .map(action => { + if (ACTIONS_WITH_ICONS.has(action.__key)) { + action.icon = action.__key; + } + return action; + }) + .filter(action => !this._shouldSkipAction(action)); + } + + _getActionPriority(action) { + if (action.__type && action.__key) { + const overrideAction = this._actionPriorityOverrides + .find(i => i.type === action.__type && i.key === action.__key); + + if (overrideAction !== undefined) { + return overrideAction.priority; + } + } + if (action.__key === 'review') { + return ActionPriority.REVIEW; + } else if (action.__primary) { + return ActionPriority.PRIMARY; + } else if (action.__type === ActionType.CHANGE) { + return ActionPriority.CHANGE; + } else if (action.__type === ActionType.REVISION) { + return ActionPriority.REVISION; + } + return ActionPriority.DEFAULT; + } + + /** + * Sort comparator to define the order of change actions. + */ + _actionComparator(actionA, actionB) { + const priorityDelta = this._getActionPriority(actionA) - + this._getActionPriority(actionB); + // Sort by the button label if same priority. + if (priorityDelta === 0) { + return actionA.label > actionB.label ? 1 : -1; + } else { + return priorityDelta; + } + } + + _shouldSkipAction(action) { + return SKIP_ACTION_KEYS.includes(action.__key); + } + + _computeTopLevelActions(actionRecord, hiddenActionsRecord) { + const hiddenActions = hiddenActionsRecord.base || []; + return actionRecord.base.filter(a => { + const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; + return !(overflow || hiddenActions.includes(a.__key)); + }); + } + + _filterPrimaryActions(_topLevelActions) { + this._topLevelPrimaryActions = _topLevelActions.filter(action => + action.__primary); + this._topLevelSecondaryActions = _topLevelActions.filter(action => + !action.__primary); + } + + _computeMenuActions(actionRecord, hiddenActionsRecord) { + const hiddenActions = hiddenActionsRecord.base || []; + return actionRecord.base.filter(a => { + const overflow = this._getActionOverflowIndex(a.__type, a.__key) !== -1; + return overflow && !hiddenActions.includes(a.__key); + }).map(action => { + let key = action.__key; + if (key === '/') { key = 'delete'; } + return { + name: action.label, + id: `${key}-${action.__type}`, + action, + tooltip: action.title, + }; + }); + } + + /** + * Occasionally, a change created by a change action is not yet knwon to the + * API for a brief time. Wait for the given change number to be recognized. + * + * Returns a promise that resolves with true if a request is recognized, or + * false if the change was never recognized after all attempts. + * + * @param {number} changeNum + * @return {Promise<boolean>} + */ + _waitForChangeReachable(changeNum) { + let attempsRemaining = AWAIT_CHANGE_ATTEMPTS; + return new Promise(resolve => { + const check = () => { + attempsRemaining--; + // Pass a no-op error handler to avoid the "not found" error toast. + this.$.restAPI.getChange(changeNum, () => {}).then(response => { + // If the response is 404, the response will be undefined. + if (response) { + resolve(true); + return; + } + + if (attempsRemaining) { + this.async(check, AWAIT_CHANGE_TIMEOUT_MS); + } else { + resolve(false); + } + }); + }; + check(); + }); + } + + _handleEditTap() { + this.dispatchEvent(new CustomEvent('edit-tap', {bubbles: false})); + } + + _handleStopEditTap() { + this.dispatchEvent(new CustomEvent('stop-edit-tap', {bubbles: false})); + } + + _computeHasTooltip(title) { + return !!title; + } + + _computeHasIcon(action) { + return action.icon ? '' : 'hidden'; + } +} + +customElements.define(GrChangeActions.is, GrChangeActions);
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js index 097b92c..7aa5ecd 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_html.js
@@ -1,47 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="../../admin/gr-create-change-dialog/gr-create-change-dialog.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html"> -<link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html"> -<link rel="import" href="../gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.html"> -<link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html"> -<link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html"> -<link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html"> -<link rel="import" href="../gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.html"> -<link rel="import" href="../gr-confirm-submit-dialog/gr-confirm-submit-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-change-actions"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: flex; @@ -101,144 +76,48 @@ } </style> <div id="mainContent"> - <span - id="actionLoadingMessage" - hidden$="[[!_actionLoadingMessage]]"> + <span id="actionLoadingMessage" hidden\$="[[!_actionLoadingMessage]]"> [[_actionLoadingMessage]]</span> - <section id="primaryActions" - hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"> - <template - is="dom-repeat" - items="[[_topLevelPrimaryActions]]" - as="action"> - <gr-button - link - title$="[[action.title]]" - has-tooltip="[[_computeHasTooltip(action.title)]]" - position-below="true" - data-action-key$="[[action.__key]]" - data-action-type$="[[action.__type]]" - data-label$="[[action.label]]" - disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]" - on-click="_handleActionTap"> - <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon> + <section id="primaryActions" hidden\$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"> + <template is="dom-repeat" items="[[_topLevelPrimaryActions]]" as="action"> + <gr-button link="" title\$="[[action.title]]" has-tooltip="[[_computeHasTooltip(action.title)]]" position-below="true" data-action-key\$="[[action.__key]]" data-action-type\$="[[action.__type]]" data-label\$="[[action.label]]" disabled\$="[[_calculateDisabled(action, _hasKnownChainState)]]" on-click="_handleActionTap"> + <iron-icon class\$="[[_computeHasIcon(action)]]" icon\$="gr-icons:[[action.icon]]"></iron-icon> [[action.label]] </gr-button> </template> </section> - <section id="secondaryActions" - hidden$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"> - <template - is="dom-repeat" - items="[[_topLevelSecondaryActions]]" - as="action"> - <gr-button - link - title$="[[action.title]]" - has-tooltip="[[_computeHasTooltip(action.title)]]" - position-below="true" - data-action-key$="[[action.__key]]" - data-action-type$="[[action.__type]]" - data-label$="[[action.label]]" - disabled$="[[_calculateDisabled(action, _hasKnownChainState)]]" - on-click="_handleActionTap"> - <iron-icon class$="[[_computeHasIcon(action)]]" icon$="gr-icons:[[action.icon]]"></iron-icon> + <section id="secondaryActions" hidden\$="[[_shouldHideActions(_topLevelActions.*, _loading)]]"> + <template is="dom-repeat" items="[[_topLevelSecondaryActions]]" as="action"> + <gr-button link="" title\$="[[action.title]]" has-tooltip="[[_computeHasTooltip(action.title)]]" position-below="true" data-action-key\$="[[action.__key]]" data-action-type\$="[[action.__type]]" data-label\$="[[action.label]]" disabled\$="[[_calculateDisabled(action, _hasKnownChainState)]]" on-click="_handleActionTap"> + <iron-icon class\$="[[_computeHasIcon(action)]]" icon\$="gr-icons:[[action.icon]]"></iron-icon> [[action.label]] </gr-button> </template> </section> - <gr-button hidden$="[[!_loading]]" disabled>Loading actions...</gr-button> - <gr-dropdown - id="moreActions" - link - tabindex="0" - vertical-offset="32" - horizontal-align="right" - on-tap-item="_handleOveflowItemTap" - hidden$="[[_shouldHideActions(_menuActions.*, _loading)]]" - disabled-ids="[[_disabledMenuActions]]" - items="[[_menuActions]]"> + <gr-button hidden\$="[[!_loading]]" disabled="">Loading actions...</gr-button> + <gr-dropdown id="moreActions" link="" tabindex="0" vertical-offset="32" horizontal-align="right" on-tap-item="_handleOveflowItemTap" hidden\$="[[_shouldHideActions(_menuActions.*, _loading)]]" disabled-ids="[[_disabledMenuActions]]" items="[[_menuActions]]"> <iron-icon icon="gr-icons:more-vert"></iron-icon> <span id="moreMessage">More</span> </gr-dropdown> </div> - <gr-overlay id="overlay" with-backdrop> - <gr-confirm-rebase-dialog id="confirmRebase" - class="confirmDialog" - change-number="[[change._number]]" - on-confirm="_handleRebaseConfirm" - on-cancel="_handleConfirmDialogCancel" - branch="[[change.branch]]" - has-parent="[[hasParent]]" - rebase-on-current="[[_revisionRebaseAction.rebaseOnCurrent]]" - hidden></gr-confirm-rebase-dialog> - <gr-confirm-cherrypick-dialog id="confirmCherrypick" - class="confirmDialog" - change-status="[[changeStatus]]" - commit-message="[[commitMessage]]" - commit-num="[[commitNum]]" - on-confirm="_handleCherrypickConfirm" - on-cancel="_handleConfirmDialogCancel" - project="[[change.project]]" - hidden></gr-confirm-cherrypick-dialog> - <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" - class="confirmDialog" - on-confirm="_handleCherrypickConflictConfirm" - on-cancel="_handleConfirmDialogCancel" - hidden></gr-confirm-cherrypick-conflict-dialog> - <gr-confirm-move-dialog id="confirmMove" - class="confirmDialog" - on-confirm="_handleMoveConfirm" - on-cancel="_handleConfirmDialogCancel" - project="[[change.project]]" - hidden></gr-confirm-move-dialog> - <gr-confirm-revert-dialog id="confirmRevertDialog" - class="confirmDialog" - on-confirm="_handleRevertDialogConfirm" - on-cancel="_handleConfirmDialogCancel" - hidden></gr-confirm-revert-dialog> - <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog" - class="confirmDialog" - commit-message="[[commitMessage]]" - on-confirm="_handleRevertSubmissionDialogConfirm" - on-cancel="_handleConfirmDialogCancel" - hidden></gr-confirm-revert-submission-dialog> - <gr-confirm-abandon-dialog id="confirmAbandonDialog" - class="confirmDialog" - on-confirm="_handleAbandonDialogConfirm" - on-cancel="_handleConfirmDialogCancel" - hidden></gr-confirm-abandon-dialog> - <gr-confirm-submit-dialog - id="confirmSubmitDialog" - class="confirmDialog" - change="[[change]]" - action="[[_revisionSubmitAction]]" - on-cancel="_handleConfirmDialogCancel" - on-confirm="_handleSubmitConfirm" hidden></gr-confirm-submit-dialog> - <gr-dialog id="createFollowUpDialog" - class="confirmDialog" - confirm-label="Create" - on-confirm="_handleCreateFollowUpChange" - on-cancel="_handleCloseCreateFollowUpChange"> + <gr-overlay id="overlay" with-backdrop=""> + <gr-confirm-rebase-dialog id="confirmRebase" class="confirmDialog" change-number="[[change._number]]" on-confirm="_handleRebaseConfirm" on-cancel="_handleConfirmDialogCancel" branch="[[change.branch]]" has-parent="[[hasParent]]" rebase-on-current="[[_revisionRebaseAction.rebaseOnCurrent]]" hidden=""></gr-confirm-rebase-dialog> + <gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" change-status="[[changeStatus]]" commit-message="[[commitMessage]]" commit-num="[[commitNum]]" on-confirm="_handleCherrypickConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-cherrypick-dialog> + <gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" class="confirmDialog" on-confirm="_handleCherrypickConflictConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-cherrypick-conflict-dialog> + <gr-confirm-move-dialog id="confirmMove" class="confirmDialog" on-confirm="_handleMoveConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-move-dialog> + <gr-confirm-revert-dialog id="confirmRevertDialog" class="confirmDialog" on-confirm="_handleRevertDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-revert-dialog> + <gr-confirm-revert-submission-dialog id="confirmRevertSubmissionDialog" class="confirmDialog" commit-message="[[commitMessage]]" on-confirm="_handleRevertSubmissionDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-revert-submission-dialog> + <gr-confirm-abandon-dialog id="confirmAbandonDialog" class="confirmDialog" on-confirm="_handleAbandonDialogConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-abandon-dialog> + <gr-confirm-submit-dialog id="confirmSubmitDialog" class="confirmDialog" change="[[change]]" action="[[_revisionSubmitAction]]" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleSubmitConfirm" hidden=""></gr-confirm-submit-dialog> + <gr-dialog id="createFollowUpDialog" class="confirmDialog" confirm-label="Create" on-confirm="_handleCreateFollowUpChange" on-cancel="_handleCloseCreateFollowUpChange"> <div class="header" slot="header"> Create Follow-Up Change </div> <div class="main" slot="main"> - <gr-create-change-dialog - id="createFollowUpChange" - branch="[[change.branch]]" - base-change="[[change.id]]" - repo-name="[[change.project]]" - private-by-default="[[privateByDefault]]"></gr-create-change-dialog> + <gr-create-change-dialog id="createFollowUpChange" branch="[[change.branch]]" base-change="[[change.id]]" repo-name="[[change.project]]" private-by-default="[[privateByDefault]]"></gr-create-change-dialog> </div> </gr-dialog> - <gr-dialog - id="confirmDeleteDialog" - class="confirmDialog" - confirm-label="Delete" - confirm-on-enter - on-cancel="_handleConfirmDialogCancel" - on-confirm="_handleDeleteConfirm"> + <gr-dialog id="confirmDeleteDialog" class="confirmDialog" confirm-label="Delete" confirm-on-enter="" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleDeleteConfirm"> <div class="header" slot="header"> Delete Change </div> @@ -246,13 +125,7 @@ Do you really want to delete the change? </div> </gr-dialog> - <gr-dialog - id="confirmDeleteEditDialog" - class="confirmDialog" - confirm-label="Delete" - confirm-on-enter - on-cancel="_handleConfirmDialogCancel" - on-confirm="_handleDeleteEditConfirm"> + <gr-dialog id="confirmDeleteEditDialog" class="confirmDialog" confirm-label="Delete" confirm-on-enter="" on-cancel="_handleConfirmDialogCancel" on-confirm="_handleDeleteEditConfirm"> <div class="header" slot="header"> Delete Change Edit </div> @@ -264,6 +137,4 @@ <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting" category="change-actions"></gr-reporting> - </template> - <script src="gr-change-actions.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html index ee036cf..0d78fb4 100644 --- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-actions</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-change-actions.html"> +<script type="module" src="./gr-change-actions.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-change-actions.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,885 +43,1704 @@ </template> </test-fixture> -<script> - // TODO(dhruvsri): remove use of _populateRevertMessage as it's private - suite('gr-change-actions tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-change-actions.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +// TODO(dhruvsri): remove use of _populateRevertMessage as it's private +suite('gr-change-actions tests', () => { + let element; + let sandbox; - suite('basic tests', () => { - setup(() => { - stub('gr-rest-api-interface', { - getChangeRevisionActions() { + suite('basic tests', () => { + setup(() => { + stub('gr-rest-api-interface', { + getChangeRevisionActions() { + return Promise.resolve({ + cherrypick: { + method: 'POST', + label: 'Cherry Pick', + title: 'Cherry pick change to a different branch', + enabled: true, + }, + rebase: { + method: 'POST', + label: 'Rebase', + title: 'Rebase onto tip of branch or parent change', + enabled: true, + }, + submit: { + method: 'POST', + label: 'Submit', + title: 'Submit patch set 2 into master', + enabled: true, + }, + revert_submission: { + method: 'POST', + label: 'Revert submission', + title: 'Revert this submission', + enabled: true, + }, + }); + }, + send(method, url, payload) { + if (method !== 'POST') { + return Promise.reject(new Error('bad method')); + } + + if (url === '/changes/test~42/revisions/2/submit') { return Promise.resolve({ - cherrypick: { - method: 'POST', - label: 'Cherry Pick', - title: 'Cherry pick change to a different branch', - enabled: true, - }, - rebase: { - method: 'POST', - label: 'Rebase', - title: 'Rebase onto tip of branch or parent change', - enabled: true, - }, - submit: { - method: 'POST', - label: 'Submit', - title: 'Submit patch set 2 into master', - enabled: true, - }, - revert_submission: { - method: 'POST', - label: 'Revert submission', - title: 'Revert this submission', - enabled: true, - }, + ok: true, + text() { return Promise.resolve(')]}\'\n{}'); }, }); - }, - send(method, url, payload) { - if (method !== 'POST') { - return Promise.reject(new Error('bad method')); - } + } else if (url === '/changes/test~42/revisions/2/rebase') { + return Promise.resolve({ + ok: true, + text() { return Promise.resolve(')]}\'\n{}'); }, + }); + } - if (url === '/changes/test~42/revisions/2/submit') { - return Promise.resolve({ - ok: true, - text() { return Promise.resolve(')]}\'\n{}'); }, - }); - } else if (url === '/changes/test~42/revisions/2/rebase') { - return Promise.resolve({ - ok: true, - text() { return Promise.resolve(')]}\'\n{}'); }, - }); - } - - return Promise.reject(new Error('bad url')); - }, - getProjectConfig() { return Promise.resolve({}); }, - }); - - sandbox = sinon.sandbox.create(); - sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); - - element = fixture('basic'); - element.change = {}; - element.changeNum = '42'; - element.latestPatchNum = '2'; - element.actions = { - '/': { - method: 'DELETE', - label: 'Delete Change', - title: 'Delete change X_X', - enabled: true, - }, - }; - sandbox.stub(element.$.confirmCherrypick.$.restAPI, - 'getRepoBranches').returns(Promise.resolve([])); - sandbox.stub(element.$.confirmMove.$.restAPI, - 'getRepoBranches').returns(Promise.resolve([])); - - return element.reload(); + return Promise.reject(new Error('bad url')); + }, + getProjectConfig() { return Promise.resolve({}); }, }); - teardown(() => { - sandbox.restore(); - }); + sandbox = sinon.sandbox.create(); + sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); - test('show-revision-actions event should fire', done => { - const spy = sinon.spy(element, '_sendShowRevisionActions'); - element.reload(); - flush(() => { - assert.isTrue(spy.called); - done(); - }); - }); + element = fixture('basic'); + element.change = {}; + element.changeNum = '42'; + element.latestPatchNum = '2'; + element.actions = { + '/': { + method: 'DELETE', + label: 'Delete Change', + title: 'Delete change X_X', + enabled: true, + }, + }; + sandbox.stub(element.$.confirmCherrypick.$.restAPI, + 'getRepoBranches').returns(Promise.resolve([])); + sandbox.stub(element.$.confirmMove.$.restAPI, + 'getRepoBranches').returns(Promise.resolve([])); - test('primary and secondary actions split properly', () => { - // Submit should be the only primary action. - assert.equal(element._topLevelPrimaryActions.length, 1); - assert.equal(element._topLevelPrimaryActions[0].label, 'Submit'); - assert.equal(element._topLevelSecondaryActions.length, - element._topLevelActions.length - 1); - }); + return element.reload(); + }); - test('revert submission action is skipped', () => { - assert.isFalse(element._allActionValues.includes(action => - action.key === 'revert_submission')); - }); + teardown(() => { + sandbox.restore(); + }); - test('_shouldHideActions', () => { - assert.isTrue(element._shouldHideActions(undefined, true)); - assert.isTrue(element._shouldHideActions({base: {}}, false)); - assert.isFalse(element._shouldHideActions({base: ['test']}, false)); + test('show-revision-actions event should fire', done => { + const spy = sinon.spy(element, '_sendShowRevisionActions'); + element.reload(); + flush(() => { + assert.isTrue(spy.called); + done(); }); + }); - test('plugin revision actions', done => { - sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns( - Promise.resolve('the-url')); - element.revisionActions = { - 'plugin~action': {}, - }; - assert.isOk(element.revisionActions['plugin~action']); - flush(() => { - assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith( - element.changeNum, element.latestPatchNum, '/plugin~action')); - assert.equal(element.revisionActions['plugin~action'].__url, 'the-url'); - done(); - }); + test('primary and secondary actions split properly', () => { + // Submit should be the only primary action. + assert.equal(element._topLevelPrimaryActions.length, 1); + assert.equal(element._topLevelPrimaryActions[0].label, 'Submit'); + assert.equal(element._topLevelSecondaryActions.length, + element._topLevelActions.length - 1); + }); + + test('revert submission action is skipped', () => { + assert.isFalse(element._allActionValues.includes(action => + action.key === 'revert_submission')); + }); + + test('_shouldHideActions', () => { + assert.isTrue(element._shouldHideActions(undefined, true)); + assert.isTrue(element._shouldHideActions({base: {}}, false)); + assert.isFalse(element._shouldHideActions({base: ['test']}, false)); + }); + + test('plugin revision actions', done => { + sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns( + Promise.resolve('the-url')); + element.revisionActions = { + 'plugin~action': {}, + }; + assert.isOk(element.revisionActions['plugin~action']); + flush(() => { + assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith( + element.changeNum, element.latestPatchNum, '/plugin~action')); + assert.equal(element.revisionActions['plugin~action'].__url, 'the-url'); + done(); }); + }); - test('plugin change actions', done => { - sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns( - Promise.resolve('the-url')); - element.actions = { - 'plugin~action': {}, - }; - assert.isOk(element.actions['plugin~action']); - flush(() => { - assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith( - element.changeNum, null, '/plugin~action')); - assert.equal(element.actions['plugin~action'].__url, 'the-url'); - done(); - }); + test('plugin change actions', done => { + sandbox.stub(element.$.restAPI, 'getChangeActionURL').returns( + Promise.resolve('the-url')); + element.actions = { + 'plugin~action': {}, + }; + assert.isOk(element.actions['plugin~action']); + flush(() => { + assert.isTrue(element.$.restAPI.getChangeActionURL.calledWith( + element.changeNum, null, '/plugin~action')); + assert.equal(element.actions['plugin~action'].__url, 'the-url'); + done(); }); + }); - test('not supported actions are filtered out', () => { - element.revisionActions = {followup: {}}; - assert.equal(element.querySelectorAll( - 'section gr-button[data-action-type="revision"]').length, 0); - }); + test('not supported actions are filtered out', () => { + element.revisionActions = {followup: {}}; + assert.equal(element.querySelectorAll( + 'section gr-button[data-action-type="revision"]').length, 0); + }); - test('getActionDetails', () => { - element.revisionActions = Object.assign({ - 'plugin~action': {}, - }, element.revisionActions); - assert.isUndefined(element.getActionDetails('rubbish')); - assert.strictEqual(element.revisionActions['plugin~action'], - element.getActionDetails('plugin~action')); - assert.strictEqual(element.revisionActions['rebase'], - element.getActionDetails('rebase')); - }); + test('getActionDetails', () => { + element.revisionActions = Object.assign({ + 'plugin~action': {}, + }, element.revisionActions); + assert.isUndefined(element.getActionDetails('rubbish')); + assert.strictEqual(element.revisionActions['plugin~action'], + element.getActionDetails('plugin~action')); + assert.strictEqual(element.revisionActions['rebase'], + element.getActionDetails('rebase')); + }); - test('hide revision action', done => { + test('hide revision action', done => { + flush(() => { + const buttonEl = element.shadowRoot + .querySelector('[data-action-key="submit"]'); + assert.isOk(buttonEl); + assert.throws(element.setActionHidden.bind(element, 'invalid type')); + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, true); + assert.lengthOf(element._hiddenActions, 1); + element.setActionHidden(element.ActionType.REVISION, + element.RevisionActions.SUBMIT, true); + assert.lengthOf(element._hiddenActions, 1); flush(() => { const buttonEl = element.shadowRoot .querySelector('[data-action-key="submit"]'); - assert.isOk(buttonEl); - assert.throws(element.setActionHidden.bind(element, 'invalid type')); + assert.isNotOk(buttonEl); + element.setActionHidden(element.ActionType.REVISION, - element.RevisionActions.SUBMIT, true); - assert.lengthOf(element._hiddenActions, 1); - element.setActionHidden(element.ActionType.REVISION, - element.RevisionActions.SUBMIT, true); - assert.lengthOf(element._hiddenActions, 1); + element.RevisionActions.SUBMIT, false); flush(() => { const buttonEl = element.shadowRoot .querySelector('[data-action-key="submit"]'); - assert.isNotOk(buttonEl); - - element.setActionHidden(element.ActionType.REVISION, - element.RevisionActions.SUBMIT, false); - flush(() => { - const buttonEl = element.shadowRoot - .querySelector('[data-action-key="submit"]'); - assert.isOk(buttonEl); - assert.isFalse(buttonEl.hasAttribute('hidden')); - done(); - }); + assert.isOk(buttonEl); + assert.isFalse(buttonEl.hasAttribute('hidden')); + done(); }); }); }); + }); - test('buttons exist', done => { - element._loading = false; - flush(() => { - const buttonEls = Polymer.dom(element.root) - .querySelectorAll('gr-button'); - const menuItems = element.$.moreActions.items; + test('buttons exist', done => { + element._loading = false; + flush(() => { + const buttonEls = dom(element.root) + .querySelectorAll('gr-button'); + const menuItems = element.$.moreActions.items; - // Total button number is one greater than the number of total actions - // due to the existence of the overflow menu trigger. - assert.equal(buttonEls.length + menuItems.length, - element._allActionValues.length + 1); - assert.isFalse(element.hidden); - done(); - }); + // Total button number is one greater than the number of total actions + // due to the existence of the overflow menu trigger. + assert.equal(buttonEls.length + menuItems.length, + element._allActionValues.length + 1); + assert.isFalse(element.hidden); + done(); }); + }); - test('delete buttons have explicit labels', done => { - flush(() => { - const deleteItems = element.$.moreActions.items - .filter(item => item.id.startsWith('delete')); - assert.equal(deleteItems.length, 1); - assert.notEqual(deleteItems[0].name); - assert.equal(deleteItems[0].name, 'Delete change'); - done(); - }); + test('delete buttons have explicit labels', done => { + flush(() => { + const deleteItems = element.$.moreActions.items + .filter(item => item.id.startsWith('delete')); + assert.equal(deleteItems.length, 1); + assert.notEqual(deleteItems[0].name); + assert.equal(deleteItems[0].name, 'Delete change'); + done(); }); + }); - test('get revision object from change', () => { - const revObj = {_number: 2, foo: 'bar'}; - const change = { - revisions: { - rev1: {_number: 1}, - rev2: revObj, - }, - }; - assert.deepEqual(element._getRevision(change, '2'), revObj); - }); + test('get revision object from change', () => { + const revObj = {_number: 2, foo: 'bar'}; + const change = { + revisions: { + rev1: {_number: 1}, + rev2: revObj, + }, + }; + assert.deepEqual(element._getRevision(change, '2'), revObj); + }); - test('_actionComparator sort order', () => { - const actions = [ - {label: '123', __type: 'change', __key: 'review'}, - {label: 'abc-ro', __type: 'revision'}, - {label: 'abc', __type: 'change'}, - {label: 'def', __type: 'change'}, - {label: 'def-p', __type: 'change', __primary: true}, - ]; + test('_actionComparator sort order', () => { + const actions = [ + {label: '123', __type: 'change', __key: 'review'}, + {label: 'abc-ro', __type: 'revision'}, + {label: 'abc', __type: 'change'}, + {label: 'def', __type: 'change'}, + {label: 'def-p', __type: 'change', __primary: true}, + ]; - const result = actions.slice(); - result.reverse(); - result.sort(element._actionComparator.bind(element)); - assert.deepEqual(result, actions); - }); + const result = actions.slice(); + result.reverse(); + result.sort(element._actionComparator.bind(element)); + assert.deepEqual(result, actions); + }); - test('submit change', () => { - const showSpy = sandbox.spy(element, '_showActionDialog'); - sandbox.stub(element.$.restAPI, 'getFromProjectLookup') - .returns(Promise.resolve('test')); - sandbox.stub(element, 'fetchChangeUpdates', - () => Promise.resolve({isLatest: true})); - sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve()); - element.change = { - revisions: { - rev1: {_number: 1}, - rev2: {_number: 2}, - }, - }; - element.latestPatchNum = '2'; + test('submit change', () => { + const showSpy = sandbox.spy(element, '_showActionDialog'); + sandbox.stub(element.$.restAPI, 'getFromProjectLookup') + .returns(Promise.resolve('test')); + sandbox.stub(element, 'fetchChangeUpdates', + () => Promise.resolve({isLatest: true})); + sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve()); + element.change = { + revisions: { + rev1: {_number: 1}, + rev2: {_number: 2}, + }, + }; + element.latestPatchNum = '2'; + const submitButton = element.shadowRoot + .querySelector('gr-button[data-action-key="submit"]'); + assert.ok(submitButton); + MockInteractions.tap(submitButton); + + flushAsynchronousOperations(); + assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog)); + }); + + test('submit change, tap on icon', done => { + sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done); + sandbox.stub(element.$.restAPI, 'getFromProjectLookup') + .returns(Promise.resolve('test')); + sandbox.stub(element, 'fetchChangeUpdates', + () => Promise.resolve({isLatest: true})); + sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve()); + element.change = { + revisions: { + rev1: {_number: 1}, + rev2: {_number: 2}, + }, + }; + element.latestPatchNum = '2'; + + const submitIcon = + element.shadowRoot + .querySelector('gr-button[data-action-key="submit"] iron-icon'); + assert.ok(submitIcon); + MockInteractions.tap(submitIcon); + }); + + test('_handleSubmitConfirm', () => { + const fireStub = sandbox.stub(element, '_fireAction'); + sandbox.stub(element, '_canSubmitChange').returns(true); + element._handleSubmitConfirm(); + assert.isTrue(fireStub.calledOnce); + assert.deepEqual(fireStub.lastCall.args, + ['/submit', element.revisionActions.submit, true]); + }); + + test('_handleSubmitConfirm when not able to submit', () => { + const fireStub = sandbox.stub(element, '_fireAction'); + sandbox.stub(element, '_canSubmitChange').returns(false); + element._handleSubmitConfirm(); + assert.isFalse(fireStub.called); + }); + + test('submit change with plugin hook', done => { + sandbox.stub(element, '_canSubmitChange', + () => false); + const fireActionStub = sandbox.stub(element, '_fireAction'); + flush(() => { const submitButton = element.shadowRoot .querySelector('gr-button[data-action-key="submit"]'); assert.ok(submitButton); MockInteractions.tap(submitButton); + assert.equal(fireActionStub.callCount, 0); - flushAsynchronousOperations(); - assert.isTrue(showSpy.calledWith(element.$.confirmSubmitDialog)); + done(); }); + }); - test('submit change, tap on icon', done => { - sandbox.stub(element.$.confirmSubmitDialog, 'resetFocus', done); - sandbox.stub(element.$.restAPI, 'getFromProjectLookup') - .returns(Promise.resolve('test')); - sandbox.stub(element, 'fetchChangeUpdates', - () => Promise.resolve({isLatest: true})); - sandbox.stub(element.$.overlay, 'open').returns(Promise.resolve()); - element.change = { - revisions: { - rev1: {_number: 1}, - rev2: {_number: 2}, - }, - }; - element.latestPatchNum = '2'; + test('chain state', () => { + assert.equal(element._hasKnownChainState, false); + element.hasParent = true; + assert.equal(element._hasKnownChainState, true); + element.hasParent = false; + }); - const submitIcon = - element.shadowRoot - .querySelector('gr-button[data-action-key="submit"] iron-icon'); - assert.ok(submitIcon); - MockInteractions.tap(submitIcon); - }); + test('_calculateDisabled', () => { + let hasKnownChainState = false; + const action = {__key: 'rebase', enabled: true}; + assert.equal( + element._calculateDisabled(action, hasKnownChainState), true); - test('_handleSubmitConfirm', () => { - const fireStub = sandbox.stub(element, '_fireAction'); - sandbox.stub(element, '_canSubmitChange').returns(true); - element._handleSubmitConfirm(); - assert.isTrue(fireStub.calledOnce); - assert.deepEqual(fireStub.lastCall.args, - ['/submit', element.revisionActions.submit, true]); - }); + action.__key = 'delete'; + assert.equal( + element._calculateDisabled(action, hasKnownChainState), false); - test('_handleSubmitConfirm when not able to submit', () => { - const fireStub = sandbox.stub(element, '_fireAction'); - sandbox.stub(element, '_canSubmitChange').returns(false); - element._handleSubmitConfirm(); - assert.isFalse(fireStub.called); - }); + action.__key = 'rebase'; + hasKnownChainState = true; + assert.equal( + element._calculateDisabled(action, hasKnownChainState), false); - test('submit change with plugin hook', done => { - sandbox.stub(element, '_canSubmitChange', - () => false); - const fireActionStub = sandbox.stub(element, '_fireAction'); - flush(() => { - const submitButton = element.shadowRoot - .querySelector('gr-button[data-action-key="submit"]'); - assert.ok(submitButton); - MockInteractions.tap(submitButton); - assert.equal(fireActionStub.callCount, 0); + action.enabled = false; + assert.equal( + element._calculateDisabled(action, hasKnownChainState), true); + }); - done(); - }); - }); - - test('chain state', () => { - assert.equal(element._hasKnownChainState, false); - element.hasParent = true; - assert.equal(element._hasKnownChainState, true); - element.hasParent = false; - }); - - test('_calculateDisabled', () => { - let hasKnownChainState = false; - const action = {__key: 'rebase', enabled: true}; - assert.equal( - element._calculateDisabled(action, hasKnownChainState), true); - - action.__key = 'delete'; - assert.equal( - element._calculateDisabled(action, hasKnownChainState), false); - - action.__key = 'rebase'; - hasKnownChainState = true; - assert.equal( - element._calculateDisabled(action, hasKnownChainState), false); - - action.enabled = false; - assert.equal( - element._calculateDisabled(action, hasKnownChainState), true); - }); - - test('rebase change', done => { - const fireActionStub = sandbox.stub(element, '_fireAction'); - const fetchChangesStub = sandbox.stub(element.$.confirmRebase, - 'fetchRecentChanges').returns(Promise.resolve([])); - element._hasKnownChainState = true; - flush(() => { - const rebaseButton = element.shadowRoot - .querySelector('gr-button[data-action-key="rebase"]'); - MockInteractions.tap(rebaseButton); - const rebaseAction = { - __key: 'rebase', - __type: 'revision', - __primary: false, - enabled: true, - label: 'Rebase', - method: 'POST', - title: 'Rebase onto tip of branch or parent change', - }; - assert.isTrue(fetchChangesStub.called); - element._handleRebaseConfirm({detail: {base: '1234'}}); - rebaseAction.rebaseOnCurrent = true; - assert.deepEqual(fireActionStub.lastCall.args, - ['/rebase', rebaseAction, true, {base: '1234'}]); - done(); - }); - }); - - test(`rebase dialog gets recent changes each time it's opened`, done => { - const fetchChangesStub = sandbox.stub(element.$.confirmRebase, - 'fetchRecentChanges').returns(Promise.resolve([])); - element._hasKnownChainState = true; + test('rebase change', done => { + const fireActionStub = sandbox.stub(element, '_fireAction'); + const fetchChangesStub = sandbox.stub(element.$.confirmRebase, + 'fetchRecentChanges').returns(Promise.resolve([])); + element._hasKnownChainState = true; + flush(() => { const rebaseButton = element.shadowRoot .querySelector('gr-button[data-action-key="rebase"]'); MockInteractions.tap(rebaseButton); - assert.isTrue(fetchChangesStub.calledOnce); + const rebaseAction = { + __key: 'rebase', + __type: 'revision', + __primary: false, + enabled: true, + label: 'Rebase', + method: 'POST', + title: 'Rebase onto tip of branch or parent change', + }; + assert.isTrue(fetchChangesStub.called); + element._handleRebaseConfirm({detail: {base: '1234'}}); + rebaseAction.rebaseOnCurrent = true; + assert.deepEqual(fireActionStub.lastCall.args, + ['/rebase', rebaseAction, true, {base: '1234'}]); + done(); + }); + }); + test(`rebase dialog gets recent changes each time it's opened`, done => { + const fetchChangesStub = sandbox.stub(element.$.confirmRebase, + 'fetchRecentChanges').returns(Promise.resolve([])); + element._hasKnownChainState = true; + const rebaseButton = element.shadowRoot + .querySelector('gr-button[data-action-key="rebase"]'); + MockInteractions.tap(rebaseButton); + assert.isTrue(fetchChangesStub.calledOnce); + + flush(() => { + element.$.confirmRebase.fire('cancel'); + MockInteractions.tap(rebaseButton); + assert.isTrue(fetchChangesStub.calledTwice); + done(); + }); + }); + + test('two dialogs are not shown at the same time', done => { + element._hasKnownChainState = true; + flush(() => { + const rebaseButton = element.shadowRoot + .querySelector('gr-button[data-action-key="rebase"]'); + assert.ok(rebaseButton); + MockInteractions.tap(rebaseButton); + flushAsynchronousOperations(); + assert.isFalse(element.$.confirmRebase.hidden); + + element._handleCherrypickTap(); + flushAsynchronousOperations(); + assert.isTrue(element.$.confirmRebase.hidden); + assert.isFalse(element.$.confirmCherrypick.hidden); + done(); + }); + }); + + test('fullscreen-overlay-opened hides content', () => { + sandbox.spy(element, '_handleHideBackgroundContent'); + element.$.overlay.fire('fullscreen-overlay-opened'); + assert.isTrue(element._handleHideBackgroundContent.called); + assert.isTrue(element.$.mainContent.classList.contains('overlayOpen')); + }); + + test('fullscreen-overlay-closed shows content', () => { + sandbox.spy(element, '_handleShowBackgroundContent'); + element.$.overlay.fire('fullscreen-overlay-closed'); + assert.isTrue(element._handleShowBackgroundContent.called); + assert.isFalse(element.$.mainContent.classList.contains('overlayOpen')); + }); + + test('_setLabelValuesOnRevert', () => { + const labels = {'Foo': 1, 'Bar-Baz': -2}; + const changeId = 1234; + sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels); + const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview') + .returns(Promise.resolve()); + return element._setLabelValuesOnRevert(changeId).then(() => { + assert.isTrue(saveStub.calledOnce); + assert.equal(saveStub.lastCall.args[0], changeId); + assert.deepEqual(saveStub.lastCall.args[2], {labels}); + }); + }); + + suite('change edits', () => { + test('disableEdit', () => { + element.set('editMode', false); + element.set('editPatchsetLoaded', false); + element.change = {status: 'NEW'}; + element.set('disableEdit', true); + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="publishEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="rebaseEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="deleteEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="stopEdit"]')); + }); + + test('shows confirm dialog for delete edit', () => { + element.set('editMode', true); + element.set('editPatchsetLoaded', true); + + const fireActionStub = sandbox.stub(element, '_fireAction'); + element._handleDeleteEditTap(); + assert.isFalse(element.$.confirmDeleteEditDialog.hidden); + MockInteractions.tap( + element.shadowRoot + .querySelector('#confirmDeleteEditDialog') + .shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.equal(fireActionStub.lastCall.args[0], '/edit'); + }); + + test('hide publishEdit and rebaseEdit if change is not open', () => { + element.set('editMode', true); + element.set('editPatchsetLoaded', true); + element.change = {status: 'MERGED'}; + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="publishEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="rebaseEdit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="deleteEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + }); + + test('edit patchset is loaded, needs rebase', () => { + element.set('editMode', true); + element.set('editPatchsetLoaded', true); + element.change = {status: 'NEW'}; + element.editBasedOnCurrentPatchSet = false; + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="publishEdit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="rebaseEdit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="deleteEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="stopEdit"]')); + }); + + test('edit patchset is loaded, does not need rebase', () => { + element.set('editMode', true); + element.set('editPatchsetLoaded', true); + element.change = {status: 'NEW'}; + element.editBasedOnCurrentPatchSet = true; + flushAsynchronousOperations(); + + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="publishEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="rebaseEdit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="deleteEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="stopEdit"]')); + }); + + test('edit mode is loaded, no edit patchset', () => { + element.set('editMode', true); + element.set('editPatchsetLoaded', false); + element.change = {status: 'NEW'}; + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="publishEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="rebaseEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="deleteEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="stopEdit"]')); + }); + + test('normal patch set', () => { + element.set('editMode', false); + element.set('editPatchsetLoaded', false); + element.change = {status: 'NEW'}; + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="publishEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="rebaseEdit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="deleteEdit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="stopEdit"]')); + }); + + test('edit action', done => { + element.addEventListener('edit-tap', () => { done(); }); + element.set('editMode', true); + element.change = {status: 'NEW'}; + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + assert.isOk(element.shadowRoot + .querySelector('gr-button[data-action-key="stopEdit"]')); + element.change = {status: 'MERGED'}; + flushAsynchronousOperations(); + + assert.isNotOk(element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]')); + element.change = {status: 'NEW'}; + element.set('editMode', false); + flushAsynchronousOperations(); + + const editButton = element.shadowRoot + .querySelector('gr-button[data-action-key="edit"]'); + assert.isOk(editButton); + MockInteractions.tap(editButton); + }); + }); + + suite('cherry-pick', () => { + let fireActionStub; + + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + sandbox.stub(window, 'alert'); + }); + + test('works', () => { + element._handleCherrypickTap(); + const action = { + __key: 'cherrypick', + __type: 'revision', + __primary: false, + enabled: true, + label: 'Cherry pick', + method: 'POST', + title: 'Cherry pick change to a different branch', + }; + + element._handleCherrypickConfirm(); + assert.equal(fireActionStub.callCount, 0); + + element.$.confirmCherrypick.branch = 'master'; + element._handleCherrypickConfirm(); + assert.equal(fireActionStub.callCount, 0); // Still needs a message. + + // Add attributes that are used to determine the message. + element.$.confirmCherrypick.commitMessage = 'foo message'; + element.$.confirmCherrypick.changeStatus = 'OPEN'; + element.$.confirmCherrypick.commitNum = '123'; + + element._handleCherrypickConfirm(); + + assert.equal(element.$.confirmCherrypick.$.messageInput.value, + 'foo message'); + + assert.deepEqual(fireActionStub.lastCall.args, [ + '/cherrypick', action, true, { + destination: 'master', + base: null, + message: 'foo message', + allow_conflicts: false, + }, + ]); + }); + + test('cherry pick even with conflicts', () => { + element._handleCherrypickTap(); + const action = { + __key: 'cherrypick', + __type: 'revision', + __primary: false, + enabled: true, + label: 'Cherry pick', + method: 'POST', + title: 'Cherry pick change to a different branch', + }; + + element.$.confirmCherrypick.branch = 'master'; + + // Add attributes that are used to determine the message. + element.$.confirmCherrypick.commitMessage = 'foo message'; + element.$.confirmCherrypick.changeStatus = 'OPEN'; + element.$.confirmCherrypick.commitNum = '123'; + + element._handleCherrypickConflictConfirm(); + + assert.deepEqual(fireActionStub.lastCall.args, [ + '/cherrypick', action, true, { + destination: 'master', + base: null, + message: 'foo message', + allow_conflicts: true, + }, + ]); + }); + + test('branch name cleared when re-open cherrypick', () => { + const emptyBranchName = ''; + element.$.confirmCherrypick.branch = 'master'; + + element._handleCherrypickTap(); + assert.equal(element.$.confirmCherrypick.branch, emptyBranchName); + }); + }); + + suite('move change', () => { + let fireActionStub; + + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + sandbox.stub(window, 'alert'); + }); + + test('works', () => { + element._handleMoveTap(); + + element._handleMoveConfirm(); + assert.equal(fireActionStub.callCount, 0); + + element.$.confirmMove.branch = 'master'; + element._handleMoveConfirm(); + assert.equal(fireActionStub.callCount, 1); + }); + + test('branch name cleared when re-open move', () => { + const emptyBranchName = ''; + element.$.confirmMove.branch = 'master'; + + element._handleMoveTap(); + assert.equal(element.$.confirmMove.branch, emptyBranchName); + }); + }); + + test('custom actions', done => { + // Add a button with the same key as a server-based one to ensure + // collisions are taken care of. + const key = element.addActionButton(element.ActionType.REVISION, 'Bork!'); + element.addEventListener(key + '-tap', e => { + assert.equal(e.detail.node.getAttribute('data-action-key'), key); + element.removeActionButton(key); flush(() => { - element.$.confirmRebase.fire('cancel'); - MockInteractions.tap(rebaseButton); - assert.isTrue(fetchChangesStub.calledTwice); - done(); - }); - }); - - test('two dialogs are not shown at the same time', done => { - element._hasKnownChainState = true; - flush(() => { - const rebaseButton = element.shadowRoot - .querySelector('gr-button[data-action-key="rebase"]'); - assert.ok(rebaseButton); - MockInteractions.tap(rebaseButton); - flushAsynchronousOperations(); - assert.isFalse(element.$.confirmRebase.hidden); - - element._handleCherrypickTap(); - flushAsynchronousOperations(); - assert.isTrue(element.$.confirmRebase.hidden); - assert.isFalse(element.$.confirmCherrypick.hidden); - done(); - }); - }); - - test('fullscreen-overlay-opened hides content', () => { - sandbox.spy(element, '_handleHideBackgroundContent'); - element.$.overlay.fire('fullscreen-overlay-opened'); - assert.isTrue(element._handleHideBackgroundContent.called); - assert.isTrue(element.$.mainContent.classList.contains('overlayOpen')); - }); - - test('fullscreen-overlay-closed shows content', () => { - sandbox.spy(element, '_handleShowBackgroundContent'); - element.$.overlay.fire('fullscreen-overlay-closed'); - assert.isTrue(element._handleShowBackgroundContent.called); - assert.isFalse(element.$.mainContent.classList.contains('overlayOpen')); - }); - - test('_setLabelValuesOnRevert', () => { - const labels = {'Foo': 1, 'Bar-Baz': -2}; - const changeId = 1234; - sandbox.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels); - const saveStub = sandbox.stub(element.$.restAPI, 'saveChangeReview') - .returns(Promise.resolve()); - return element._setLabelValuesOnRevert(changeId).then(() => { - assert.isTrue(saveStub.calledOnce); - assert.equal(saveStub.lastCall.args[0], changeId); - assert.deepEqual(saveStub.lastCall.args[2], {labels}); - }); - }); - - suite('change edits', () => { - test('disableEdit', () => { - element.set('editMode', false); - element.set('editPatchsetLoaded', false); - element.change = {status: 'NEW'}; - element.set('disableEdit', true); - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="publishEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="rebaseEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="deleteEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="stopEdit"]')); - }); - - test('shows confirm dialog for delete edit', () => { - element.set('editMode', true); - element.set('editPatchsetLoaded', true); - - const fireActionStub = sandbox.stub(element, '_fireAction'); - element._handleDeleteEditTap(); - assert.isFalse(element.$.confirmDeleteEditDialog.hidden); - MockInteractions.tap( - element.shadowRoot - .querySelector('#confirmDeleteEditDialog') - .shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.equal(fireActionStub.lastCall.args[0], '/edit'); - }); - - test('hide publishEdit and rebaseEdit if change is not open', () => { - element.set('editMode', true); - element.set('editPatchsetLoaded', true); - element.change = {status: 'MERGED'}; - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="publishEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="rebaseEdit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="deleteEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - }); - - test('edit patchset is loaded, needs rebase', () => { - element.set('editMode', true); - element.set('editPatchsetLoaded', true); - element.change = {status: 'NEW'}; - element.editBasedOnCurrentPatchSet = false; - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="publishEdit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="rebaseEdit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="deleteEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="stopEdit"]')); - }); - - test('edit patchset is loaded, does not need rebase', () => { - element.set('editMode', true); - element.set('editPatchsetLoaded', true); - element.change = {status: 'NEW'}; - element.editBasedOnCurrentPatchSet = true; - flushAsynchronousOperations(); - - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="publishEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="rebaseEdit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="deleteEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="stopEdit"]')); - }); - - test('edit mode is loaded, no edit patchset', () => { - element.set('editMode', true); - element.set('editPatchsetLoaded', false); - element.change = {status: 'NEW'}; - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="publishEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="rebaseEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="deleteEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="stopEdit"]')); - }); - - test('normal patch set', () => { - element.set('editMode', false); - element.set('editPatchsetLoaded', false); - element.change = {status: 'NEW'}; - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="publishEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="rebaseEdit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="deleteEdit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="stopEdit"]')); - }); - - test('edit action', done => { - element.addEventListener('edit-tap', () => { done(); }); - element.set('editMode', true); - element.change = {status: 'NEW'}; - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - assert.isOk(element.shadowRoot - .querySelector('gr-button[data-action-key="stopEdit"]')); - element.change = {status: 'MERGED'}; - flushAsynchronousOperations(); - - assert.isNotOk(element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]')); - element.change = {status: 'NEW'}; - element.set('editMode', false); - flushAsynchronousOperations(); - - const editButton = element.shadowRoot - .querySelector('gr-button[data-action-key="edit"]'); - assert.isOk(editButton); - MockInteractions.tap(editButton); - }); - }); - - suite('cherry-pick', () => { - let fireActionStub; - - setup(() => { - fireActionStub = sandbox.stub(element, '_fireAction'); - sandbox.stub(window, 'alert'); - }); - - test('works', () => { - element._handleCherrypickTap(); - const action = { - __key: 'cherrypick', - __type: 'revision', - __primary: false, - enabled: true, - label: 'Cherry pick', - method: 'POST', - title: 'Cherry pick change to a different branch', - }; - - element._handleCherrypickConfirm(); - assert.equal(fireActionStub.callCount, 0); - - element.$.confirmCherrypick.branch = 'master'; - element._handleCherrypickConfirm(); - assert.equal(fireActionStub.callCount, 0); // Still needs a message. - - // Add attributes that are used to determine the message. - element.$.confirmCherrypick.commitMessage = 'foo message'; - element.$.confirmCherrypick.changeStatus = 'OPEN'; - element.$.confirmCherrypick.commitNum = '123'; - - element._handleCherrypickConfirm(); - - assert.equal(element.$.confirmCherrypick.$.messageInput.value, - 'foo message'); - - assert.deepEqual(fireActionStub.lastCall.args, [ - '/cherrypick', action, true, { - destination: 'master', - base: null, - message: 'foo message', - allow_conflicts: false, - }, - ]); - }); - - test('cherry pick even with conflicts', () => { - element._handleCherrypickTap(); - const action = { - __key: 'cherrypick', - __type: 'revision', - __primary: false, - enabled: true, - label: 'Cherry pick', - method: 'POST', - title: 'Cherry pick change to a different branch', - }; - - element.$.confirmCherrypick.branch = 'master'; - - // Add attributes that are used to determine the message. - element.$.confirmCherrypick.commitMessage = 'foo message'; - element.$.confirmCherrypick.changeStatus = 'OPEN'; - element.$.confirmCherrypick.commitNum = '123'; - - element._handleCherrypickConflictConfirm(); - - assert.deepEqual(fireActionStub.lastCall.args, [ - '/cherrypick', action, true, { - destination: 'master', - base: null, - message: 'foo message', - allow_conflicts: true, - }, - ]); - }); - - test('branch name cleared when re-open cherrypick', () => { - const emptyBranchName = ''; - element.$.confirmCherrypick.branch = 'master'; - - element._handleCherrypickTap(); - assert.equal(element.$.confirmCherrypick.branch, emptyBranchName); - }); - }); - - suite('move change', () => { - let fireActionStub; - - setup(() => { - fireActionStub = sandbox.stub(element, '_fireAction'); - sandbox.stub(window, 'alert'); - }); - - test('works', () => { - element._handleMoveTap(); - - element._handleMoveConfirm(); - assert.equal(fireActionStub.callCount, 0); - - element.$.confirmMove.branch = 'master'; - element._handleMoveConfirm(); - assert.equal(fireActionStub.callCount, 1); - }); - - test('branch name cleared when re-open move', () => { - const emptyBranchName = ''; - element.$.confirmMove.branch = 'master'; - - element._handleMoveTap(); - assert.equal(element.$.confirmMove.branch, emptyBranchName); - }); - }); - - test('custom actions', done => { - // Add a button with the same key as a server-based one to ensure - // collisions are taken care of. - const key = element.addActionButton(element.ActionType.REVISION, 'Bork!'); - element.addEventListener(key + '-tap', e => { - assert.equal(e.detail.node.getAttribute('data-action-key'), key); - element.removeActionButton(key); - flush(() => { - assert.notOk(element.shadowRoot - .querySelector('[data-action-key="' + key + '"]')); - done(); - }); - }); - flush(() => { - MockInteractions.tap(element.shadowRoot + assert.notOk(element.shadowRoot .querySelector('[data-action-key="' + key + '"]')); + done(); }); }); + flush(() => { + MockInteractions.tap(element.shadowRoot + .querySelector('[data-action-key="' + key + '"]')); + }); + }); - test('_setLoadingOnButtonWithKey top-level', () => { - const key = 'rebase'; - const type = 'revision'; - const cleanup = element._setLoadingOnButtonWithKey(type, key); - assert.equal(element._actionLoadingMessage, 'Rebasing...'); + test('_setLoadingOnButtonWithKey top-level', () => { + const key = 'rebase'; + const type = 'revision'; + const cleanup = element._setLoadingOnButtonWithKey(type, key); + assert.equal(element._actionLoadingMessage, 'Rebasing...'); - const button = element.shadowRoot - .querySelector('[data-action-key="' + key + '"]'); - assert.isTrue(button.hasAttribute('loading')); - assert.isTrue(button.disabled); + const button = element.shadowRoot + .querySelector('[data-action-key="' + key + '"]'); + assert.isTrue(button.hasAttribute('loading')); + assert.isTrue(button.disabled); - assert.isOk(cleanup); - assert.isFunction(cleanup); - cleanup(); + assert.isOk(cleanup); + assert.isFunction(cleanup); + cleanup(); - assert.isFalse(button.hasAttribute('loading')); - assert.isFalse(button.disabled); - assert.isNotOk(element._actionLoadingMessage); + assert.isFalse(button.hasAttribute('loading')); + assert.isFalse(button.disabled); + assert.isNotOk(element._actionLoadingMessage); + }); + + test('_setLoadingOnButtonWithKey overflow menu', () => { + const key = 'cherrypick'; + const type = 'revision'; + const cleanup = element._setLoadingOnButtonWithKey(type, key); + assert.equal(element._actionLoadingMessage, 'Cherry-picking...'); + assert.include(element._disabledMenuActions, 'cherrypick'); + assert.isFunction(cleanup); + + cleanup(); + + assert.notOk(element._actionLoadingMessage); + assert.notInclude(element._disabledMenuActions, 'cherrypick'); + }); + + suite('abandon change', () => { + let alertStub; + let fireActionStub; + + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + alertStub = sandbox.stub(window, 'alert'); + element.actions = { + abandon: { + method: 'POST', + label: 'Abandon', + title: 'Abandon the change', + enabled: true, + }, + }; + return element.reload(); }); - test('_setLoadingOnButtonWithKey overflow menu', () => { - const key = 'cherrypick'; - const type = 'revision'; - const cleanup = element._setLoadingOnButtonWithKey(type, key); - assert.equal(element._actionLoadingMessage, 'Cherry-picking...'); - assert.include(element._disabledMenuActions, 'cherrypick'); - assert.isFunction(cleanup); - - cleanup(); - - assert.notOk(element._actionLoadingMessage); - assert.notInclude(element._disabledMenuActions, 'cherrypick'); - }); - - suite('abandon change', () => { - let alertStub; - let fireActionStub; - - setup(() => { - fireActionStub = sandbox.stub(element, '_fireAction'); - alertStub = sandbox.stub(window, 'alert'); - element.actions = { - abandon: { - method: 'POST', - label: 'Abandon', - title: 'Abandon the change', - enabled: true, - }, - }; - return element.reload(); - }); - - test('abandon change with message', done => { - const newAbandonMsg = 'Test Abandon Message'; - element.$.confirmAbandonDialog.message = newAbandonMsg; - flush(() => { - const abandonButton = - element.shadowRoot - .querySelector('gr-button[data-action-key="abandon"]'); - MockInteractions.tap(abandonButton); - - assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg); - done(); - }); - }); - - test('abandon change with no message', done => { - flush(() => { - const abandonButton = - element.shadowRoot - .querySelector('gr-button[data-action-key="abandon"]'); - MockInteractions.tap(abandonButton); - - assert.isUndefined(element.$.confirmAbandonDialog.message); - done(); - }); - }); - - test('works', () => { - element.$.confirmAbandonDialog.message = 'original message'; - const restoreButton = + test('abandon change with message', done => { + const newAbandonMsg = 'Test Abandon Message'; + element.$.confirmAbandonDialog.message = newAbandonMsg; + flush(() => { + const abandonButton = element.shadowRoot .querySelector('gr-button[data-action-key="abandon"]'); - MockInteractions.tap(restoreButton); + MockInteractions.tap(abandonButton); - element.$.confirmAbandonDialog.message = 'foo message'; - element._handleAbandonDialogConfirm(); - assert.notOk(alertStub.called); - - const action = { - __key: 'abandon', - __type: 'change', - __primary: false, - enabled: true, - label: 'Abandon', - method: 'POST', - title: 'Abandon the change', - }; - assert.deepEqual(fireActionStub.lastCall.args, [ - '/abandon', action, false, { - message: 'foo message', - }]); + assert.equal(element.$.confirmAbandonDialog.message, newAbandonMsg); + done(); }); }); - suite('revert change', () => { - let fireActionStub; + test('abandon change with no message', done => { + flush(() => { + const abandonButton = + element.shadowRoot + .querySelector('gr-button[data-action-key="abandon"]'); + MockInteractions.tap(abandonButton); - setup(() => { - fireActionStub = sandbox.stub(element, '_fireAction'); - element.commitMessage = 'random commit message'; - element.change.current_revision = 'abcdef'; - element.actions = { - revert: { - method: 'POST', - label: 'Revert', - title: 'Revert the change', - enabled: true, - }, - }; - return element.reload(); + assert.isUndefined(element.$.confirmAbandonDialog.message); + done(); }); + }); - test('revert change with plugin hook', done => { - const newRevertMsg = 'Modified revert msg'; - sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg', - () => newRevertMsg); + test('works', () => { + element.$.confirmAbandonDialog.message = 'original message'; + const restoreButton = + element.shadowRoot + .querySelector('gr-button[data-action-key="abandon"]'); + MockInteractions.tap(restoreButton); + + element.$.confirmAbandonDialog.message = 'foo message'; + element._handleAbandonDialogConfirm(); + assert.notOk(alertStub.called); + + const action = { + __key: 'abandon', + __type: 'change', + __primary: false, + enabled: true, + label: 'Abandon', + method: 'POST', + title: 'Abandon the change', + }; + assert.deepEqual(fireActionStub.lastCall.args, [ + '/abandon', action, false, { + message: 'foo message', + }]); + }); + }); + + suite('revert change', () => { + let fireActionStub; + + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + element.commitMessage = 'random commit message'; + element.change.current_revision = 'abcdef'; + element.actions = { + revert: { + method: 'POST', + label: 'Revert', + title: 'Revert the change', + enabled: true, + }, + }; + return element.reload(); + }); + + test('revert change with plugin hook', done => { + const newRevertMsg = 'Modified revert msg'; + sandbox.stub(element.$.confirmRevertDialog, '_modifyRevertMsg', + () => newRevertMsg); + element.change = { + current_revision: 'abc1234', + }; + sandbox.stub(element.$.restAPI, 'getChanges') + .returns(Promise.resolve([ + {change_id: '12345678901234', topic: 'T', subject: 'random'}, + {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)}, + ])); + sandbox.stub(element.$.confirmRevertDialog, + '_populateRevertSubmissionMessage', () => 'original msg'); + flush(() => { + const revertButton = element.shadowRoot + .querySelector('gr-button[data-action-key="revert"]'); + MockInteractions.tap(revertButton); + flush(() => { + assert.equal(element.$.confirmRevertDialog._message, newRevertMsg); + done(); + }); + }); + }); + + suite('revert change submitted together', () => { + setup(() => { element.change = { - current_revision: 'abc1234', + submission_id: '199', + current_revision: '2000', }; sandbox.stub(element.$.restAPI, 'getChanges') .returns(Promise.resolve([ {change_id: '12345678901234', topic: 'T', subject: 'random'}, {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)}, ])); - sandbox.stub(element.$.confirmRevertDialog, - '_populateRevertSubmissionMessage', () => 'original msg'); + }); + + test('confirm revert dialog shows both options', done => { + const revertButton = element.shadowRoot + .querySelector('gr-button[data-action-key="revert"]'); + MockInteractions.tap(revertButton); flush(() => { - const revertButton = element.shadowRoot - .querySelector('gr-button[data-action-key="revert"]'); - MockInteractions.tap(revertButton); + const confirmRevertDialog = element.$.confirmRevertDialog; + const revertSingleChangeLabel = confirmRevertDialog + .shadowRoot.querySelector('.revertSingleChange'); + const revertSubmissionLabel = confirmRevertDialog. + shadowRoot.querySelector('.revertSubmission'); + assert(revertSingleChangeLabel.innerText.trim() === + 'Revert single change'); + assert(revertSubmissionLabel.innerText.trim() === + 'Revert entire submission (2 Changes)'); + let expectedMsg = 'Revert submission 199' + '\n\n' + + 'Reason for revert: <INSERT REASONING HERE>' + '\n' + + 'Reverted Changes:' + '\n' + + '1234567890:random' + '\n' + + '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + + '\n'; + assert.equal(confirmRevertDialog._message, expectedMsg); + const radioInputs = confirmRevertDialog.shadowRoot + .querySelectorAll('input[name="revertOptions"]'); + MockInteractions.tap(radioInputs[0]); flush(() => { - assert.equal(element.$.confirmRevertDialog._message, newRevertMsg); + expectedMsg = 'Revert "random commit message"\n\nThis reverts ' + + 'commit 2000.\n\nReason' + + ' for revert: <INSERT REASONING HERE>\n'; + assert.equal(confirmRevertDialog._message, expectedMsg); done(); }); }); }); - suite('revert change submitted together', () => { + test('submit fails if message is not edited', done => { + const revertButton = element.shadowRoot + .querySelector('gr-button[data-action-key="revert"]'); + const confirmRevertDialog = element.$.confirmRevertDialog; + MockInteractions.tap(revertButton); + const fireStub = sandbox.stub(confirmRevertDialog, 'fire'); + flush(() => { + const confirmButton = element.$.confirmRevertDialog.shadowRoot + .querySelector('gr-dialog') + .shadowRoot.querySelector('#confirm'); + MockInteractions.tap(confirmButton); + flush(() => { + assert.isTrue(confirmRevertDialog._showErrorMessage); + assert.isFalse(fireStub.called); + done(); + }); + }); + }); + + test('message modification is retained on switching', done => { + const revertButton = element.shadowRoot + .querySelector('gr-button[data-action-key="revert"]'); + const confirmRevertDialog = element.$.confirmRevertDialog; + MockInteractions.tap(revertButton); + flush(() => { + const radioInputs = confirmRevertDialog.shadowRoot + .querySelectorAll('input[name="revertOptions"]'); + const revertSubmissionMsg = 'Revert submission 199' + '\n\n' + + 'Reason for revert: <INSERT REASONING HERE>' + '\n' + + 'Reverted Changes:' + '\n' + + '1234567890:random' + '\n' + + '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + + '\n'; + const singleChangeMsg = + 'Revert "random commit message"\n\nThis reverts ' + + 'commit 2000.\n\nReason' + + ' for revert: <INSERT REASONING HERE>\n'; + assert.equal(confirmRevertDialog._message, revertSubmissionMsg); + const newRevertMsg = revertSubmissionMsg + 'random'; + const newSingleChangeMsg = singleChangeMsg + 'random'; + confirmRevertDialog._message = newRevertMsg; + MockInteractions.tap(radioInputs[0]); + flush(() => { + assert.equal(confirmRevertDialog._message, singleChangeMsg); + confirmRevertDialog._message = newSingleChangeMsg; + MockInteractions.tap(radioInputs[1]); + flush(() => { + assert.equal(confirmRevertDialog._message, newRevertMsg); + MockInteractions.tap(radioInputs[0]); + flush(() => { + assert.equal( + confirmRevertDialog._message, + newSingleChangeMsg + ); + done(); + }); + }); + }); + }); + }); + }); + + suite('revert single change', () => { + setup(() => { + element.change = { + submission_id: '199', + current_revision: '2000', + }; + sandbox.stub(element.$.restAPI, 'getChanges') + .returns(Promise.resolve([ + {change_id: '12345678901234', topic: 'T', subject: 'random'}, + ])); + }); + + test('submit fails if message is not edited', done => { + const revertButton = element.shadowRoot + .querySelector('gr-button[data-action-key="revert"]'); + const confirmRevertDialog = element.$.confirmRevertDialog; + MockInteractions.tap(revertButton); + const fireStub = sandbox.stub(confirmRevertDialog, 'fire'); + flush(() => { + const confirmButton = element.$.confirmRevertDialog.shadowRoot + .querySelector('gr-dialog') + .shadowRoot.querySelector('#confirm'); + MockInteractions.tap(confirmButton); + flush(() => { + assert.isTrue(confirmRevertDialog._showErrorMessage); + assert.isFalse(fireStub.called); + done(); + }); + }); + }); + + test('confirm revert dialog shows no radio button', done => { + const revertButton = element.shadowRoot + .querySelector('gr-button[data-action-key="revert"]'); + MockInteractions.tap(revertButton); + flush(() => { + const confirmRevertDialog = element.$.confirmRevertDialog; + const radioInputs = confirmRevertDialog.shadowRoot + .querySelectorAll('input[name="revertOptions"]'); + assert.equal(radioInputs.length, 0); + const msg = 'Revert "random commit message"\n\n' + + 'This reverts commit 2000.\n\nReason ' + + 'for revert: <INSERT REASONING HERE>\n'; + assert.equal(confirmRevertDialog._message, msg); + const editedMsg = msg + 'hello'; + confirmRevertDialog._message += 'hello'; + const confirmButton = element.$.confirmRevertDialog.shadowRoot + .querySelector('gr-dialog') + .shadowRoot.querySelector('#confirm'); + MockInteractions.tap(confirmButton); + flush(() => { + assert.equal(fireActionStub.getCall(0).args[0], '/revert'); + assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert'); + assert.equal(fireActionStub.getCall(0).args[3].message, + editedMsg); + done(); + }); + }); + }); + }); + }); + + suite('mark change private', () => { + setup(() => { + const privateAction = { + __key: 'private', + __type: 'change', + __primary: false, + method: 'POST', + label: 'Mark private', + title: 'Working...', + enabled: true, + }; + + element.actions = { + private: privateAction, + }; + + element.change.is_private = false; + + element.changeNum = '2'; + element.latestPatchNum = '2'; + + return element.reload(); + }); + + test('make sure the mark private change button is not outside of the ' + + 'overflow menu', done => { + flush(() => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="private"]')); + done(); + }); + }); + + test('private change', done => { + flush(() => { + assert.isOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="private-change"]')); + element.setActionOverflow('change', 'private', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="private"]')); + assert.isNotOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="private-change"]')); + done(); + }); + }); + }); + + suite('unmark private change', () => { + setup(() => { + const unmarkPrivateAction = { + __key: 'private.delete', + __type: 'change', + __primary: false, + method: 'POST', + label: 'Unmark private', + title: 'Working...', + enabled: true, + }; + + element.actions = { + 'private.delete': unmarkPrivateAction, + }; + + element.change.is_private = true; + + element.changeNum = '2'; + element.latestPatchNum = '2'; + + return element.reload(); + }); + + test('make sure the unmark private change button is not outside of the ' + + 'overflow menu', done => { + flush(() => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="private.delete"]')); + done(); + }); + }); + + test('unmark the private change', done => { + flush(() => { + assert.isOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="private.delete-change"]') + ); + element.setActionOverflow('change', 'private.delete', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="private.delete"]')); + assert.isNotOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="private.delete-change"]') + ); + done(); + }); + }); + }); + + suite('delete change', () => { + let fireActionStub; + let deleteAction; + + setup(() => { + fireActionStub = sandbox.stub(element, '_fireAction'); + element.change = { + current_revision: 'abc1234', + }; + deleteAction = { + method: 'DELETE', + label: 'Delete Change', + title: 'Delete change X_X', + enabled: true, + }; + element.actions = { + '/': deleteAction, + }; + }); + + test('does not delete on action', () => { + element._handleDeleteTap(); + assert.isFalse(fireActionStub.called); + }); + + test('shows confirm dialog', () => { + element._handleDeleteTap(); + assert.isFalse(element.shadowRoot + .querySelector('#confirmDeleteDialog').hidden); + MockInteractions.tap( + element.shadowRoot + .querySelector('#confirmDeleteDialog') + .shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + assert.isTrue(fireActionStub.calledWith('/', deleteAction, false)); + }); + + test('hides delete confirm on cancel', () => { + element._handleDeleteTap(); + MockInteractions.tap( + element.shadowRoot + .querySelector('#confirmDeleteDialog') + .shadowRoot + .querySelector('gr-button:not([primary])')); + flushAsynchronousOperations(); + assert.isTrue(element.shadowRoot + .querySelector('#confirmDeleteDialog').hidden); + assert.isFalse(fireActionStub.called); + }); + }); + + suite('ignore change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const IgnoreAction = { + __key: 'ignore', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Ignore', + title: 'Working...', + enabled: true, + }; + + element.actions = { + ignore: IgnoreAction, + }; + + element.changeNum = '2'; + element.latestPatchNum = '2'; + + element.reload().then(() => { flush(done); }); + }); + + test('make sure the ignore button is not outside of the overflow menu', + () => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="ignore"]')); + }); + + test('ignoring change', () => { + assert.isOk(element.$.moreActions.shadowRoot + .querySelector('span[data-id="ignore-change"]')); + element.setActionOverflow('change', 'ignore', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="ignore"]')); + assert.isNotOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="ignore-change"]')); + }); + }); + + suite('unignore change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const UnignoreAction = { + __key: 'unignore', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Unignore', + title: 'Working...', + enabled: true, + }; + + element.actions = { + unignore: UnignoreAction, + }; + + element.changeNum = '2'; + element.latestPatchNum = '2'; + + element.reload().then(() => { flush(done); }); + }); + + test('unignore button is not outside of the overflow menu', () => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="unignore"]')); + }); + + test('unignoring change', () => { + assert.isOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="unignore-change"]')); + element.setActionOverflow('change', 'unignore', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="unignore"]')); + assert.isNotOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="unignore-change"]')); + }); + }); + + suite('reviewed change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const ReviewedAction = { + __key: 'reviewed', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Mark reviewed', + title: 'Working...', + enabled: true, + }; + + element.actions = { + reviewed: ReviewedAction, + }; + + element.changeNum = '2'; + element.latestPatchNum = '2'; + + element.reload().then(() => { flush(done); }); + }); + + test('make sure the reviewed button is not outside of the overflow menu', + () => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="reviewed"]')); + }); + + test('reviewing change', () => { + assert.isOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="reviewed-change"]')); + element.setActionOverflow('change', 'reviewed', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="reviewed"]')); + assert.isNotOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="reviewed-change"]')); + }); + }); + + suite('unreviewed change', () => { + setup(done => { + sandbox.stub(element, '_fireAction'); + + const UnreviewedAction = { + __key: 'unreviewed', + __type: 'change', + __primary: false, + method: 'PUT', + label: 'Mark unreviewed', + title: 'Working...', + enabled: true, + }; + + element.actions = { + unreviewed: UnreviewedAction, + }; + + element.changeNum = '2'; + element.latestPatchNum = '2'; + + element.reload().then(() => { flush(done); }); + }); + + test('unreviewed button not outside of the overflow menu', () => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="unreviewed"]')); + }); + + test('unreviewed change', () => { + assert.isOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="unreviewed-change"]')); + element.setActionOverflow('change', 'unreviewed', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="unreviewed"]')); + assert.isNotOk( + element.$.moreActions.shadowRoot + .querySelector('span[data-id="unreviewed-change"]')); + }); + }); + + suite('quick approve', () => { + setup(() => { + element.change = { + current_revision: 'abc1234', + }; + element.change = { + current_revision: 'abc1234', + labels: { + foo: { + values: { + '-1': '', + ' 0': '', + '+1': '', + }, + }, + }, + permitted_labels: { + foo: ['-1', ' 0', '+1'], + }, + }; + flushAsynchronousOperations(); + }); + + test('added when can approve', () => { + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNotNull(approveButton); + }); + + test('hide quick approve', () => { + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNotNull(approveButton); + assert.isFalse(element._hideQuickApproveAction); + + // Assert approve button gets removed from list of buttons. + element.hideQuickApproveAction(); + flushAsynchronousOperations(); + const approveButtonUpdated = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButtonUpdated); + assert.isTrue(element._hideQuickApproveAction); + }); + + test('is first in list of secondary actions', () => { + const approveButton = element.$.secondaryActions + .querySelector('gr-button'); + assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); + }); + + test('not added when already approved', () => { + element.change = { + current_revision: 'abc1234', + labels: { + foo: { + approved: {}, + values: {}, + }, + }, + permitted_labels: { + foo: [' 0', '+1'], + }, + }; + flushAsynchronousOperations(); + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('not added when label not permitted', () => { + element.change = { + current_revision: 'abc1234', + labels: { + foo: {values: {}}, + }, + permitted_labels: { + bar: [], + }, + }; + flushAsynchronousOperations(); + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('approves when tapped', () => { + const fireActionStub = sandbox.stub(element, '_fireAction'); + MockInteractions.tap( + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']')); + flushAsynchronousOperations(); + assert.isTrue(fireActionStub.called); + assert.isTrue(fireActionStub.calledWith('/review')); + const payload = fireActionStub.lastCall.args[3]; + assert.deepEqual(payload.labels, {foo: '+1'}); + }); + + test('not added when multiple labels are required', () => { + element.change = { + current_revision: 'abc1234', + labels: { + foo: {values: {}}, + bar: {values: {}}, + }, + permitted_labels: { + foo: [' 0', '+1'], + bar: [' 0', '+1', '+2'], + }, + }; + flushAsynchronousOperations(); + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('button label for missing approval', () => { + element.change = { + current_revision: 'abc1234', + labels: { + foo: { + values: { + ' 0': '', + '+1': '', + }, + }, + bar: {approved: {}, values: {}}, + }, + permitted_labels: { + foo: [' 0', '+1'], + bar: [' 0', '+1', '+2'], + }, + }; + flushAsynchronousOperations(); + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); + }); + + test('no quick approve if score is not maximal for a label', () => { + element.change = { + current_revision: 'abc1234', + labels: { + bar: { + value: 1, + values: { + ' 0': '', + '+1': '', + '+2': '', + }, + }, + }, + permitted_labels: { + bar: [' 0', '+1'], + }, + }; + flushAsynchronousOperations(); + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.isNull(approveButton); + }); + + test('approving label with a non-max score', () => { + element.change = { + current_revision: 'abc1234', + labels: { + bar: { + value: 1, + values: { + ' 0': '', + '+1': '', + '+2': '', + }, + }, + }, + permitted_labels: { + bar: [' 0', '+1', '+2'], + }, + }; + flushAsynchronousOperations(); + const approveButton = + element.shadowRoot + .querySelector('gr-button[data-action-key=\'review\']'); + assert.equal(approveButton.getAttribute('data-label'), 'bar+2'); + }); + }); + + test('adds download revision action', () => { + const handler = sandbox.stub(); + element.addEventListener('download-tap', handler); + assert.ok(element.revisionActions.download); + element._handleDownloadTap(); + flushAsynchronousOperations(); + + assert.isTrue(handler.called); + }); + + test('changing changeNum or patchNum does not reload', () => { + const reloadStub = sandbox.stub(element, 'reload'); + element.changeNum = 123; + assert.isFalse(reloadStub.called); + element.latestPatchNum = 456; + assert.isFalse(reloadStub.called); + }); + + test('_toSentenceCase', () => { + assert.equal(element._toSentenceCase('blah blah'), 'Blah blah'); + assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah'); + assert.equal(element._toSentenceCase('b'), 'B'); + assert.equal(element._toSentenceCase(''), ''); + assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()'); + }); + + suite('setActionOverflow', () => { + test('move action from overflow', () => { + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="cherrypick"]')); + assert.strictEqual( + element.$.moreActions.items[0].id, 'cherrypick-revision'); + element.setActionOverflow('revision', 'cherrypick', false); + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="cherrypick"]')); + assert.notEqual( + element.$.moreActions.items[0].id, 'cherrypick-revision'); + }); + + test('move action to overflow', () => { + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="submit"]')); + element.setActionOverflow('revision', 'submit', true); + flushAsynchronousOperations(); + assert.isNotOk(element.shadowRoot + .querySelector('[data-action-key="submit"]')); + assert.strictEqual( + element.$.moreActions.items[3].id, 'submit-revision'); + }); + + suite('_waitForChangeReachable', () => { + setup(() => { + sandbox.stub(element, 'async', fn => fn()); + }); + + const makeGetChange = numTries => () => { + if (numTries === 1) { + return Promise.resolve({_number: 123}); + } else { + numTries--; + return Promise.resolve(undefined); + } + }; + + test('succeed', () => { + sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5)); + return element._waitForChangeReachable(123).then(success => { + assert.isTrue(success); + }); + }); + + test('fail', () => { + sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6)); + return element._waitForChangeReachable(123).then(success => { + assert.isFalse(success); + }); + }); + }); + }); + + suite('_send', () => { + let cleanup; + let payload; + let onShowError; + let onShowAlert; + let getResponseObjectStub; + + setup(() => { + cleanup = sinon.stub(); + element.changeNum = 42; + element.latestPatchNum = 12; + payload = {foo: 'bar'}; + + onShowError = sinon.stub(); + element.addEventListener('show-error', onShowError); + onShowAlert = sinon.stub(); + element.addEventListener('show-alert', onShowAlert); + }); + + suite('happy path', () => { + let sendStub; + setup(() => { + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({isLatest: true})); + sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction') + .returns(Promise.resolve({})); + getResponseObjectStub = sandbox.stub(element.$.restAPI, + 'getResponseObject'); + sandbox.stub(Gerrit.Nav, + 'navigateToChange').returns(Promise.resolve(true)); + }); + + test('change action', done => { + element + ._send('DELETE', payload, '/endpoint', false, cleanup) + .then(() => { + assert.isFalse(onShowError.called); + assert.isTrue(cleanup.calledOnce); + assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint', + null, payload)); + done(); + }); + }); + + suite('show revert submission dialog', () => { setup(() => { - element.change = { - submission_id: '199', - current_revision: '2000', - }; + element.change.submission_id = '199'; + element.change.current_revision = '2000'; sandbox.stub(element.$.restAPI, 'getChanges') .returns(Promise.resolve([ {change_id: '12345678901234', topic: 'T', subject: 'random'}, @@ -923,1047 +1748,232 @@ ])); }); - test('confirm revert dialog shows both options', done => { - const revertButton = element.shadowRoot - .querySelector('gr-button[data-action-key="revert"]'); - MockInteractions.tap(revertButton); - flush(() => { - const confirmRevertDialog = element.$.confirmRevertDialog; - const revertSingleChangeLabel = confirmRevertDialog - .shadowRoot.querySelector('.revertSingleChange'); - const revertSubmissionLabel = confirmRevertDialog. - shadowRoot.querySelector('.revertSubmission'); - assert(revertSingleChangeLabel.innerText.trim() === - 'Revert single change'); - assert(revertSubmissionLabel.innerText.trim() === - 'Revert entire submission (2 Changes)'); - let expectedMsg = 'Revert submission 199' + '\n\n' + - 'Reason for revert: <INSERT REASONING HERE>' + '\n' + - 'Reverted Changes:' + '\n' + - '1234567890:random' + '\n' + - '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + - '\n'; - assert.equal(confirmRevertDialog._message, expectedMsg); - const radioInputs = confirmRevertDialog.shadowRoot - .querySelectorAll('input[name="revertOptions"]'); - MockInteractions.tap(radioInputs[0]); - flush(() => { - expectedMsg = 'Revert "random commit message"\n\nThis reverts ' - + 'commit 2000.\n\nReason' - + ' for revert: <INSERT REASONING HERE>\n'; - assert.equal(confirmRevertDialog._message, expectedMsg); - done(); - }); - }); - }); - - test('submit fails if message is not edited', done => { - const revertButton = element.shadowRoot - .querySelector('gr-button[data-action-key="revert"]'); - const confirmRevertDialog = element.$.confirmRevertDialog; - MockInteractions.tap(revertButton); - const fireStub = sandbox.stub(confirmRevertDialog, 'fire'); - flush(() => { - const confirmButton = element.$.confirmRevertDialog.shadowRoot - .querySelector('gr-dialog') - .shadowRoot.querySelector('#confirm'); - MockInteractions.tap(confirmButton); - flush(() => { - assert.isTrue(confirmRevertDialog._showErrorMessage); - assert.isFalse(fireStub.called); - done(); - }); - }); - }); - - test('message modification is retained on switching', done => { - const revertButton = element.shadowRoot - .querySelector('gr-button[data-action-key="revert"]'); - const confirmRevertDialog = element.$.confirmRevertDialog; - MockInteractions.tap(revertButton); - flush(() => { - const radioInputs = confirmRevertDialog.shadowRoot - .querySelectorAll('input[name="revertOptions"]'); - const revertSubmissionMsg = 'Revert submission 199' + '\n\n' + + test('revert submission shows submissionId', done => { + const expectedMsg = 'Revert submission 199' + '\n\n' + 'Reason for revert: <INSERT REASONING HERE>' + '\n' + 'Reverted Changes:' + '\n' + - '1234567890:random' + '\n' + - '23456:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + + '1234567890: random' + '\n' + + '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + '\n'; - const singleChangeMsg = - 'Revert "random commit message"\n\nThis reverts ' - + 'commit 2000.\n\nReason' - + ' for revert: <INSERT REASONING HERE>\n'; - assert.equal(confirmRevertDialog._message, revertSubmissionMsg); - const newRevertMsg = revertSubmissionMsg + 'random'; - const newSingleChangeMsg = singleChangeMsg + 'random'; - confirmRevertDialog._message = newRevertMsg; - MockInteractions.tap(radioInputs[0]); - flush(() => { - assert.equal(confirmRevertDialog._message, singleChangeMsg); - confirmRevertDialog._message = newSingleChangeMsg; - MockInteractions.tap(radioInputs[1]); - flush(() => { - assert.equal(confirmRevertDialog._message, newRevertMsg); - MockInteractions.tap(radioInputs[0]); - flush(() => { - assert.equal( - confirmRevertDialog._message, - newSingleChangeMsg - ); + const modifiedMsg = expectedMsg + 'abcd'; + sandbox.stub(element.$.confirmRevertSubmissionDialog, + '_modifyRevertSubmissionMsg').returns(modifiedMsg); + element.showRevertSubmissionDialog(); + flush(() => { + const msg = element.$.confirmRevertSubmissionDialog.message; + assert.equal(msg, modifiedMsg); + done(); + }); + }); + }); + + suite('single changes revert', () => { + let navigateToSearchQueryStub; + setup(() => { + getResponseObjectStub + .returns(Promise.resolve({revert_changes: [ + {change_id: 12345}, + ]})); + navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav, + 'navigateToSearchQuery'); + }); + + test('revert submission single change', done => { + element._send('POST', {message: 'Revert submission'}, + '/revert_submission', false, cleanup).then(res => { + element._handleResponse({__key: 'revert_submission'}, {}). + then(() => { + assert.isTrue(navigateToSearchQueryStub.called); done(); }); - }); - }); }); }); }); - suite('revert single change', () => { + suite('multiple changes revert', () => { + let showActionDialogStub; + let navigateToSearchQueryStub; setup(() => { - element.change = { - submission_id: '199', - current_revision: '2000', - }; - sandbox.stub(element.$.restAPI, 'getChanges') - .returns(Promise.resolve([ - {change_id: '12345678901234', topic: 'T', subject: 'random'}, - ])); + getResponseObjectStub + .returns(Promise.resolve({revert_changes: [ + {change_id: 12345, topic: 'T'}, + {change_id: 23456, topic: 'T'}, + ]})); + showActionDialogStub = sandbox.stub(element, '_showActionDialog'); + navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav, + 'navigateToSearchQuery'); }); - test('submit fails if message is not edited', done => { - const revertButton = element.shadowRoot - .querySelector('gr-button[data-action-key="revert"]'); - const confirmRevertDialog = element.$.confirmRevertDialog; - MockInteractions.tap(revertButton); - const fireStub = sandbox.stub(confirmRevertDialog, 'fire'); - flush(() => { - const confirmButton = element.$.confirmRevertDialog.shadowRoot - .querySelector('gr-dialog') - .shadowRoot.querySelector('#confirm'); - MockInteractions.tap(confirmButton); - flush(() => { - assert.isTrue(confirmRevertDialog._showErrorMessage); - assert.isFalse(fireStub.called); + test('revert submission multiple change', done => { + element._send('POST', {message: 'Revert submission'}, + '/revert_submission', false, cleanup).then(res => { + element._handleResponse({__key: 'revert_submission'}, {}).then( + () => { + assert.isFalse(showActionDialogStub.called); + assert.isTrue(navigateToSearchQueryStub.calledWith( + 'topic: T')); + done(); + }); + }); + }); + }); + + test('revision action', done => { + element + ._send('DELETE', payload, '/endpoint', true, cleanup) + .then(() => { + assert.isFalse(onShowError.called); + assert.isTrue(cleanup.calledOnce); + assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint', + 12, payload)); done(); }); - }); - }); + }); + }); - test('confirm revert dialog shows no radio button', done => { - const revertButton = element.shadowRoot - .querySelector('gr-button[data-action-key="revert"]'); - MockInteractions.tap(revertButton); - flush(() => { - const confirmRevertDialog = element.$.confirmRevertDialog; - const radioInputs = confirmRevertDialog.shadowRoot - .querySelectorAll('input[name="revertOptions"]'); - assert.equal(radioInputs.length, 0); - const msg = 'Revert "random commit message"\n\n' - + 'This reverts commit 2000.\n\nReason ' - + 'for revert: <INSERT REASONING HERE>\n'; - assert.equal(confirmRevertDialog._message, msg); - const editedMsg = msg + 'hello'; - confirmRevertDialog._message += 'hello'; - const confirmButton = element.$.confirmRevertDialog.shadowRoot - .querySelector('gr-dialog') - .shadowRoot.querySelector('#confirm'); - MockInteractions.tap(confirmButton); - flush(() => { - assert.equal(fireActionStub.getCall(0).args[0], '/revert'); - assert.equal(fireActionStub.getCall(0).args[1].__key, 'revert'); - assert.equal(fireActionStub.getCall(0).args[3].message, - editedMsg); - done(); + suite('failure modes', () => { + test('non-latest', () => { + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({isLatest: false})); + const sendStub = sandbox.stub(element.$.restAPI, + 'executeChangeAction'); + + return element._send('DELETE', payload, '/endpoint', true, cleanup) + .then(() => { + assert.isTrue(onShowAlert.calledOnce); + assert.isFalse(onShowError.called); + assert.isTrue(cleanup.calledOnce); + assert.isFalse(sendStub.called); }); - }); - }); - }); - }); - - suite('mark change private', () => { - setup(() => { - const privateAction = { - __key: 'private', - __type: 'change', - __primary: false, - method: 'POST', - label: 'Mark private', - title: 'Working...', - enabled: true, - }; - - element.actions = { - private: privateAction, - }; - - element.change.is_private = false; - - element.changeNum = '2'; - element.latestPatchNum = '2'; - - return element.reload(); }); - test('make sure the mark private change button is not outside of the ' + - 'overflow menu', done => { - flush(() => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="private"]')); - done(); - }); - }); - - test('private change', done => { - flush(() => { - assert.isOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="private-change"]')); - element.setActionOverflow('change', 'private', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="private"]')); - assert.isNotOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="private-change"]')); - done(); - }); - }); - }); - - suite('unmark private change', () => { - setup(() => { - const unmarkPrivateAction = { - __key: 'private.delete', - __type: 'change', - __primary: false, - method: 'POST', - label: 'Unmark private', - title: 'Working...', - enabled: true, - }; - - element.actions = { - 'private.delete': unmarkPrivateAction, - }; - - element.change.is_private = true; - - element.changeNum = '2'; - element.latestPatchNum = '2'; - - return element.reload(); - }); - - test('make sure the unmark private change button is not outside of the ' + - 'overflow menu', done => { - flush(() => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="private.delete"]')); - done(); - }); - }); - - test('unmark the private change', done => { - flush(() => { - assert.isOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="private.delete-change"]') - ); - element.setActionOverflow('change', 'private.delete', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="private.delete"]')); - assert.isNotOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="private.delete-change"]') - ); - done(); - }); - }); - }); - - suite('delete change', () => { - let fireActionStub; - let deleteAction; - - setup(() => { - fireActionStub = sandbox.stub(element, '_fireAction'); - element.change = { - current_revision: 'abc1234', - }; - deleteAction = { - method: 'DELETE', - label: 'Delete Change', - title: 'Delete change X_X', - enabled: true, - }; - element.actions = { - '/': deleteAction, - }; - }); - - test('does not delete on action', () => { - element._handleDeleteTap(); - assert.isFalse(fireActionStub.called); - }); - - test('shows confirm dialog', () => { - element._handleDeleteTap(); - assert.isFalse(element.shadowRoot - .querySelector('#confirmDeleteDialog').hidden); - MockInteractions.tap( - element.shadowRoot - .querySelector('#confirmDeleteDialog') - .shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - assert.isTrue(fireActionStub.calledWith('/', deleteAction, false)); - }); - - test('hides delete confirm on cancel', () => { - element._handleDeleteTap(); - MockInteractions.tap( - element.shadowRoot - .querySelector('#confirmDeleteDialog') - .shadowRoot - .querySelector('gr-button:not([primary])')); - flushAsynchronousOperations(); - assert.isTrue(element.shadowRoot - .querySelector('#confirmDeleteDialog').hidden); - assert.isFalse(fireActionStub.called); - }); - }); - - suite('ignore change', () => { - setup(done => { - sandbox.stub(element, '_fireAction'); - - const IgnoreAction = { - __key: 'ignore', - __type: 'change', - __primary: false, - method: 'PUT', - label: 'Ignore', - title: 'Working...', - enabled: true, - }; - - element.actions = { - ignore: IgnoreAction, - }; - - element.changeNum = '2'; - element.latestPatchNum = '2'; - - element.reload().then(() => { flush(done); }); - }); - - test('make sure the ignore button is not outside of the overflow menu', - () => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="ignore"]')); - }); - - test('ignoring change', () => { - assert.isOk(element.$.moreActions.shadowRoot - .querySelector('span[data-id="ignore-change"]')); - element.setActionOverflow('change', 'ignore', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="ignore"]')); - assert.isNotOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="ignore-change"]')); - }); - }); - - suite('unignore change', () => { - setup(done => { - sandbox.stub(element, '_fireAction'); - - const UnignoreAction = { - __key: 'unignore', - __type: 'change', - __primary: false, - method: 'PUT', - label: 'Unignore', - title: 'Working...', - enabled: true, - }; - - element.actions = { - unignore: UnignoreAction, - }; - - element.changeNum = '2'; - element.latestPatchNum = '2'; - - element.reload().then(() => { flush(done); }); - }); - - test('unignore button is not outside of the overflow menu', () => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="unignore"]')); - }); - - test('unignoring change', () => { - assert.isOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="unignore-change"]')); - element.setActionOverflow('change', 'unignore', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="unignore"]')); - assert.isNotOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="unignore-change"]')); - }); - }); - - suite('reviewed change', () => { - setup(done => { - sandbox.stub(element, '_fireAction'); - - const ReviewedAction = { - __key: 'reviewed', - __type: 'change', - __primary: false, - method: 'PUT', - label: 'Mark reviewed', - title: 'Working...', - enabled: true, - }; - - element.actions = { - reviewed: ReviewedAction, - }; - - element.changeNum = '2'; - element.latestPatchNum = '2'; - - element.reload().then(() => { flush(done); }); - }); - - test('make sure the reviewed button is not outside of the overflow menu', - () => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="reviewed"]')); - }); - - test('reviewing change', () => { - assert.isOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="reviewed-change"]')); - element.setActionOverflow('change', 'reviewed', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="reviewed"]')); - assert.isNotOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="reviewed-change"]')); - }); - }); - - suite('unreviewed change', () => { - setup(done => { - sandbox.stub(element, '_fireAction'); - - const UnreviewedAction = { - __key: 'unreviewed', - __type: 'change', - __primary: false, - method: 'PUT', - label: 'Mark unreviewed', - title: 'Working...', - enabled: true, - }; - - element.actions = { - unreviewed: UnreviewedAction, - }; - - element.changeNum = '2'; - element.latestPatchNum = '2'; - - element.reload().then(() => { flush(done); }); - }); - - test('unreviewed button not outside of the overflow menu', () => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="unreviewed"]')); - }); - - test('unreviewed change', () => { - assert.isOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="unreviewed-change"]')); - element.setActionOverflow('change', 'unreviewed', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="unreviewed"]')); - assert.isNotOk( - element.$.moreActions.shadowRoot - .querySelector('span[data-id="unreviewed-change"]')); - }); - }); - - suite('quick approve', () => { - setup(() => { - element.change = { - current_revision: 'abc1234', - }; - element.change = { - current_revision: 'abc1234', - labels: { - foo: { - values: { - '-1': '', - ' 0': '', - '+1': '', - }, - }, - }, - permitted_labels: { - foo: ['-1', ' 0', '+1'], - }, - }; - flushAsynchronousOperations(); - }); - - test('added when can approve', () => { - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNotNull(approveButton); - }); - - test('hide quick approve', () => { - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNotNull(approveButton); - assert.isFalse(element._hideQuickApproveAction); - - // Assert approve button gets removed from list of buttons. - element.hideQuickApproveAction(); - flushAsynchronousOperations(); - const approveButtonUpdated = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNull(approveButtonUpdated); - assert.isTrue(element._hideQuickApproveAction); - }); - - test('is first in list of secondary actions', () => { - const approveButton = element.$.secondaryActions - .querySelector('gr-button'); - assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); - }); - - test('not added when already approved', () => { - element.change = { - current_revision: 'abc1234', - labels: { - foo: { - approved: {}, - values: {}, - }, - }, - permitted_labels: { - foo: [' 0', '+1'], - }, - }; - flushAsynchronousOperations(); - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNull(approveButton); - }); - - test('not added when label not permitted', () => { - element.change = { - current_revision: 'abc1234', - labels: { - foo: {values: {}}, - }, - permitted_labels: { - bar: [], - }, - }; - flushAsynchronousOperations(); - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNull(approveButton); - }); - - test('approves when tapped', () => { - const fireActionStub = sandbox.stub(element, '_fireAction'); - MockInteractions.tap( - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']')); - flushAsynchronousOperations(); - assert.isTrue(fireActionStub.called); - assert.isTrue(fireActionStub.calledWith('/review')); - const payload = fireActionStub.lastCall.args[3]; - assert.deepEqual(payload.labels, {foo: '+1'}); - }); - - test('not added when multiple labels are required', () => { - element.change = { - current_revision: 'abc1234', - labels: { - foo: {values: {}}, - bar: {values: {}}, - }, - permitted_labels: { - foo: [' 0', '+1'], - bar: [' 0', '+1', '+2'], - }, - }; - flushAsynchronousOperations(); - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNull(approveButton); - }); - - test('button label for missing approval', () => { - element.change = { - current_revision: 'abc1234', - labels: { - foo: { - values: { - ' 0': '', - '+1': '', - }, - }, - bar: {approved: {}, values: {}}, - }, - permitted_labels: { - foo: [' 0', '+1'], - bar: [' 0', '+1', '+2'], - }, - }; - flushAsynchronousOperations(); - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.equal(approveButton.getAttribute('data-label'), 'foo+1'); - }); - - test('no quick approve if score is not maximal for a label', () => { - element.change = { - current_revision: 'abc1234', - labels: { - bar: { - value: 1, - values: { - ' 0': '', - '+1': '', - '+2': '', - }, - }, - }, - permitted_labels: { - bar: [' 0', '+1'], - }, - }; - flushAsynchronousOperations(); - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.isNull(approveButton); - }); - - test('approving label with a non-max score', () => { - element.change = { - current_revision: 'abc1234', - labels: { - bar: { - value: 1, - values: { - ' 0': '', - '+1': '', - '+2': '', - }, - }, - }, - permitted_labels: { - bar: [' 0', '+1', '+2'], - }, - }; - flushAsynchronousOperations(); - const approveButton = - element.shadowRoot - .querySelector('gr-button[data-action-key=\'review\']'); - assert.equal(approveButton.getAttribute('data-label'), 'bar+2'); - }); - }); - - test('adds download revision action', () => { - const handler = sandbox.stub(); - element.addEventListener('download-tap', handler); - assert.ok(element.revisionActions.download); - element._handleDownloadTap(); - flushAsynchronousOperations(); - - assert.isTrue(handler.called); - }); - - test('changing changeNum or patchNum does not reload', () => { - const reloadStub = sandbox.stub(element, 'reload'); - element.changeNum = 123; - assert.isFalse(reloadStub.called); - element.latestPatchNum = 456; - assert.isFalse(reloadStub.called); - }); - - test('_toSentenceCase', () => { - assert.equal(element._toSentenceCase('blah blah'), 'Blah blah'); - assert.equal(element._toSentenceCase('BLAH BLAH'), 'Blah blah'); - assert.equal(element._toSentenceCase('b'), 'B'); - assert.equal(element._toSentenceCase(''), ''); - assert.equal(element._toSentenceCase('!@#$%^&*()'), '!@#$%^&*()'); - }); - - suite('setActionOverflow', () => { - test('move action from overflow', () => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="cherrypick"]')); - assert.strictEqual( - element.$.moreActions.items[0].id, 'cherrypick-revision'); - element.setActionOverflow('revision', 'cherrypick', false); - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="cherrypick"]')); - assert.notEqual( - element.$.moreActions.items[0].id, 'cherrypick-revision'); - }); - - test('move action to overflow', () => { - assert.isOk(element.shadowRoot - .querySelector('[data-action-key="submit"]')); - element.setActionOverflow('revision', 'submit', true); - flushAsynchronousOperations(); - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="submit"]')); - assert.strictEqual( - element.$.moreActions.items[3].id, 'submit-revision'); - }); - - suite('_waitForChangeReachable', () => { - setup(() => { - sandbox.stub(element, 'async', fn => fn()); - }); - - const makeGetChange = numTries => () => { - if (numTries === 1) { - return Promise.resolve({_number: 123}); - } else { - numTries--; - return Promise.resolve(undefined); - } - }; - - test('succeed', () => { - sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(5)); - return element._waitForChangeReachable(123).then(success => { - assert.isTrue(success); - }); - }); - - test('fail', () => { - sandbox.stub(element.$.restAPI, 'getChange', makeGetChange(6)); - return element._waitForChangeReachable(123).then(success => { - assert.isFalse(success); - }); - }); - }); - }); - - suite('_send', () => { - let cleanup; - let payload; - let onShowError; - let onShowAlert; - let getResponseObjectStub; - - setup(() => { - cleanup = sinon.stub(); - element.changeNum = 42; - element.latestPatchNum = 12; - payload = {foo: 'bar'}; - - onShowError = sinon.stub(); - element.addEventListener('show-error', onShowError); - onShowAlert = sinon.stub(); - element.addEventListener('show-alert', onShowAlert); - }); - - suite('happy path', () => { - let sendStub; - setup(() => { - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({isLatest: true})); - sendStub = sandbox.stub(element.$.restAPI, 'executeChangeAction') - .returns(Promise.resolve({})); - getResponseObjectStub = sandbox.stub(element.$.restAPI, - 'getResponseObject'); - sandbox.stub(Gerrit.Nav, - 'navigateToChange').returns(Promise.resolve(true)); - }); - - test('change action', done => { - element - ._send('DELETE', payload, '/endpoint', false, cleanup) - .then(() => { - assert.isFalse(onShowError.called); - assert.isTrue(cleanup.calledOnce); - assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint', - null, payload)); - done(); - }); - }); - - suite('show revert submission dialog', () => { - setup(() => { - element.change.submission_id = '199'; - element.change.current_revision = '2000'; - sandbox.stub(element.$.restAPI, 'getChanges') - .returns(Promise.resolve([ - {change_id: '12345678901234', topic: 'T', subject: 'random'}, - {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)}, - ])); - }); - - test('revert submission shows submissionId', done => { - const expectedMsg = 'Revert submission 199' + '\n\n' + - 'Reason for revert: <INSERT REASONING HERE>' + '\n' + - 'Reverted Changes:' + '\n' + - '1234567890: random' + '\n' + - '23456: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' + - '\n'; - const modifiedMsg = expectedMsg + 'abcd'; - sandbox.stub(element.$.confirmRevertSubmissionDialog, - '_modifyRevertSubmissionMsg').returns(modifiedMsg); - element.showRevertSubmissionDialog(); - flush(() => { - const msg = element.$.confirmRevertSubmissionDialog.message; - assert.equal(msg, modifiedMsg); - done(); + test('send fails', () => { + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({isLatest: true})); + const sendStub = sandbox.stub(element.$.restAPI, + 'executeChangeAction', + (num, method, patchNum, endpoint, payload, onErr) => { + onErr(); + return Promise.resolve(null); }); - }); - }); + const handleErrorStub = sandbox.stub(element, '_handleResponseError'); - suite('single changes revert', () => { - let navigateToSearchQueryStub; - setup(() => { - getResponseObjectStub - .returns(Promise.resolve({revert_changes: [ - {change_id: 12345}, - ]})); - navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav, - 'navigateToSearchQuery'); - }); - - test('revert submission single change', done => { - element._send('POST', {message: 'Revert submission'}, - '/revert_submission', false, cleanup).then(res => { - element._handleResponse({__key: 'revert_submission'}, {}). - then(() => { - assert.isTrue(navigateToSearchQueryStub.called); - done(); - }); + return element._send('DELETE', payload, '/endpoint', true, cleanup) + .then(() => { + assert.isFalse(onShowError.called); + assert.isTrue(cleanup.called); + assert.isTrue(sendStub.calledOnce); + assert.isTrue(handleErrorStub.called); }); - }); - }); - - suite('multiple changes revert', () => { - let showActionDialogStub; - let navigateToSearchQueryStub; - setup(() => { - getResponseObjectStub - .returns(Promise.resolve({revert_changes: [ - {change_id: 12345, topic: 'T'}, - {change_id: 23456, topic: 'T'}, - ]})); - showActionDialogStub = sandbox.stub(element, '_showActionDialog'); - navigateToSearchQueryStub = sandbox.stub(Gerrit.Nav, - 'navigateToSearchQuery'); - }); - - test('revert submission multiple change', done => { - element._send('POST', {message: 'Revert submission'}, - '/revert_submission', false, cleanup).then(res => { - element._handleResponse({__key: 'revert_submission'}, {}).then( - () => { - assert.isFalse(showActionDialogStub.called); - assert.isTrue(navigateToSearchQueryStub.calledWith( - 'topic: T')); - done(); - }); - }); - }); - }); - - test('revision action', done => { - element - ._send('DELETE', payload, '/endpoint', true, cleanup) - .then(() => { - assert.isFalse(onShowError.called); - assert.isTrue(cleanup.calledOnce); - assert.isTrue(sendStub.calledWith(42, 'DELETE', '/endpoint', - 12, payload)); - done(); - }); - }); }); - - suite('failure modes', () => { - test('non-latest', () => { - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({isLatest: false})); - const sendStub = sandbox.stub(element.$.restAPI, - 'executeChangeAction'); - - return element._send('DELETE', payload, '/endpoint', true, cleanup) - .then(() => { - assert.isTrue(onShowAlert.calledOnce); - assert.isFalse(onShowError.called); - assert.isTrue(cleanup.calledOnce); - assert.isFalse(sendStub.called); - }); - }); - - test('send fails', () => { - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({isLatest: true})); - const sendStub = sandbox.stub(element.$.restAPI, - 'executeChangeAction', - (num, method, patchNum, endpoint, payload, onErr) => { - onErr(); - return Promise.resolve(null); - }); - const handleErrorStub = sandbox.stub(element, '_handleResponseError'); - - return element._send('DELETE', payload, '/endpoint', true, cleanup) - .then(() => { - assert.isFalse(onShowError.called); - assert.isTrue(cleanup.called); - assert.isTrue(sendStub.calledOnce); - assert.isTrue(handleErrorStub.called); - }); - }); - }); - }); - - test('_handleAction reports', () => { - sandbox.stub(element, '_fireAction'); - const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction'); - element._handleAction('type', 'key'); - assert.isTrue(reportStub.called); - assert.equal(reportStub.lastCall.args[0], 'type-key'); }); }); - suite('getChangeRevisionActions returns only some actions', () => { - let element; - let sandbox; - let changeRevisionActions; - - setup(() => { - stub('gr-rest-api-interface', { - getChangeRevisionActions() { - return Promise.resolve(changeRevisionActions); - }, - send(method, url, payload) { - return Promise.reject(new Error('error')); - }, - getProjectConfig() { return Promise.resolve({}); }, - }); - - sandbox = sinon.sandbox.create(); - sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); - - element = fixture('basic'); - // getChangeRevisionActions is not called without - // set the following properies - element.change = {}; - element.changeNum = '42'; - element.latestPatchNum = '2'; - - sandbox.stub(element.$.confirmCherrypick.$.restAPI, - 'getRepoBranches').returns(Promise.resolve([])); - sandbox.stub(element.$.confirmMove.$.restAPI, - 'getRepoBranches').returns(Promise.resolve([])); - return element.reload(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('confirmSubmitDialog and confirmRebase properties are changed', () => { - changeRevisionActions = {}; - element.reload(); - assert.strictEqual(element.$.confirmSubmitDialog.action, null); - assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null); - }); - - test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => { - const currentRevisionActions = { - cherrypick: { - enabled: true, - label: 'Cherry Pick', - method: 'POST', - title: 'cherrypick', - }, - }; - element._parentIsCurrent = undefined; - element._updateRebaseAction(currentRevisionActions); - assert.isTrue(element._parentIsCurrent); - }); - - test('_updateRebaseAction', () => { - const currentRevisionActions = { - cherrypick: { - enabled: true, - label: 'Cherry Pick', - method: 'POST', - title: 'cherrypick', - }, - rebase: { - enabled: true, - label: 'Rebase', - method: 'POST', - title: 'Rebase onto tip of branch or parent change', - }, - }; - element._parentIsCurrent = undefined; - - // Rebase enabled should always end up true. - // When rebase is enabled initially, rebaseOnCurrent should be set to - // true. - assert.equal(element._updateRebaseAction(currentRevisionActions), - currentRevisionActions); - - assert.isTrue(currentRevisionActions.rebase.enabled); - assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent); - assert.isFalse(element._parentIsCurrent); - - delete currentRevisionActions.rebase.enabled; - - // When rebase is not enabled initially, rebaseOnCurrent should be set to - // false. - assert.equal(element._updateRebaseAction(currentRevisionActions), - currentRevisionActions); - - assert.isTrue(currentRevisionActions.rebase.enabled); - assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent); - assert.isTrue(element._parentIsCurrent); - }); + test('_handleAction reports', () => { + sandbox.stub(element, '_fireAction'); + const reportStub = sandbox.stub(element.$.reporting, 'reportInteraction'); + element._handleAction('type', 'key'); + assert.isTrue(reportStub.called); + assert.equal(reportStub.lastCall.args[0], 'type-key'); }); }); + + suite('getChangeRevisionActions returns only some actions', () => { + let element; + let sandbox; + let changeRevisionActions; + + setup(() => { + stub('gr-rest-api-interface', { + getChangeRevisionActions() { + return Promise.resolve(changeRevisionActions); + }, + send(method, url, payload) { + return Promise.reject(new Error('error')); + }, + getProjectConfig() { return Promise.resolve({}); }, + }); + + sandbox = sinon.sandbox.create(); + sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); + + element = fixture('basic'); + // getChangeRevisionActions is not called without + // set the following properies + element.change = {}; + element.changeNum = '42'; + element.latestPatchNum = '2'; + + sandbox.stub(element.$.confirmCherrypick.$.restAPI, + 'getRepoBranches').returns(Promise.resolve([])); + sandbox.stub(element.$.confirmMove.$.restAPI, + 'getRepoBranches').returns(Promise.resolve([])); + return element.reload(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('confirmSubmitDialog and confirmRebase properties are changed', () => { + changeRevisionActions = {}; + element.reload(); + assert.strictEqual(element.$.confirmSubmitDialog.action, null); + assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null); + }); + + test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => { + const currentRevisionActions = { + cherrypick: { + enabled: true, + label: 'Cherry Pick', + method: 'POST', + title: 'cherrypick', + }, + }; + element._parentIsCurrent = undefined; + element._updateRebaseAction(currentRevisionActions); + assert.isTrue(element._parentIsCurrent); + }); + + test('_updateRebaseAction', () => { + const currentRevisionActions = { + cherrypick: { + enabled: true, + label: 'Cherry Pick', + method: 'POST', + title: 'cherrypick', + }, + rebase: { + enabled: true, + label: 'Rebase', + method: 'POST', + title: 'Rebase onto tip of branch or parent change', + }, + }; + element._parentIsCurrent = undefined; + + // Rebase enabled should always end up true. + // When rebase is enabled initially, rebaseOnCurrent should be set to + // true. + assert.equal(element._updateRebaseAction(currentRevisionActions), + currentRevisionActions); + + assert.isTrue(currentRevisionActions.rebase.enabled); + assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent); + assert.isFalse(element._parentIsCurrent); + + delete currentRevisionActions.rebase.enabled; + + // When rebase is not enabled initially, rebaseOnCurrent should be set to + // false. + assert.equal(element._updateRebaseAction(currentRevisionActions), + currentRevisionActions); + + assert.isTrue(currentRevisionActions.rebase.enabled); + assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent); + assert.isTrue(element._parentIsCurrent); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html index 3269dc0..3ee7219 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-metadata</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html"> -<link rel="import" href="gr-change-metadata.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../plugins/gr-plugin-host/gr-plugin-host.js"></script> +<script type="module" src="./gr-change-metadata.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../plugins/gr-plugin-host/gr-plugin-host.js'; +import './gr-change-metadata.js'; +void(0); +</script> <test-fixture id="element"> <template> @@ -42,139 +48,143 @@ </template> </test-fixture> -<script> - suite('gr-change-metadata integration tests', async () => { - await readyToTest(); - let sandbox; - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../plugins/gr-plugin-host/gr-plugin-host.js'; +import './gr-change-metadata.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-change-metadata integration tests', () => { + let sandbox; + let element; - const sectionSelectors = [ - 'section.assignee', - 'section.strategy', - 'section.topic', - ]; + const sectionSelectors = [ + 'section.assignee', + 'section.strategy', + 'section.topic', + ]; - const labels = { - CI: { - all: [ - {value: 1, name: 'user 2', _account_id: 1}, - {value: 2, name: 'user '}, - ], - values: { - ' 0': 'Don\'t submit as-is', - '+1': 'No score', - '+2': 'Looks good to me', - }, + const labels = { + CI: { + all: [ + {value: 1, name: 'user 2', _account_id: 1}, + {value: 2, name: 'user '}, + ], + values: { + ' 0': 'Don\'t submit as-is', + '+1': 'No score', + '+2': 'Looks good to me', }, - }; + }, + }; - const getStyle = function(selector, name) { - return window.getComputedStyle( - Polymer.dom(element.root).querySelector(selector))[name]; - }; + const getStyle = function(selector, name) { + return window.getComputedStyle( + dom(element.root).querySelector(selector))[name]; + }; - function createElement() { - const element = fixture('element'); - element.change = {labels, status: 'NEW'}; - element.revision = {}; - return element; - } + function createElement() { + const element = fixture('element'); + element.change = {labels, status: 'NEW'}; + element.revision = {}; + return element; + } - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getLoggedIn() { return Promise.resolve(false); }, - deleteVote() { return Promise.resolve({ok: true}); }, - }); - }); - - teardown(() => { - sandbox.restore(); - Gerrit._testOnly_resetPlugins(); - }); - - suite('by default', () => { - setup(done => { - element = createElement(); - flush(done); - }); - - for (const sectionSelector of sectionSelectors) { - test(sectionSelector + ' does not have display: none', () => { - assert.notEqual(getStyle(sectionSelector, 'display'), 'none'); - }); - } - }); - - suite('with plugin style', () => { - setup(done => { - Gerrit._testOnly_resetPlugins(); - const pluginHost = fixture('plugin-host'); - pluginHost.config = { - plugin: { - js_resource_paths: [], - html_resource_paths: [ - new URL('test/plugin.html?' + Math.random(), - window.location.href).toString(), - ], - }, - }; - element = createElement(); - const importSpy = sandbox.spy(element.$.externalStyle, '_import'); - Gerrit.awaitPluginsLoaded().then(() => { - Promise.all(importSpy.returnValues).then(() => { - flush(done); - }); - }); - }); - - for (const sectionSelector of sectionSelectors) { - test(sectionSelector + ' may have display: none', () => { - assert.equal(getStyle(sectionSelector, 'display'), 'none'); - }); - } - }); - - suite('label updates', () => { - let plugin; - - setup(() => { - Gerrit.install(p => plugin = p, '0.1', - new URL('test/plugin.html?' + Math.random(), - window.location.href).toString()); - sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); - Gerrit._loadPlugins([]); - element = createElement(); - }); - - test('labels changed callback', done => { - let callCount = 0; - const labelChangeSpy = sandbox.spy(arg => { - callCount++; - if (callCount === 1) { - assert.deepEqual(arg, labels); - assert.equal(arg.CI.all.length, 2); - element.set(['change', 'labels'], { - CI: { - all: [ - {value: 1, name: 'user 2', _account_id: 1}, - ], - values: { - ' 0': 'Don\'t submit as-is', - '+1': 'No score', - '+2': 'Looks good to me', - }, - }, - }); - } else if (callCount === 2) { - assert.equal(arg.CI.all.length, 1); - done(); - } - }); - - plugin.changeMetadata().onLabelsChanged(labelChangeSpy); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, + deleteVote() { return Promise.resolve({ok: true}); }, }); }); + + teardown(() => { + sandbox.restore(); + Gerrit._testOnly_resetPlugins(); + }); + + suite('by default', () => { + setup(done => { + element = createElement(); + flush(done); + }); + + for (const sectionSelector of sectionSelectors) { + test(sectionSelector + ' does not have display: none', () => { + assert.notEqual(getStyle(sectionSelector, 'display'), 'none'); + }); + } + }); + + suite('with plugin style', () => { + setup(done => { + Gerrit._testOnly_resetPlugins(); + const pluginHost = fixture('plugin-host'); + pluginHost.config = { + plugin: { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html?' + Math.random(), + window.location.href).toString(), + ], + }, + }; + element = createElement(); + const importSpy = sandbox.spy(element.$.externalStyle, '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues).then(() => { + flush(done); + }); + }); + }); + + for (const sectionSelector of sectionSelectors) { + test(sectionSelector + ' may have display: none', () => { + assert.equal(getStyle(sectionSelector, 'display'), 'none'); + }); + } + }); + + suite('label updates', () => { + let plugin; + + setup(() => { + Gerrit.install(p => plugin = p, '0.1', + new URL('test/plugin.html?' + Math.random(), + window.location.href).toString()); + sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); + Gerrit._loadPlugins([]); + element = createElement(); + }); + + test('labels changed callback', done => { + let callCount = 0; + const labelChangeSpy = sandbox.spy(arg => { + callCount++; + if (callCount === 1) { + assert.deepEqual(arg, labels); + assert.equal(arg.CI.all.length, 2); + element.set(['change', 'labels'], { + CI: { + all: [ + {value: 1, name: 'user 2', _account_id: 1}, + ], + values: { + ' 0': 'Don\'t submit as-is', + '+1': 'No score', + '+2': 'Looks good to me', + }, + }, + }); + } else if (callCount === 2) { + assert.equal(arg.CI.all.length, 1); + done(); + } + }); + + plugin.changeMetadata().onLabelsChanged(labelChangeSpy); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js index ea11514..2e9cdf9 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -14,493 +14,524 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const HASHTAG_ADD_MESSAGE = 'Add Hashtag'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../../styles/gr-change-metadata-shared-styles.js'; +import '../../../styles/gr-change-view-integration-shared-styles.js'; +import '../../../styles/gr-voting-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../plugins/gr-external-style/gr-external-style.js'; +import '../../shared/gr-account-chip/gr-account-chip.js'; +import '../../shared/gr-account-link/gr-account-link.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-editable-label/gr-editable-label.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-limited-text/gr-limited-text.js'; +import '../../shared/gr-linked-chip/gr-linked-chip.js'; +import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-change-requirements/gr-change-requirements.js'; +import '../gr-commit-info/gr-commit-info.js'; +import '../gr-reviewer-list/gr-reviewer-list.js'; +import '../../shared/gr-account-list/gr-account-list.js'; +import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js'; +import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-metadata_html.js'; - const SubmitTypeLabel = { - FAST_FORWARD_ONLY: 'Fast Forward Only', - MERGE_IF_NECESSARY: 'Merge if Necessary', - REBASE_IF_NECESSARY: 'Rebase if Necessary', - MERGE_ALWAYS: 'Always Merge', - REBASE_ALWAYS: 'Rebase Always', - CHERRY_PICK: 'Cherry Pick', - }; +const HASHTAG_ADD_MESSAGE = 'Add Hashtag'; - const NOT_CURRENT_MESSAGE = 'Not current - rebase possible'; +const SubmitTypeLabel = { + FAST_FORWARD_ONLY: 'Fast Forward Only', + MERGE_IF_NECESSARY: 'Merge if Necessary', + REBASE_IF_NECESSARY: 'Rebase if Necessary', + MERGE_ALWAYS: 'Always Merge', + REBASE_ALWAYS: 'Rebase Always', + CHERRY_PICK: 'Cherry Pick', +}; +const NOT_CURRENT_MESSAGE = 'Not current - rebase possible'; + +/** + * @enum {string} + */ +const CertificateStatus = { /** - * @enum {string} + * This certificate status is bad. */ - const CertificateStatus = { - /** - * This certificate status is bad. - */ - BAD: 'BAD', - /** - * This certificate status is OK. - */ - OK: 'OK', - /** - * This certificate status is TRUSTED. - */ - TRUSTED: 'TRUSTED', - }; - + BAD: 'BAD', /** - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * This certificate status is OK. */ - class GrChangeMetadata extends Polymer.mixinBehaviors( [ - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-metadata'; } - /** - * Fired when the change topic is changed. - * - * @event topic-changed - */ + OK: 'OK', + /** + * This certificate status is TRUSTED. + */ + TRUSTED: 'TRUSTED', +}; - static get properties() { - return { +/** + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrChangeMetadata extends mixinBehaviors( [ + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-change-metadata'; } + /** + * Fired when the change topic is changed. + * + * @event topic-changed + */ + + static get properties() { + return { + /** @type {?} */ + change: Object, + labels: { + type: Object, + notify: true, + }, + account: Object, /** @type {?} */ - change: Object, - labels: { - type: Object, - notify: true, + revision: Object, + commitInfo: Object, + _mutable: { + type: Boolean, + computed: '_computeIsMutable(account)', + }, + /** @type {?} */ + serverConfig: Object, + parentIsCurrent: Boolean, + _notCurrentMessage: { + type: String, + value: NOT_CURRENT_MESSAGE, + readOnly: true, + }, + _topicReadOnly: { + type: Boolean, + computed: '_computeTopicReadOnly(_mutable, change)', + }, + _hashtagReadOnly: { + type: Boolean, + computed: '_computeHashtagReadOnly(_mutable, change)', + }, + /** + * @type {Gerrit.PushCertificateValidation} + */ + _pushCertificateValidation: { + type: Object, + computed: '_computePushCertificateValidation(serverConfig, change)', + }, + _showRequirements: { + type: Boolean, + computed: '_computeShowRequirements(change)', + }, + + _assignee: Array, + _isWip: { + type: Boolean, + computed: '_computeIsWip(change)', + }, + _newHashtag: String, + + _settingTopic: { + type: Boolean, + value: false, + }, + + _currentParents: { + type: Array, + computed: '_computeParents(change)', + }, + + /** @type {?} */ + _CHANGE_ROLE: { + type: Object, + readOnly: true, + value: { + OWNER: 'owner', + UPLOADER: 'uploader', + AUTHOR: 'author', + COMMITTER: 'committer', }, - account: Object, - /** @type {?} */ - revision: Object, - commitInfo: Object, - _mutable: { - type: Boolean, - computed: '_computeIsMutable(account)', - }, - /** @type {?} */ - serverConfig: Object, - parentIsCurrent: Boolean, - _notCurrentMessage: { - type: String, - value: NOT_CURRENT_MESSAGE, - readOnly: true, - }, - _topicReadOnly: { - type: Boolean, - computed: '_computeTopicReadOnly(_mutable, change)', - }, - _hashtagReadOnly: { - type: Boolean, - computed: '_computeHashtagReadOnly(_mutable, change)', - }, - /** - * @type {Gerrit.PushCertificateValidation} - */ - _pushCertificateValidation: { - type: Object, - computed: '_computePushCertificateValidation(serverConfig, change)', - }, - _showRequirements: { - type: Boolean, - computed: '_computeShowRequirements(change)', - }, + }, + }; + } - _assignee: Array, - _isWip: { - type: Boolean, - computed: '_computeIsWip(change)', - }, - _newHashtag: String, + static get observers() { + return [ + '_changeChanged(change)', + '_labelsChanged(change.labels)', + '_assigneeChanged(_assignee.*)', + ]; + } - _settingTopic: { - type: Boolean, - value: false, - }, + _labelsChanged(labels) { + this.labels = Object.assign({}, labels) || null; + } - _currentParents: { - type: Array, - computed: '_computeParents(change)', - }, + _changeChanged(change) { + this._assignee = change.assignee ? [change.assignee] : []; + } - /** @type {?} */ - _CHANGE_ROLE: { - type: Object, - readOnly: true, - value: { - OWNER: 'owner', - UPLOADER: 'uploader', - AUTHOR: 'author', - COMMITTER: 'committer', - }, - }, - }; - } - - static get observers() { - return [ - '_changeChanged(change)', - '_labelsChanged(change.labels)', - '_assigneeChanged(_assignee.*)', - ]; - } - - _labelsChanged(labels) { - this.labels = Object.assign({}, labels) || null; - } - - _changeChanged(change) { - this._assignee = change.assignee ? [change.assignee] : []; - } - - _assigneeChanged(assigneeRecord) { - if (!this.change) { return; } - const assignee = assigneeRecord.base; - if (assignee.length) { - const acct = assignee[0]; - if (this.change.assignee && - acct._account_id === this.change.assignee._account_id) { return; } - this.set(['change', 'assignee'], acct); - this.$.restAPI.setAssignee(this.change._number, acct._account_id); - } else { - if (!this.change.assignee) { return; } - this.set(['change', 'assignee'], undefined); - this.$.restAPI.deleteAssignee(this.change._number); - } - } - - _computeHideStrategy(change) { - return !this.changeIsOpen(change); - } - - /** - * @param {Object} commitInfo - * @return {?Array} If array is empty, returns null instead so - * an existential check can be used to hide or show the webLinks - * section. - */ - _computeWebLinks(commitInfo, serverConfig) { - if (!commitInfo) { return null; } - const weblinks = Gerrit.Nav.getChangeWeblinks( - this.change ? this.change.repo : '', - commitInfo.commit, - { - weblinks: commitInfo.web_links, - config: serverConfig, - }); - return weblinks.length ? weblinks : null; - } - - _computeStrategy(change) { - return SubmitTypeLabel[change.submit_type]; - } - - _computeLabelNames(labels) { - return Object.keys(labels).sort(); - } - - _handleTopicChanged(e, topic) { - const lastTopic = this.change.topic; - if (!topic.length) { topic = null; } - this._settingTopic = true; - this.$.restAPI.setChangeTopic(this.change._number, topic) - .then(newTopic => { - this._settingTopic = false; - this.set(['change', 'topic'], newTopic); - if (newTopic !== lastTopic) { - this.dispatchEvent(new CustomEvent( - 'topic-changed', {bubbles: true, composed: true})); - } - }); - } - - _showAddTopic(changeRecord, settingTopic) { - const hasTopic = !!changeRecord && - !!changeRecord.base && !!changeRecord.base.topic; - return !hasTopic && !settingTopic; - } - - _showTopicChip(changeRecord, settingTopic) { - const hasTopic = !!changeRecord && - !!changeRecord.base && !!changeRecord.base.topic; - return hasTopic && !settingTopic; - } - - _showCherryPickOf(changeRecord) { - const hasCherryPickOf = !!changeRecord && - !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change && - !!changeRecord.base.cherry_pick_of_patch_set; - return hasCherryPickOf; - } - - _handleHashtagChanged(e) { - const lastHashtag = this.change.hashtag; - if (!this._newHashtag.length) { return; } - const newHashtag = this._newHashtag; - this._newHashtag = ''; - this.$.restAPI.setChangeHashtag( - this.change._number, {add: [newHashtag]}).then(newHashtag => { - this.set(['change', 'hashtags'], newHashtag); - if (newHashtag !== lastHashtag) { - this.dispatchEvent( - new CustomEvent('hashtag-changed', { - bubbles: true, composed: true})); - } - }); - } - - _computeTopicReadOnly(mutable, change) { - return !mutable || - !change || - !change.actions || - !change.actions.topic || - !change.actions.topic.enabled; - } - - _computeHashtagReadOnly(mutable, change) { - return !mutable || - !change || - !change.actions || - !change.actions.hashtags || - !change.actions.hashtags.enabled; - } - - _computeAssigneeReadOnly(mutable, change) { - return !mutable || - !change || - !change.actions || - !change.actions.assignee || - !change.actions.assignee.enabled; - } - - _computeTopicPlaceholder(_topicReadOnly) { - // Action items in Material Design are uppercase -- placeholder label text - // is sentence case. - return _topicReadOnly ? 'No topic' : 'ADD TOPIC'; - } - - _computeHashtagPlaceholder(_hashtagReadOnly) { - return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE; - } - - _computeShowRequirements(change) { - if (change.status !== this.ChangeStatus.NEW) { - // TODO(maximeg) change this to display the stored - // requirements, once it is implemented server-side. - return false; - } - const hasRequirements = !!change.requirements && - Object.keys(change.requirements).length > 0; - const hasLabels = !!change.labels && - Object.keys(change.labels).length > 0; - return hasRequirements || hasLabels || !!change.work_in_progress; - } - - /** - * @return {?Gerrit.PushCertificateValidation} object representing data for - * the push validation. - */ - _computePushCertificateValidation(serverConfig, change) { - if (!change || !serverConfig || !serverConfig.receive || - !serverConfig.receive.enable_signed_push) { - return null; - } - const rev = change.revisions[change.current_revision]; - if (!rev.push_certificate || !rev.push_certificate.key) { - return { - class: 'help', - icon: 'gr-icons:help', - message: 'This patch set was created without a push certificate', - }; - } - - const key = rev.push_certificate.key; - switch (key.status) { - case CertificateStatus.BAD: - return { - class: 'invalid', - icon: 'gr-icons:close', - message: this._problems('Push certificate is invalid', key), - }; - case CertificateStatus.OK: - return { - class: 'notTrusted', - icon: 'gr-icons:info', - message: this._problems( - 'Push certificate is valid, but key is not trusted', key), - }; - case CertificateStatus.TRUSTED: - return { - class: 'trusted', - icon: 'gr-icons:check', - message: this._problems( - 'Push certificate is valid and key is trusted', key), - }; - default: - throw new Error(`unknown certificate status: ${key.status}`); - } - } - - _problems(msg, key) { - if (!key || !key.problems || key.problems.length === 0) { - return msg; - } - - return [msg + ':'].concat(key.problems).join('\n'); - } - - _computeShowRepoBranchTogether(repo, branch) { - return !!repo && !!branch && repo.length + branch.length < 40; - } - - _computeProjectUrl(project) { - return Gerrit.Nav.getUrlForProjectChanges(project); - } - - _computeBranchUrl(project, branch) { - if (!this.change || !this.change.status) return ''; - return Gerrit.Nav.getUrlForBranch(branch, project, - this.change.status == this.ChangeStatus.NEW ? 'open' : - this.change.status.toLowerCase()); - } - - _computeCherryPickOfUrl(change, patchset, project) { - return Gerrit.Nav.getUrlForChangeById(change, project, patchset); - } - - _computeTopicUrl(topic) { - return Gerrit.Nav.getUrlForTopic(topic); - } - - _computeHashtagUrl(hashtag) { - return Gerrit.Nav.getUrlForHashtag(hashtag); - } - - _handleTopicRemoved(e) { - const target = Polymer.dom(e).rootTarget; - target.disabled = true; - this.$.restAPI.setChangeTopic(this.change._number, null) - .then(() => { - target.disabled = false; - this.set(['change', 'topic'], ''); - this.dispatchEvent( - new CustomEvent('topic-changed', - {bubbles: true, composed: true})); - }) - .catch(err => { - target.disabled = false; - return; - }); - } - - _handleHashtagRemoved(e) { - e.preventDefault(); - const target = Polymer.dom(e).rootTarget; - target.disabled = true; - this.$.restAPI.setChangeHashtag(this.change._number, - {remove: [target.text]}) - .then(newHashtag => { - target.disabled = false; - this.set(['change', 'hashtags'], newHashtag); - }) - .catch(err => { - target.disabled = false; - return; - }); - } - - _computeIsWip(change) { - return !!change.work_in_progress; - } - - _computeShowRoleClass(change, role) { - return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay'; - } - - /** - * Get the user with the specified role on the change. Returns null if the - * user with that role is the same as the owner. - * - * @param {!Object} change - * @param {string} role One of the values from _CHANGE_ROLE - * @return {Object|null} either an accound or null. - */ - _getNonOwnerRole(change, role) { - if (!change || !change.current_revision || - !change.revisions[change.current_revision]) { - return null; - } - - const rev = change.revisions[change.current_revision]; - if (!rev) { return null; } - - if (role === this._CHANGE_ROLE.UPLOADER && - rev.uploader && - change.owner._account_id !== rev.uploader._account_id) { - return rev.uploader; - } - - if (role === this._CHANGE_ROLE.AUTHOR && - rev.commit && rev.commit.author && - change.owner.email !== rev.commit.author.email) { - return rev.commit.author; - } - - if (role === this._CHANGE_ROLE.COMMITTER && - rev.commit && rev.commit.committer && - change.owner.email !== rev.commit.committer.email) { - return rev.commit.committer; - } - - return null; - } - - _computeParents(change) { - if (!change || !change.current_revision || - !change.revisions[change.current_revision] || - !change.revisions[change.current_revision].commit) { - return undefined; - } - return change.revisions[change.current_revision].commit.parents; - } - - _computeParentsLabel(parents) { - return parents && parents.length > 1 ? 'Parents' : 'Parent'; - } - - _computeParentListClass(parents, parentIsCurrent) { - // Undefined check for polymer 2 - if (parents === undefined || parentIsCurrent === undefined) { - return ''; - } - - return [ - 'parentList', - parents && parents.length > 1 ? 'merge' : 'nonMerge', - parentIsCurrent ? 'current' : 'notCurrent', - ].join(' '); - } - - _computeIsMutable(account) { - return !!Object.keys(account).length; - } - - editTopic() { - if (this._topicReadOnly || this.change.topic) { return; } - // Cannot use `this.$.ID` syntax because the element exists inside of a - // dom-if. - this.shadowRoot.querySelector('.topicEditableLabel').open(); - } - - _getReviewerSuggestionsProvider(change) { - const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, - change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY); - provider.init(); - return provider; + _assigneeChanged(assigneeRecord) { + if (!this.change) { return; } + const assignee = assigneeRecord.base; + if (assignee.length) { + const acct = assignee[0]; + if (this.change.assignee && + acct._account_id === this.change.assignee._account_id) { return; } + this.set(['change', 'assignee'], acct); + this.$.restAPI.setAssignee(this.change._number, acct._account_id); + } else { + if (!this.change.assignee) { return; } + this.set(['change', 'assignee'], undefined); + this.$.restAPI.deleteAssignee(this.change._number); } } - customElements.define(GrChangeMetadata.is, GrChangeMetadata); -})(); + _computeHideStrategy(change) { + return !this.changeIsOpen(change); + } + + /** + * @param {Object} commitInfo + * @return {?Array} If array is empty, returns null instead so + * an existential check can be used to hide or show the webLinks + * section. + */ + _computeWebLinks(commitInfo, serverConfig) { + if (!commitInfo) { return null; } + const weblinks = Gerrit.Nav.getChangeWeblinks( + this.change ? this.change.repo : '', + commitInfo.commit, + { + weblinks: commitInfo.web_links, + config: serverConfig, + }); + return weblinks.length ? weblinks : null; + } + + _computeStrategy(change) { + return SubmitTypeLabel[change.submit_type]; + } + + _computeLabelNames(labels) { + return Object.keys(labels).sort(); + } + + _handleTopicChanged(e, topic) { + const lastTopic = this.change.topic; + if (!topic.length) { topic = null; } + this._settingTopic = true; + this.$.restAPI.setChangeTopic(this.change._number, topic) + .then(newTopic => { + this._settingTopic = false; + this.set(['change', 'topic'], newTopic); + if (newTopic !== lastTopic) { + this.dispatchEvent(new CustomEvent( + 'topic-changed', {bubbles: true, composed: true})); + } + }); + } + + _showAddTopic(changeRecord, settingTopic) { + const hasTopic = !!changeRecord && + !!changeRecord.base && !!changeRecord.base.topic; + return !hasTopic && !settingTopic; + } + + _showTopicChip(changeRecord, settingTopic) { + const hasTopic = !!changeRecord && + !!changeRecord.base && !!changeRecord.base.topic; + return hasTopic && !settingTopic; + } + + _showCherryPickOf(changeRecord) { + const hasCherryPickOf = !!changeRecord && + !!changeRecord.base && !!changeRecord.base.cherry_pick_of_change && + !!changeRecord.base.cherry_pick_of_patch_set; + return hasCherryPickOf; + } + + _handleHashtagChanged(e) { + const lastHashtag = this.change.hashtag; + if (!this._newHashtag.length) { return; } + const newHashtag = this._newHashtag; + this._newHashtag = ''; + this.$.restAPI.setChangeHashtag( + this.change._number, {add: [newHashtag]}).then(newHashtag => { + this.set(['change', 'hashtags'], newHashtag); + if (newHashtag !== lastHashtag) { + this.dispatchEvent( + new CustomEvent('hashtag-changed', { + bubbles: true, composed: true})); + } + }); + } + + _computeTopicReadOnly(mutable, change) { + return !mutable || + !change || + !change.actions || + !change.actions.topic || + !change.actions.topic.enabled; + } + + _computeHashtagReadOnly(mutable, change) { + return !mutable || + !change || + !change.actions || + !change.actions.hashtags || + !change.actions.hashtags.enabled; + } + + _computeAssigneeReadOnly(mutable, change) { + return !mutable || + !change || + !change.actions || + !change.actions.assignee || + !change.actions.assignee.enabled; + } + + _computeTopicPlaceholder(_topicReadOnly) { + // Action items in Material Design are uppercase -- placeholder label text + // is sentence case. + return _topicReadOnly ? 'No topic' : 'ADD TOPIC'; + } + + _computeHashtagPlaceholder(_hashtagReadOnly) { + return _hashtagReadOnly ? '' : HASHTAG_ADD_MESSAGE; + } + + _computeShowRequirements(change) { + if (change.status !== this.ChangeStatus.NEW) { + // TODO(maximeg) change this to display the stored + // requirements, once it is implemented server-side. + return false; + } + const hasRequirements = !!change.requirements && + Object.keys(change.requirements).length > 0; + const hasLabels = !!change.labels && + Object.keys(change.labels).length > 0; + return hasRequirements || hasLabels || !!change.work_in_progress; + } + + /** + * @return {?Gerrit.PushCertificateValidation} object representing data for + * the push validation. + */ + _computePushCertificateValidation(serverConfig, change) { + if (!change || !serverConfig || !serverConfig.receive || + !serverConfig.receive.enable_signed_push) { + return null; + } + const rev = change.revisions[change.current_revision]; + if (!rev.push_certificate || !rev.push_certificate.key) { + return { + class: 'help', + icon: 'gr-icons:help', + message: 'This patch set was created without a push certificate', + }; + } + + const key = rev.push_certificate.key; + switch (key.status) { + case CertificateStatus.BAD: + return { + class: 'invalid', + icon: 'gr-icons:close', + message: this._problems('Push certificate is invalid', key), + }; + case CertificateStatus.OK: + return { + class: 'notTrusted', + icon: 'gr-icons:info', + message: this._problems( + 'Push certificate is valid, but key is not trusted', key), + }; + case CertificateStatus.TRUSTED: + return { + class: 'trusted', + icon: 'gr-icons:check', + message: this._problems( + 'Push certificate is valid and key is trusted', key), + }; + default: + throw new Error(`unknown certificate status: ${key.status}`); + } + } + + _problems(msg, key) { + if (!key || !key.problems || key.problems.length === 0) { + return msg; + } + + return [msg + ':'].concat(key.problems).join('\n'); + } + + _computeShowRepoBranchTogether(repo, branch) { + return !!repo && !!branch && repo.length + branch.length < 40; + } + + _computeProjectUrl(project) { + return Gerrit.Nav.getUrlForProjectChanges(project); + } + + _computeBranchUrl(project, branch) { + if (!this.change || !this.change.status) return ''; + return Gerrit.Nav.getUrlForBranch(branch, project, + this.change.status == this.ChangeStatus.NEW ? 'open' : + this.change.status.toLowerCase()); + } + + _computeCherryPickOfUrl(change, patchset, project) { + return Gerrit.Nav.getUrlForChangeById(change, project, patchset); + } + + _computeTopicUrl(topic) { + return Gerrit.Nav.getUrlForTopic(topic); + } + + _computeHashtagUrl(hashtag) { + return Gerrit.Nav.getUrlForHashtag(hashtag); + } + + _handleTopicRemoved(e) { + const target = dom(e).rootTarget; + target.disabled = true; + this.$.restAPI.setChangeTopic(this.change._number, null) + .then(() => { + target.disabled = false; + this.set(['change', 'topic'], ''); + this.dispatchEvent( + new CustomEvent('topic-changed', + {bubbles: true, composed: true})); + }) + .catch(err => { + target.disabled = false; + return; + }); + } + + _handleHashtagRemoved(e) { + e.preventDefault(); + const target = dom(e).rootTarget; + target.disabled = true; + this.$.restAPI.setChangeHashtag(this.change._number, + {remove: [target.text]}) + .then(newHashtag => { + target.disabled = false; + this.set(['change', 'hashtags'], newHashtag); + }) + .catch(err => { + target.disabled = false; + return; + }); + } + + _computeIsWip(change) { + return !!change.work_in_progress; + } + + _computeShowRoleClass(change, role) { + return this._getNonOwnerRole(change, role) ? '' : 'hideDisplay'; + } + + /** + * Get the user with the specified role on the change. Returns null if the + * user with that role is the same as the owner. + * + * @param {!Object} change + * @param {string} role One of the values from _CHANGE_ROLE + * @return {Object|null} either an accound or null. + */ + _getNonOwnerRole(change, role) { + if (!change || !change.current_revision || + !change.revisions[change.current_revision]) { + return null; + } + + const rev = change.revisions[change.current_revision]; + if (!rev) { return null; } + + if (role === this._CHANGE_ROLE.UPLOADER && + rev.uploader && + change.owner._account_id !== rev.uploader._account_id) { + return rev.uploader; + } + + if (role === this._CHANGE_ROLE.AUTHOR && + rev.commit && rev.commit.author && + change.owner.email !== rev.commit.author.email) { + return rev.commit.author; + } + + if (role === this._CHANGE_ROLE.COMMITTER && + rev.commit && rev.commit.committer && + change.owner.email !== rev.commit.committer.email) { + return rev.commit.committer; + } + + return null; + } + + _computeParents(change) { + if (!change || !change.current_revision || + !change.revisions[change.current_revision] || + !change.revisions[change.current_revision].commit) { + return undefined; + } + return change.revisions[change.current_revision].commit.parents; + } + + _computeParentsLabel(parents) { + return parents && parents.length > 1 ? 'Parents' : 'Parent'; + } + + _computeParentListClass(parents, parentIsCurrent) { + // Undefined check for polymer 2 + if (parents === undefined || parentIsCurrent === undefined) { + return ''; + } + + return [ + 'parentList', + parents && parents.length > 1 ? 'merge' : 'nonMerge', + parentIsCurrent ? 'current' : 'notCurrent', + ].join(' '); + } + + _computeIsMutable(account) { + return !!Object.keys(account).length; + } + + editTopic() { + if (this._topicReadOnly || this.change.topic) { return; } + // Cannot use `this.$.ID` syntax because the element exists inside of a + // dom-if. + this.shadowRoot.querySelector('.topicEditableLabel').open(); + } + + _getReviewerSuggestionsProvider(change) { + const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, + change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY); + provider.init(); + return provider; + } +} + +customElements.define(GrChangeMetadata.is, GrChangeMetadata);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js index ad3b621..786a118 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
@@ -1,48 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../styles/gr-change-metadata-shared-styles.html"> -<link rel="import" href="../../../styles/gr-change-view-integration-shared-styles.html"> -<link rel="import" href="../../../styles/gr-voting-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../plugins/gr-external-style/gr-external-style.html"> -<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html"> -<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-change-requirements/gr-change-requirements.html"> -<link rel="import" href="../gr-commit-info/gr-commit-info.html"> -<link rel="import" href="../gr-reviewer-list/gr-reviewer-list.html"> -<link rel="import" href="../../shared/gr-account-list/gr-account-list.html"> -<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script> -<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script> - -<dom-module id="gr-change-metadata"> - <template> +export const htmlTemplate = html` <style include="gr-change-metadata-shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -128,9 +102,7 @@ <section> <span class="title">Updated</span> <span class="value"> - <gr-date-formatter - has-tooltip - date-str="[[change.updated]]"></gr-date-formatter> + <gr-date-formatter has-tooltip="" date-str="[[change.updated]]"></gr-date-formatter> </span> </section> <section> @@ -138,92 +110,65 @@ <span class="value"> <gr-account-link account="[[change.owner]]"></gr-account-link> <template is="dom-if" if="[[_pushCertificateValidation]]"> - <gr-tooltip-content - has-tooltip - title$="[[_pushCertificateValidation.message]]"> - <iron-icon - class$="icon [[_pushCertificateValidation.class]]" - icon="[[_pushCertificateValidation.icon]]"> + <gr-tooltip-content has-tooltip="" title\$="[[_pushCertificateValidation.message]]"> + <iron-icon class\$="icon [[_pushCertificateValidation.class]]" icon="[[_pushCertificateValidation.icon]]"> </iron-icon> </gr-tooltip-content> </template> </span> </section> - <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.UPLOADER)]]"> + <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.UPLOADER)]]"> <span class="title">Uploader</span> <span class="value"> - <gr-account-link - account="[[_getNonOwnerRole(change, _CHANGE_ROLE.UPLOADER)]]" - ></gr-account-link> + <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.UPLOADER)]]"></gr-account-link> </span> </section> - <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.AUTHOR)]]"> + <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.AUTHOR)]]"> <span class="title">Author</span> <span class="value"> - <gr-account-link - account="[[_getNonOwnerRole(change, _CHANGE_ROLE.AUTHOR)]]" - ></gr-account-link> + <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.AUTHOR)]]"></gr-account-link> </span> </section> - <section class$="[[_computeShowRoleClass(change, _CHANGE_ROLE.COMMITTER)]]"> + <section class\$="[[_computeShowRoleClass(change, _CHANGE_ROLE.COMMITTER)]]"> <span class="title">Committer</span> <span class="value"> - <gr-account-link - account="[[_getNonOwnerRole(change, _CHANGE_ROLE.COMMITTER)]]" - ></gr-account-link> + <gr-account-link account="[[_getNonOwnerRole(change, _CHANGE_ROLE.COMMITTER)]]"></gr-account-link> </span> </section> <section class="assignee"> <span class="title">Assignee</span> <span class="value"> - <gr-account-list - id="assigneeValue" - placeholder="Set assignee..." - max-count="1" - skip-suggest-on-empty - accounts="{{_assignee}}" - readonly="[[_computeAssigneeReadOnly(_mutable, change)]]" - suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"> + <gr-account-list id="assigneeValue" placeholder="Set assignee..." max-count="1" skip-suggest-on-empty="" accounts="{{_assignee}}" readonly="[[_computeAssigneeReadOnly(_mutable, change)]]" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"> </gr-account-list> </span> </section> <section> <span class="title">Reviewers</span> <span class="value"> - <gr-reviewer-list - change="{{change}}" - mutable="[[_mutable]]" - reviewers-only - max-reviewers-displayed="3"></gr-reviewer-list> + <gr-reviewer-list change="{{change}}" mutable="[[_mutable]]" reviewers-only="" max-reviewers-displayed="3"></gr-reviewer-list> </span> </section> <section> <span class="title">CC</span> <span class="value"> - <gr-reviewer-list - change="{{change}}" - mutable="[[_mutable]]" - ccs-only - max-reviewers-displayed="3"></gr-reviewer-list> + <gr-reviewer-list change="{{change}}" mutable="[[_mutable]]" ccs-only="" max-reviewers-displayed="3"></gr-reviewer-list> </span> </section> - <template is="dom-if" - if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]"> + <template is="dom-if" if="[[_computeShowRepoBranchTogether(change.project, change.branch)]]"> <section> <span class="title">Repo / Branch</span> <span class="value"> - <a href$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a> + <a href\$="[[_computeProjectUrl(change.project)]]">[[change.project]]</a> / - <a href$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a> + <a href\$="[[_computeBranchUrl(change.project, change.branch)]]">[[change.branch]]</a> </span> </section> </template> - <template is="dom-if" - if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]"> + <template is="dom-if" if="[[!_computeShowRepoBranchTogether(change.project, change.branch)]]"> <section> <span class="title">Repo</span> <span class="value"> - <a href$="[[_computeProjectUrl(change.project)]]"> + <a href\$="[[_computeProjectUrl(change.project)]]"> <gr-limited-text limit="40" text="[[change.project]]"></gr-limited-text> </a> </span> @@ -231,7 +176,7 @@ <section> <span class="title">Branch</span> <span class="value"> - <a href$="[[_computeBranchUrl(change.project, change.branch)]]"> + <a href\$="[[_computeBranchUrl(change.project, change.branch)]]"> <gr-limited-text limit="40" text="[[change.branch]]"></gr-limited-text> </a> </span> @@ -240,18 +185,11 @@ <section> <span class="title">[[_computeParentsLabel(_currentParents)]]</span> <span class="value"> - <ol class$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]"> + <ol class\$="[[_computeParentListClass(_currentParents, parentIsCurrent)]]"> <template is="dom-repeat" items="[[_currentParents]]" as="parent"> <li> - <gr-commit-info - change="[[change]]" - commit-info="[[parent]]" - server-config="[[serverConfig]]"></gr-commit-info> - <gr-tooltip-content - id="parentNotCurrentMessage" - has-tooltip - show-icon - title$="[[_notCurrentMessage]]"></gr-tooltip-content> + <gr-commit-info change="[[change]]" commit-info="[[parent]]" server-config="[[serverConfig]]"></gr-commit-info> + <gr-tooltip-content id="parentNotCurrentMessage" has-tooltip="" show-icon="" title\$="[[_notCurrentMessage]]"></gr-tooltip-content> </li> </template> </ol> @@ -260,27 +198,11 @@ <section class="topic"> <span class="title">Topic</span> <span class="value"> - <template - is="dom-if" - if="[[_showTopicChip(change.*, _settingTopic)]]"> - <gr-linked-chip - text="[[change.topic]]" - limit="40" - href="[[_computeTopicUrl(change.topic)]]" - removable="[[!_topicReadOnly]]" - on-remove="_handleTopicRemoved"></gr-linked-chip> + <template is="dom-if" if="[[_showTopicChip(change.*, _settingTopic)]]"> + <gr-linked-chip text="[[change.topic]]" limit="40" href="[[_computeTopicUrl(change.topic)]]" removable="[[!_topicReadOnly]]" on-remove="_handleTopicRemoved"></gr-linked-chip> </template> - <template - is="dom-if" - if="[[_showAddTopic(change.*, _settingTopic)]]"> - <gr-editable-label - class="topicEditableLabel" - label-text="Add a topic" - value="[[change.topic]]" - max-length="1024" - placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" - read-only="[[_topicReadOnly]]" - on-changed="_handleTopicChanged"></gr-editable-label> + <template is="dom-if" if="[[_showAddTopic(change.*, _settingTopic)]]"> + <gr-editable-label class="topicEditableLabel" label-text="Add a topic" value="[[change.topic]]" max-length="1024" placeholder="[[_computeTopicPlaceholder(_topicReadOnly)]]" read-only="[[_topicReadOnly]]" on-changed="_handleTopicChanged"></gr-editable-label> </template> </span> </section> @@ -288,16 +210,14 @@ <section> <span class="title">Cherry pick of</span> <span class="value"> - <a href$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]"> - <gr-limited-text - text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]" - limit="40"> + <a href\$="[[_computeCherryPickOfUrl(change.cherry_pick_of_change, change.cherry_pick_of_patch_set, change.project)]]"> + <gr-limited-text text="[[change.cherry_pick_of_change]],[[change.cherry_pick_of_patch_set]]" limit="40"> </gr-limited-text> </a> </span> </section> </template> - <section class="strategy" hidden$="[[_computeHideStrategy(change)]]" hidden> + <section class="strategy" hidden\$="[[_computeHideStrategy(change)]]" hidden=""> <span class="title">Strategy</span> <span class="value">[[_computeStrategy(change)]]</span> </section> @@ -305,36 +225,21 @@ <span class="title">Hashtags</span> <span class="value"> <template is="dom-repeat" items="[[change.hashtags]]"> - <gr-linked-chip - class="hashtagChip" - text="[[item]]" - href="[[_computeHashtagUrl(item)]]" - removable="[[!_hashtagReadOnly]]" - on-remove="_handleHashtagRemoved"> + <gr-linked-chip class="hashtagChip" text="[[item]]" href="[[_computeHashtagUrl(item)]]" removable="[[!_hashtagReadOnly]]" on-remove="_handleHashtagRemoved"> </gr-linked-chip> </template> <template is="dom-if" if="[[!_hashtagReadOnly]]"> - <gr-editable-label - uppercase - label-text="Add a hashtag" - value="{{_newHashtag}}" - placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]" - read-only="[[_hashtagReadOnly]]" - on-changed="_handleHashtagChanged"></gr-editable-label> + <gr-editable-label uppercase="" label-text="Add a hashtag" value="{{_newHashtag}}" placeholder="[[_computeHashtagPlaceholder(_hashtagReadOnly)]]" read-only="[[_hashtagReadOnly]]" on-changed="_handleHashtagChanged"></gr-editable-label> </template> </span> </section> <div class="separatedSection"> - <gr-change-requirements - change="{{change}}" - account="[[account]]" - mutable="[[_mutable]]"></gr-change-requirements> + <gr-change-requirements change="{{change}}" account="[[account]]" mutable="[[_mutable]]"></gr-change-requirements> </div> - <section id="webLinks" hidden$="[[!_computeWebLinks(commitInfo, serverConfig)]]"> + <section id="webLinks" hidden\$="[[!_computeWebLinks(commitInfo, serverConfig)]]"> <span class="title">Links</span> <span class="value"> - <template is="dom-repeat" - items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link"> + <template is="dom-repeat" items="[[_computeWebLinks(commitInfo, serverConfig)]]" as="link"> <a href="[[link.url]]" class="webLink" rel="noopener" target="_blank"> [[link.name]] </a> @@ -348,6 +253,4 @@ </gr-endpoint-decorator> </gr-external-style> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-change-metadata.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html index 055f3f0..5fe53f8d 100644 --- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-metadata</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../core/gr-router/gr-router.html"> -<link rel="import" href="gr-change-metadata.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../core/gr-router/gr-router.js"></script> +<script type="module" src="./gr-change-metadata.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../core/gr-router/gr-router.js'; +import './gr-change-metadata.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,733 +42,736 @@ </template> </test-fixture> -<script> - suite('gr-change-metadata tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../core/gr-router/gr-router.js'; +import './gr-change-metadata.js'; +suite('gr-change-metadata tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-endpoint-decorator', { + _import: sandbox.stub().returns(Promise.resolve()), + }); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, + }); + + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('computed fields', () => { + assert.isFalse(element._computeHideStrategy({status: 'NEW'})); + assert.isTrue(element._computeHideStrategy({status: 'MERGED'})); + assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'})); + assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}), + 'Cherry Pick'); + assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}), + 'Rebase Always'); + }); + + test('computed fields requirements', () => { + assert.isFalse(element._computeShowRequirements({status: 'MERGED'})); + assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'})); + + // No labels and no requirements: submit status is useless + assert.isFalse(element._computeShowRequirements({ + status: 'NEW', + labels: {}, + })); + + // Work in Progress: submit status should be present + assert.isTrue(element._computeShowRequirements({ + status: 'NEW', + labels: {}, + work_in_progress: true, + })); + + // We have at least one reason to display Submit Status + assert.isTrue(element._computeShowRequirements({ + status: 'NEW', + labels: { + Verified: { + approved: false, + }, + }, + requirements: [], + })); + assert.isTrue(element._computeShowRequirements({ + status: 'NEW', + labels: {}, + requirements: [{ + fallback_text: 'Resolve all comments', + status: 'OK', + }], + })); + }); + + test('show strategy for open change', () => { + element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}}; + flushAsynchronousOperations(); + const strategy = element.shadowRoot + .querySelector('.strategy'); + assert.ok(strategy); + assert.isFalse(strategy.hasAttribute('hidden')); + assert.equal(strategy.children[1].innerHTML, 'Cherry Pick'); + }); + + test('hide strategy for closed change', () => { + element.change = {status: 'MERGED', labels: {}}; + flushAsynchronousOperations(); + assert.isTrue(element.shadowRoot + .querySelector('.strategy').hasAttribute('hidden')); + }); + + test('weblinks use Gerrit.Nav interface', () => { + const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks') + .returns([{name: 'stubb', url: '#s'}]); + element.commitInfo = {}; + element.serverConfig = {}; + flushAsynchronousOperations(); + const webLinks = element.$.webLinks; + assert.isTrue(weblinksStub.called); + assert.isFalse(webLinks.hasAttribute('hidden')); + assert.equal(element._computeWebLinks(element.commitInfo).length, 1); + }); + + test('weblinks hidden when no weblinks', () => { + element.commitInfo = {}; + element.serverConfig = {}; + flushAsynchronousOperations(); + const webLinks = element.$.webLinks; + assert.isTrue(webLinks.hasAttribute('hidden')); + }); + + test('weblinks hidden when only gitiles weblink', () => { + element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]}; + element.serverConfig = {}; + flushAsynchronousOperations(); + const webLinks = element.$.webLinks; + assert.isTrue(webLinks.hasAttribute('hidden')); + assert.equal(element._computeWebLinks(element.commitInfo), null); + }); + + test('weblinks hidden when sole weblink is set as primary', () => { + const browser = 'browser'; + element.commitInfo = {web_links: [{name: browser, url: '#'}]}; + element.serverConfig = { + gerrit: { + primary_weblink_name: browser, + }, + }; + flushAsynchronousOperations(); + const webLinks = element.$.webLinks; + assert.isTrue(webLinks.hasAttribute('hidden')); + }); + + test('weblinks are visible when other weblinks', () => { + const router = document.createElement('gr-router'); + sandbox.stub(Gerrit.Nav, '_generateWeblinks', + router._generateWeblinks.bind(router)); + + element.commitInfo = {web_links: [{name: 'test', url: '#'}]}; + flushAsynchronousOperations(); + const webLinks = element.$.webLinks; + assert.isFalse(webLinks.hasAttribute('hidden')); + assert.equal(element._computeWebLinks(element.commitInfo).length, 1); + // With two non-gitiles weblinks, there are two returned. + element.commitInfo = { + web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]}; + assert.equal(element._computeWebLinks(element.commitInfo).length, 2); + }); + + test('weblinks are visible when gitiles and other weblinks', () => { + const router = document.createElement('gr-router'); + sandbox.stub(Gerrit.Nav, '_generateWeblinks', + router._generateWeblinks.bind(router)); + + element.commitInfo = { + web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]}; + flushAsynchronousOperations(); + const webLinks = element.$.webLinks; + assert.isFalse(webLinks.hasAttribute('hidden')); + // Only the non-gitiles weblink is returned. + assert.equal(element._computeWebLinks(element.commitInfo).length, 1); + }); + + suite('_getNonOwnerRole', () => { + let change; setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-endpoint-decorator', { - _import: sandbox.stub().returns(Promise.resolve()), - }); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getLoggedIn() { return Promise.resolve(false); }, - }); - - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('computed fields', () => { - assert.isFalse(element._computeHideStrategy({status: 'NEW'})); - assert.isTrue(element._computeHideStrategy({status: 'MERGED'})); - assert.isTrue(element._computeHideStrategy({status: 'ABANDONED'})); - assert.equal(element._computeStrategy({submit_type: 'CHERRY_PICK'}), - 'Cherry Pick'); - assert.equal(element._computeStrategy({submit_type: 'REBASE_ALWAYS'}), - 'Rebase Always'); - }); - - test('computed fields requirements', () => { - assert.isFalse(element._computeShowRequirements({status: 'MERGED'})); - assert.isFalse(element._computeShowRequirements({status: 'ABANDONED'})); - - // No labels and no requirements: submit status is useless - assert.isFalse(element._computeShowRequirements({ - status: 'NEW', - labels: {}, - })); - - // Work in Progress: submit status should be present - assert.isTrue(element._computeShowRequirements({ - status: 'NEW', - labels: {}, - work_in_progress: true, - })); - - // We have at least one reason to display Submit Status - assert.isTrue(element._computeShowRequirements({ - status: 'NEW', - labels: { - Verified: { - approved: false, - }, - }, - requirements: [], - })); - assert.isTrue(element._computeShowRequirements({ - status: 'NEW', - labels: {}, - requirements: [{ - fallback_text: 'Resolve all comments', - status: 'OK', - }], - })); - }); - - test('show strategy for open change', () => { - element.change = {status: 'NEW', submit_type: 'CHERRY_PICK', labels: {}}; - flushAsynchronousOperations(); - const strategy = element.shadowRoot - .querySelector('.strategy'); - assert.ok(strategy); - assert.isFalse(strategy.hasAttribute('hidden')); - assert.equal(strategy.children[1].innerHTML, 'Cherry Pick'); - }); - - test('hide strategy for closed change', () => { - element.change = {status: 'MERGED', labels: {}}; - flushAsynchronousOperations(); - assert.isTrue(element.shadowRoot - .querySelector('.strategy').hasAttribute('hidden')); - }); - - test('weblinks use Gerrit.Nav interface', () => { - const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks') - .returns([{name: 'stubb', url: '#s'}]); - element.commitInfo = {}; - element.serverConfig = {}; - flushAsynchronousOperations(); - const webLinks = element.$.webLinks; - assert.isTrue(weblinksStub.called); - assert.isFalse(webLinks.hasAttribute('hidden')); - assert.equal(element._computeWebLinks(element.commitInfo).length, 1); - }); - - test('weblinks hidden when no weblinks', () => { - element.commitInfo = {}; - element.serverConfig = {}; - flushAsynchronousOperations(); - const webLinks = element.$.webLinks; - assert.isTrue(webLinks.hasAttribute('hidden')); - }); - - test('weblinks hidden when only gitiles weblink', () => { - element.commitInfo = {web_links: [{name: 'gitiles', url: '#'}]}; - element.serverConfig = {}; - flushAsynchronousOperations(); - const webLinks = element.$.webLinks; - assert.isTrue(webLinks.hasAttribute('hidden')); - assert.equal(element._computeWebLinks(element.commitInfo), null); - }); - - test('weblinks hidden when sole weblink is set as primary', () => { - const browser = 'browser'; - element.commitInfo = {web_links: [{name: browser, url: '#'}]}; - element.serverConfig = { - gerrit: { - primary_weblink_name: browser, - }, - }; - flushAsynchronousOperations(); - const webLinks = element.$.webLinks; - assert.isTrue(webLinks.hasAttribute('hidden')); - }); - - test('weblinks are visible when other weblinks', () => { - const router = document.createElement('gr-router'); - sandbox.stub(Gerrit.Nav, '_generateWeblinks', - router._generateWeblinks.bind(router)); - - element.commitInfo = {web_links: [{name: 'test', url: '#'}]}; - flushAsynchronousOperations(); - const webLinks = element.$.webLinks; - assert.isFalse(webLinks.hasAttribute('hidden')); - assert.equal(element._computeWebLinks(element.commitInfo).length, 1); - // With two non-gitiles weblinks, there are two returned. - element.commitInfo = { - web_links: [{name: 'test', url: '#'}, {name: 'test2', url: '#'}]}; - assert.equal(element._computeWebLinks(element.commitInfo).length, 2); - }); - - test('weblinks are visible when gitiles and other weblinks', () => { - const router = document.createElement('gr-router'); - sandbox.stub(Gerrit.Nav, '_generateWeblinks', - router._generateWeblinks.bind(router)); - - element.commitInfo = { - web_links: [{name: 'test', url: '#'}, {name: 'gitiles', url: '#'}]}; - flushAsynchronousOperations(); - const webLinks = element.$.webLinks; - assert.isFalse(webLinks.hasAttribute('hidden')); - // Only the non-gitiles weblink is returned. - assert.equal(element._computeWebLinks(element.commitInfo).length, 1); - }); - - suite('_getNonOwnerRole', () => { - let change; - - setup(() => { - change = { - owner: { - email: 'abc@def', - _account_id: 1019328, - }, - revisions: { - rev1: { - _number: 1, - uploader: { - email: 'ghi@def', - _account_id: 1011123, - }, - commit: { - author: {email: 'jkl@def'}, - committer: {email: 'ghi@def'}, - }, - }, - }, - current_revision: 'rev1', - }; - }); - - suite('role=uploader', () => { - test('_getNonOwnerRole for uploader', () => { - assert.deepEqual( - element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER), - {email: 'ghi@def', _account_id: 1011123}); - }); - - test('_getNonOwnerRole that it does not return uploader', () => { - // Set the uploader email to be the same as the owner. - change.revisions.rev1.uploader._account_id = 1019328; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.UPLOADER)); - }); - - test('_getNonOwnerRole null for uploader with no current rev', () => { - delete change.current_revision; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.UPLOADER)); - }); - - test('_computeShowRoleClass show uploader', () => { - assert.equal(element._computeShowRoleClass( - change, element._CHANGE_ROLE.UPLOADER), ''); - }); - - test('_computeShowRoleClass hide uploader', () => { - // Set the uploader email to be the same as the owner. - change.revisions.rev1.uploader._account_id = 1019328; - assert.equal(element._computeShowRoleClass(change, - element._CHANGE_ROLE.UPLOADER), 'hideDisplay'); - }); - }); - - suite('role=committer', () => { - test('_getNonOwnerRole for committer', () => { - assert.deepEqual( - element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER), - {email: 'ghi@def'}); - }); - - test('_getNonOwnerRole that it does not return committer', () => { - // Set the committer email to be the same as the owner. - change.revisions.rev1.commit.committer.email = 'abc@def'; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.COMMITTER)); - }); - - test('_getNonOwnerRole null for committer with no current rev', () => { - delete change.current_revision; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.COMMITTER)); - }); - - test('_getNonOwnerRole null for committer with no commit', () => { - delete change.revisions.rev1.commit; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.COMMITTER)); - }); - - test('_getNonOwnerRole null for committer with no committer', () => { - delete change.revisions.rev1.commit.committer; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.COMMITTER)); - }); - }); - - suite('role=author', () => { - test('_getNonOwnerRole for author', () => { - assert.deepEqual( - element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR), - {email: 'jkl@def'}); - }); - - test('_getNonOwnerRole that it does not return author', () => { - // Set the author email to be the same as the owner. - change.revisions.rev1.commit.author.email = 'abc@def'; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.AUTHOR)); - }); - - test('_getNonOwnerRole null for author with no current rev', () => { - delete change.current_revision; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.AUTHOR)); - }); - - test('_getNonOwnerRole null for author with no commit', () => { - delete change.revisions.rev1.commit; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.AUTHOR)); - }); - - test('_getNonOwnerRole null for author with no author', () => { - delete change.revisions.rev1.commit.author; - assert.isNull(element._getNonOwnerRole(change, - element._CHANGE_ROLE.AUTHOR)); - }); - }); - }); - - test('Push Certificate Validation test BAD', () => { - const serverConfig = { - receive: { - enable_signed_push: true, - }, - }; - const change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + change = { owner: { + email: 'abc@def', _account_id: 1019328, }, revisions: { rev1: { _number: 1, - push_certificate: { - key: { - status: 'BAD', - problems: [ - 'No public keys found for key ID E5E20E52', - ], - }, + uploader: { + email: 'ghi@def', + _account_id: 1011123, + }, + commit: { + author: {email: 'jkl@def'}, + committer: {email: 'ghi@def'}, }, }, }, current_revision: 'rev1', - status: 'NEW', - labels: {}, - mergeable: true, }; - const result = - element._computePushCertificateValidation(serverConfig, change); - assert.equal(result.message, - 'Push certificate is invalid:\n' + - 'No public keys found for key ID E5E20E52'); - assert.equal(result.icon, 'gr-icons:close'); - assert.equal(result.class, 'invalid'); }); - test('Push Certificate Validation test TRUSTED', () => { - const serverConfig = { - receive: { - enable_signed_push: true, - }, - }; - const change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - owner: { - _account_id: 1019328, - }, - revisions: { - rev1: { - _number: 1, - push_certificate: { - key: { - status: 'TRUSTED', - }, - }, - }, - }, - current_revision: 'rev1', - status: 'NEW', - labels: {}, - mergeable: true, - }; - const result = - element._computePushCertificateValidation(serverConfig, change); - assert.equal(result.message, - 'Push certificate is valid and key is trusted'); - assert.equal(result.icon, 'gr-icons:check'); - assert.equal(result.class, 'trusted'); - }); - - test('Push Certificate Validation is missing test', () => { - const serverConfig = { - receive: { - enable_signed_push: true, - }, - }; - const change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - owner: { - _account_id: 1019328, - }, - revisions: { - rev1: { - _number: 1, - }, - }, - current_revision: 'rev1', - status: 'NEW', - labels: {}, - mergeable: true, - }; - const result = - element._computePushCertificateValidation(serverConfig, change); - assert.equal(result.message, - 'This patch set was created without a push certificate'); - assert.equal(result.icon, 'gr-icons:help'); - assert.equal(result.class, 'help'); - }); - - test('_computeParents', () => { - const parents = [{commit: '123', subject: 'abc'}]; - assert.isUndefined(element._computeParents( - {revisions: {456: {commit: {parents}}}})); - assert.isUndefined(element._computeParents( - {current_revision: '789', revisions: {456: {commit: {parents}}}})); - assert.equal(element._computeParents( - {current_revision: '456', revisions: {456: {commit: {parents}}}}), - parents); - }); - - test('_computeParentsLabel', () => { - const parent = {commit: 'abc123', subject: 'My parent commit'}; - assert.equal(element._computeParentsLabel([parent]), 'Parent'); - assert.equal(element._computeParentsLabel([parent, parent]), - 'Parents'); - }); - - test('_computeParentListClass', () => { - const parent = {commit: 'abc123', subject: 'My parent commit'}; - assert.equal(element._computeParentListClass([parent], true), - 'parentList nonMerge current'); - assert.equal(element._computeParentListClass([parent], false), - 'parentList nonMerge notCurrent'); - assert.equal(element._computeParentListClass([parent, parent], false), - 'parentList merge notCurrent'); - assert.equal(element._computeParentListClass([parent, parent], true), - 'parentList merge current'); - }); - - test('_showAddTopic', () => { - assert.isTrue(element._showAddTopic(null, false)); - assert.isTrue(element._showAddTopic({base: {topic: null}}, false)); - assert.isFalse(element._showAddTopic({base: {topic: null}}, true)); - assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true)); - assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false)); - }); - - test('_showTopicChip', () => { - assert.isFalse(element._showTopicChip(null, false)); - assert.isFalse(element._showTopicChip({base: {topic: null}}, false)); - assert.isFalse(element._showTopicChip({base: {topic: null}}, true)); - assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true)); - assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false)); - }); - - test('_showCherryPickOf', () => { - assert.isFalse(element._showCherryPickOf(null)); - assert.isFalse(element._showCherryPickOf({ - base: { - cherry_pick_of_change: null, - cherry_pick_of_patch_set: null, - }, - })); - assert.isTrue(element._showCherryPickOf({ - base: { - cherry_pick_of_change: 123, - cherry_pick_of_patch_set: 1, - }, - })); - }); - - suite('Topic removal', () => { - let change; - setup(() => { - change = { - _number: 'the number', - actions: { - topic: {enabled: false}, - }, - change_id: 'the id', - topic: 'the topic', - status: 'NEW', - submit_type: 'CHERRY_PICK', - labels: { - test: { - all: [{_account_id: 1, name: 'bojack', value: 1}], - default_value: 0, - values: [], - }, - }, - removable_reviewers: [], - }; + suite('role=uploader', () => { + test('_getNonOwnerRole for uploader', () => { + assert.deepEqual( + element._getNonOwnerRole(change, element._CHANGE_ROLE.UPLOADER), + {email: 'ghi@def', _account_id: 1011123}); }); - test('_computeTopicReadOnly', () => { - let mutable = false; - assert.isTrue(element._computeTopicReadOnly(mutable, change)); - mutable = true; - assert.isTrue(element._computeTopicReadOnly(mutable, change)); - change.actions.topic.enabled = true; - assert.isFalse(element._computeTopicReadOnly(mutable, change)); - mutable = false; - assert.isTrue(element._computeTopicReadOnly(mutable, change)); + test('_getNonOwnerRole that it does not return uploader', () => { + // Set the uploader email to be the same as the owner. + change.revisions.rev1.uploader._account_id = 1019328; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.UPLOADER)); }); - test('topic read only hides delete button', () => { - element.account = {}; - element.change = change; - flushAsynchronousOperations(); - const button = element.shadowRoot - .querySelector('gr-linked-chip').shadowRoot - .querySelector('gr-button'); - assert.isTrue(button.hasAttribute('hidden')); + test('_getNonOwnerRole null for uploader with no current rev', () => { + delete change.current_revision; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.UPLOADER)); }); - test('topic not read only does not hide delete button', () => { - element.account = {test: true}; - change.actions.topic.enabled = true; - element.change = change; - flushAsynchronousOperations(); - const button = element.shadowRoot - .querySelector('gr-linked-chip').shadowRoot - .querySelector('gr-button'); - assert.isFalse(button.hasAttribute('hidden')); + test('_computeShowRoleClass show uploader', () => { + assert.equal(element._computeShowRoleClass( + change, element._CHANGE_ROLE.UPLOADER), ''); + }); + + test('_computeShowRoleClass hide uploader', () => { + // Set the uploader email to be the same as the owner. + change.revisions.rev1.uploader._account_id = 1019328; + assert.equal(element._computeShowRoleClass(change, + element._CHANGE_ROLE.UPLOADER), 'hideDisplay'); }); }); - suite('Hashtag removal', () => { - let change; - setup(() => { - change = { - _number: 'the number', - actions: { - hashtags: {enabled: false}, - }, - change_id: 'the id', - hashtags: ['test-hashtag'], - status: 'NEW', - submit_type: 'CHERRY_PICK', - labels: { - test: { - all: [{_account_id: 1, name: 'bojack', value: 1}], - default_value: 0, - values: [], - }, - }, - removable_reviewers: [], - }; + suite('role=committer', () => { + test('_getNonOwnerRole for committer', () => { + assert.deepEqual( + element._getNonOwnerRole(change, element._CHANGE_ROLE.COMMITTER), + {email: 'ghi@def'}); }); - test('_computeHashtagReadOnly', () => { - flushAsynchronousOperations(); - let mutable = false; - assert.isTrue(element._computeHashtagReadOnly(mutable, change)); - mutable = true; - assert.isTrue(element._computeHashtagReadOnly(mutable, change)); - change.actions.hashtags.enabled = true; - assert.isFalse(element._computeHashtagReadOnly(mutable, change)); - mutable = false; - assert.isTrue(element._computeHashtagReadOnly(mutable, change)); + test('_getNonOwnerRole that it does not return committer', () => { + // Set the committer email to be the same as the owner. + change.revisions.rev1.commit.committer.email = 'abc@def'; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.COMMITTER)); }); - test('hashtag read only hides delete button', () => { - flushAsynchronousOperations(); - element.account = {}; - element.change = change; - flushAsynchronousOperations(); - const button = element.shadowRoot - .querySelector('gr-linked-chip').shadowRoot - .querySelector('gr-button'); - assert.isTrue(button.hasAttribute('hidden')); + test('_getNonOwnerRole null for committer with no current rev', () => { + delete change.current_revision; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.COMMITTER)); }); - test('hashtag not read only does not hide delete button', () => { - flushAsynchronousOperations(); - element.account = {test: true}; - change.actions.hashtags.enabled = true; - element.change = change; - flushAsynchronousOperations(); - const button = element.shadowRoot - .querySelector('gr-linked-chip').shadowRoot - .querySelector('gr-button'); - assert.isFalse(button.hasAttribute('hidden')); + test('_getNonOwnerRole null for committer with no commit', () => { + delete change.revisions.rev1.commit; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.COMMITTER)); + }); + + test('_getNonOwnerRole null for committer with no committer', () => { + delete change.revisions.rev1.commit.committer; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.COMMITTER)); }); }); - suite('remove reviewer votes', () => { - setup(() => { - sandbox.stub(element, '_computeTopicReadOnly').returns(true); - element.change = { - _number: 42, - change_id: 'the id', - actions: [], - topic: 'the topic', - status: 'NEW', - submit_type: 'CHERRY_PICK', - labels: { - test: { - all: [{_account_id: 1, name: 'bojack', value: 1}], - default_value: 0, - values: [], - }, - }, - removable_reviewers: [], - }; - flushAsynchronousOperations(); + suite('role=author', () => { + test('_getNonOwnerRole for author', () => { + assert.deepEqual( + element._getNonOwnerRole(change, element._CHANGE_ROLE.AUTHOR), + {email: 'jkl@def'}); }); - suite('assignee field', () => { - const dummyAccount = { - _account_id: 1, - name: 'bojack', - }; - const change = { - actions: { - assignee: {enabled: false}, - }, - assignee: dummyAccount, - }; - let deleteStub; - let setStub; - - setup(() => { - deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee'); - setStub = sandbox.stub(element.$.restAPI, 'setAssignee'); - }); - - test('changing change recomputes _assignee', () => { - assert.isFalse(!!element._assignee.length); - const change = element.change; - change.assignee = dummyAccount; - element._changeChanged(change); - assert.deepEqual(element._assignee[0], dummyAccount); - }); - - test('modifying _assignee calls API', () => { - assert.isFalse(!!element._assignee.length); - element.set('_assignee', [dummyAccount]); - assert.isTrue(setStub.calledOnce); - assert.deepEqual(element.change.assignee, dummyAccount); - element.set('_assignee', [dummyAccount]); - assert.isTrue(setStub.calledOnce); - element.set('_assignee', []); - assert.isTrue(deleteStub.calledOnce); - assert.equal(element.change.assignee, undefined); - element.set('_assignee', []); - assert.isTrue(deleteStub.calledOnce); - }); - - test('_computeAssigneeReadOnly', () => { - let mutable = false; - assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); - mutable = true; - assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); - change.actions.assignee.enabled = true; - assert.isFalse(element._computeAssigneeReadOnly(mutable, change)); - mutable = false; - assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); - }); + test('_getNonOwnerRole that it does not return author', () => { + // Set the author email to be the same as the owner. + change.revisions.rev1.commit.author.email = 'abc@def'; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.AUTHOR)); }); - test('changing topic', () => { - const newTopic = 'the new topic'; - sandbox.stub(element.$.restAPI, 'setChangeTopic').returns( - Promise.resolve(newTopic)); - element._handleTopicChanged({}, newTopic); - const topicChangedSpy = sandbox.spy(); - element.addEventListener('topic-changed', topicChangedSpy); - assert.isTrue(element.$.restAPI.setChangeTopic.calledWith( - 42, newTopic)); - return element.$.restAPI.setChangeTopic.lastCall.returnValue - .then(() => { - assert.equal(element.change.topic, newTopic); - assert.isTrue(topicChangedSpy.called); - }); + test('_getNonOwnerRole null for author with no current rev', () => { + delete change.current_revision; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.AUTHOR)); }); - test('topic removal', () => { - sandbox.stub(element.$.restAPI, 'setChangeTopic').returns( - Promise.resolve()); - const chip = element.shadowRoot - .querySelector('gr-linked-chip'); - const remove = chip.$.remove; - const topicChangedSpy = sandbox.spy(); - element.addEventListener('topic-changed', topicChangedSpy); - MockInteractions.tap(remove); - assert.isTrue(chip.disabled); - assert.isTrue(element.$.restAPI.setChangeTopic.calledWith( - 42, null)); - return element.$.restAPI.setChangeTopic.lastCall.returnValue - .then(() => { - assert.isFalse(chip.disabled); - assert.equal(element.change.topic, ''); - assert.isTrue(topicChangedSpy.called); - }); + test('_getNonOwnerRole null for author with no commit', () => { + delete change.revisions.rev1.commit; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.AUTHOR)); }); - test('changing hashtag', () => { - flushAsynchronousOperations(); - element._newHashtag = 'new hashtag'; - const newHashtag = ['new hashtag']; - sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns( - Promise.resolve(newHashtag)); - element._handleHashtagChanged({}, 'new hashtag'); - assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith( - 42, {add: ['new hashtag']})); - return element.$.restAPI.setChangeHashtag.lastCall.returnValue - .then(() => { - assert.equal(element.change.hashtags, newHashtag); - }); - }); - }); - - test('editTopic', () => { - element.account = {test: true}; - element.change = {actions: {topic: {enabled: true}}}; - flushAsynchronousOperations(); - - const label = element.shadowRoot - .querySelector('.topicEditableLabel'); - assert.ok(label); - sandbox.stub(label, 'open'); - element.editTopic(); - flushAsynchronousOperations(); - - assert.isTrue(label.open.called); - }); - - suite('plugin endpoints', () => { - test('endpoint params', done => { - element.change = {labels: {}}; - element.revision = {}; - let hookEl; - let plugin; - Gerrit.install( - p => { - plugin = p; - plugin.hook('change-metadata-item').getLastAttached() - .then(el => hookEl = el); - }, - '0.1', - 'http://some/plugins/url.html'); - Gerrit._loadPlugins([]); - flush(() => { - assert.strictEqual(hookEl.plugin, plugin); - assert.strictEqual(hookEl.change, element.change); - assert.strictEqual(hookEl.revision, element.revision); - done(); - }); + test('_getNonOwnerRole null for author with no author', () => { + delete change.revisions.rev1.commit.author; + assert.isNull(element._getNonOwnerRole(change, + element._CHANGE_ROLE.AUTHOR)); }); }); }); + + test('Push Certificate Validation test BAD', () => { + const serverConfig = { + receive: { + enable_signed_push: true, + }, + }; + const change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + owner: { + _account_id: 1019328, + }, + revisions: { + rev1: { + _number: 1, + push_certificate: { + key: { + status: 'BAD', + problems: [ + 'No public keys found for key ID E5E20E52', + ], + }, + }, + }, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + mergeable: true, + }; + const result = + element._computePushCertificateValidation(serverConfig, change); + assert.equal(result.message, + 'Push certificate is invalid:\n' + + 'No public keys found for key ID E5E20E52'); + assert.equal(result.icon, 'gr-icons:close'); + assert.equal(result.class, 'invalid'); + }); + + test('Push Certificate Validation test TRUSTED', () => { + const serverConfig = { + receive: { + enable_signed_push: true, + }, + }; + const change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + owner: { + _account_id: 1019328, + }, + revisions: { + rev1: { + _number: 1, + push_certificate: { + key: { + status: 'TRUSTED', + }, + }, + }, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + mergeable: true, + }; + const result = + element._computePushCertificateValidation(serverConfig, change); + assert.equal(result.message, + 'Push certificate is valid and key is trusted'); + assert.equal(result.icon, 'gr-icons:check'); + assert.equal(result.class, 'trusted'); + }); + + test('Push Certificate Validation is missing test', () => { + const serverConfig = { + receive: { + enable_signed_push: true, + }, + }; + const change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + owner: { + _account_id: 1019328, + }, + revisions: { + rev1: { + _number: 1, + }, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + mergeable: true, + }; + const result = + element._computePushCertificateValidation(serverConfig, change); + assert.equal(result.message, + 'This patch set was created without a push certificate'); + assert.equal(result.icon, 'gr-icons:help'); + assert.equal(result.class, 'help'); + }); + + test('_computeParents', () => { + const parents = [{commit: '123', subject: 'abc'}]; + assert.isUndefined(element._computeParents( + {revisions: {456: {commit: {parents}}}})); + assert.isUndefined(element._computeParents( + {current_revision: '789', revisions: {456: {commit: {parents}}}})); + assert.equal(element._computeParents( + {current_revision: '456', revisions: {456: {commit: {parents}}}}), + parents); + }); + + test('_computeParentsLabel', () => { + const parent = {commit: 'abc123', subject: 'My parent commit'}; + assert.equal(element._computeParentsLabel([parent]), 'Parent'); + assert.equal(element._computeParentsLabel([parent, parent]), + 'Parents'); + }); + + test('_computeParentListClass', () => { + const parent = {commit: 'abc123', subject: 'My parent commit'}; + assert.equal(element._computeParentListClass([parent], true), + 'parentList nonMerge current'); + assert.equal(element._computeParentListClass([parent], false), + 'parentList nonMerge notCurrent'); + assert.equal(element._computeParentListClass([parent, parent], false), + 'parentList merge notCurrent'); + assert.equal(element._computeParentListClass([parent, parent], true), + 'parentList merge current'); + }); + + test('_showAddTopic', () => { + assert.isTrue(element._showAddTopic(null, false)); + assert.isTrue(element._showAddTopic({base: {topic: null}}, false)); + assert.isFalse(element._showAddTopic({base: {topic: null}}, true)); + assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, true)); + assert.isFalse(element._showAddTopic({base: {topic: 'foo'}}, false)); + }); + + test('_showTopicChip', () => { + assert.isFalse(element._showTopicChip(null, false)); + assert.isFalse(element._showTopicChip({base: {topic: null}}, false)); + assert.isFalse(element._showTopicChip({base: {topic: null}}, true)); + assert.isFalse(element._showTopicChip({base: {topic: 'foo'}}, true)); + assert.isTrue(element._showTopicChip({base: {topic: 'foo'}}, false)); + }); + + test('_showCherryPickOf', () => { + assert.isFalse(element._showCherryPickOf(null)); + assert.isFalse(element._showCherryPickOf({ + base: { + cherry_pick_of_change: null, + cherry_pick_of_patch_set: null, + }, + })); + assert.isTrue(element._showCherryPickOf({ + base: { + cherry_pick_of_change: 123, + cherry_pick_of_patch_set: 1, + }, + })); + }); + + suite('Topic removal', () => { + let change; + setup(() => { + change = { + _number: 'the number', + actions: { + topic: {enabled: false}, + }, + change_id: 'the id', + topic: 'the topic', + status: 'NEW', + submit_type: 'CHERRY_PICK', + labels: { + test: { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + }, + }, + removable_reviewers: [], + }; + }); + + test('_computeTopicReadOnly', () => { + let mutable = false; + assert.isTrue(element._computeTopicReadOnly(mutable, change)); + mutable = true; + assert.isTrue(element._computeTopicReadOnly(mutable, change)); + change.actions.topic.enabled = true; + assert.isFalse(element._computeTopicReadOnly(mutable, change)); + mutable = false; + assert.isTrue(element._computeTopicReadOnly(mutable, change)); + }); + + test('topic read only hides delete button', () => { + element.account = {}; + element.change = change; + flushAsynchronousOperations(); + const button = element.shadowRoot + .querySelector('gr-linked-chip').shadowRoot + .querySelector('gr-button'); + assert.isTrue(button.hasAttribute('hidden')); + }); + + test('topic not read only does not hide delete button', () => { + element.account = {test: true}; + change.actions.topic.enabled = true; + element.change = change; + flushAsynchronousOperations(); + const button = element.shadowRoot + .querySelector('gr-linked-chip').shadowRoot + .querySelector('gr-button'); + assert.isFalse(button.hasAttribute('hidden')); + }); + }); + + suite('Hashtag removal', () => { + let change; + setup(() => { + change = { + _number: 'the number', + actions: { + hashtags: {enabled: false}, + }, + change_id: 'the id', + hashtags: ['test-hashtag'], + status: 'NEW', + submit_type: 'CHERRY_PICK', + labels: { + test: { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + }, + }, + removable_reviewers: [], + }; + }); + + test('_computeHashtagReadOnly', () => { + flushAsynchronousOperations(); + let mutable = false; + assert.isTrue(element._computeHashtagReadOnly(mutable, change)); + mutable = true; + assert.isTrue(element._computeHashtagReadOnly(mutable, change)); + change.actions.hashtags.enabled = true; + assert.isFalse(element._computeHashtagReadOnly(mutable, change)); + mutable = false; + assert.isTrue(element._computeHashtagReadOnly(mutable, change)); + }); + + test('hashtag read only hides delete button', () => { + flushAsynchronousOperations(); + element.account = {}; + element.change = change; + flushAsynchronousOperations(); + const button = element.shadowRoot + .querySelector('gr-linked-chip').shadowRoot + .querySelector('gr-button'); + assert.isTrue(button.hasAttribute('hidden')); + }); + + test('hashtag not read only does not hide delete button', () => { + flushAsynchronousOperations(); + element.account = {test: true}; + change.actions.hashtags.enabled = true; + element.change = change; + flushAsynchronousOperations(); + const button = element.shadowRoot + .querySelector('gr-linked-chip').shadowRoot + .querySelector('gr-button'); + assert.isFalse(button.hasAttribute('hidden')); + }); + }); + + suite('remove reviewer votes', () => { + setup(() => { + sandbox.stub(element, '_computeTopicReadOnly').returns(true); + element.change = { + _number: 42, + change_id: 'the id', + actions: [], + topic: 'the topic', + status: 'NEW', + submit_type: 'CHERRY_PICK', + labels: { + test: { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + }, + }, + removable_reviewers: [], + }; + flushAsynchronousOperations(); + }); + + suite('assignee field', () => { + const dummyAccount = { + _account_id: 1, + name: 'bojack', + }; + const change = { + actions: { + assignee: {enabled: false}, + }, + assignee: dummyAccount, + }; + let deleteStub; + let setStub; + + setup(() => { + deleteStub = sandbox.stub(element.$.restAPI, 'deleteAssignee'); + setStub = sandbox.stub(element.$.restAPI, 'setAssignee'); + }); + + test('changing change recomputes _assignee', () => { + assert.isFalse(!!element._assignee.length); + const change = element.change; + change.assignee = dummyAccount; + element._changeChanged(change); + assert.deepEqual(element._assignee[0], dummyAccount); + }); + + test('modifying _assignee calls API', () => { + assert.isFalse(!!element._assignee.length); + element.set('_assignee', [dummyAccount]); + assert.isTrue(setStub.calledOnce); + assert.deepEqual(element.change.assignee, dummyAccount); + element.set('_assignee', [dummyAccount]); + assert.isTrue(setStub.calledOnce); + element.set('_assignee', []); + assert.isTrue(deleteStub.calledOnce); + assert.equal(element.change.assignee, undefined); + element.set('_assignee', []); + assert.isTrue(deleteStub.calledOnce); + }); + + test('_computeAssigneeReadOnly', () => { + let mutable = false; + assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); + mutable = true; + assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); + change.actions.assignee.enabled = true; + assert.isFalse(element._computeAssigneeReadOnly(mutable, change)); + mutable = false; + assert.isTrue(element._computeAssigneeReadOnly(mutable, change)); + }); + }); + + test('changing topic', () => { + const newTopic = 'the new topic'; + sandbox.stub(element.$.restAPI, 'setChangeTopic').returns( + Promise.resolve(newTopic)); + element._handleTopicChanged({}, newTopic); + const topicChangedSpy = sandbox.spy(); + element.addEventListener('topic-changed', topicChangedSpy); + assert.isTrue(element.$.restAPI.setChangeTopic.calledWith( + 42, newTopic)); + return element.$.restAPI.setChangeTopic.lastCall.returnValue + .then(() => { + assert.equal(element.change.topic, newTopic); + assert.isTrue(topicChangedSpy.called); + }); + }); + + test('topic removal', () => { + sandbox.stub(element.$.restAPI, 'setChangeTopic').returns( + Promise.resolve()); + const chip = element.shadowRoot + .querySelector('gr-linked-chip'); + const remove = chip.$.remove; + const topicChangedSpy = sandbox.spy(); + element.addEventListener('topic-changed', topicChangedSpy); + MockInteractions.tap(remove); + assert.isTrue(chip.disabled); + assert.isTrue(element.$.restAPI.setChangeTopic.calledWith( + 42, null)); + return element.$.restAPI.setChangeTopic.lastCall.returnValue + .then(() => { + assert.isFalse(chip.disabled); + assert.equal(element.change.topic, ''); + assert.isTrue(topicChangedSpy.called); + }); + }); + + test('changing hashtag', () => { + flushAsynchronousOperations(); + element._newHashtag = 'new hashtag'; + const newHashtag = ['new hashtag']; + sandbox.stub(element.$.restAPI, 'setChangeHashtag').returns( + Promise.resolve(newHashtag)); + element._handleHashtagChanged({}, 'new hashtag'); + assert.isTrue(element.$.restAPI.setChangeHashtag.calledWith( + 42, {add: ['new hashtag']})); + return element.$.restAPI.setChangeHashtag.lastCall.returnValue + .then(() => { + assert.equal(element.change.hashtags, newHashtag); + }); + }); + }); + + test('editTopic', () => { + element.account = {test: true}; + element.change = {actions: {topic: {enabled: true}}}; + flushAsynchronousOperations(); + + const label = element.shadowRoot + .querySelector('.topicEditableLabel'); + assert.ok(label); + sandbox.stub(label, 'open'); + element.editTopic(); + flushAsynchronousOperations(); + + assert.isTrue(label.open.called); + }); + + suite('plugin endpoints', () => { + test('endpoint params', done => { + element.change = {labels: {}}; + element.revision = {}; + let hookEl; + let plugin; + Gerrit.install( + p => { + plugin = p; + plugin.hook('change-metadata-item').getLastAttached() + .then(el => hookEl = el); + }, + '0.1', + 'http://some/plugins/url.html'); + Gerrit._loadPlugins([]); + flush(() => { + assert.strictEqual(hookEl.plugin, plugin); + assert.strictEqual(hookEl.change, element.change); + assert.strictEqual(hookEl.revision, element.revision); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js index a413c6f..9dd5acf 100644 --- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js +++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements.js
@@ -14,147 +14,160 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element - */ - class GrChangeRequirements extends Polymer.mixinBehaviors( [ - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-requirements'; } +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-label/gr-label.js'; +import '../../shared/gr-label-info/gr-label-info.js'; +import '../../shared/gr-limited-text/gr-limited-text.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-requirements_html.js'; - static get properties() { - return { - /** @type {?} */ - change: Object, - account: Object, - mutable: Boolean, - _requirements: { - type: Array, - computed: '_computeRequirements(change)', - }, - _requiredLabels: { - type: Array, - value: () => [], - }, - _optionalLabels: { - type: Array, - value: () => [], - }, - _showWip: { - type: Boolean, - computed: '_computeShowWip(change)', - }, - _showOptionalLabels: { - type: Boolean, - value: true, - }, - }; - } +/** + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrChangeRequirements extends mixinBehaviors( [ + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - static get observers() { - return [ - '_computeLabels(change.labels.*)', - ]; - } + static get is() { return 'gr-change-requirements'; } - _computeShowWip(change) { - return change.work_in_progress; - } + static get properties() { + return { + /** @type {?} */ + change: Object, + account: Object, + mutable: Boolean, + _requirements: { + type: Array, + computed: '_computeRequirements(change)', + }, + _requiredLabels: { + type: Array, + value: () => [], + }, + _optionalLabels: { + type: Array, + value: () => [], + }, + _showWip: { + type: Boolean, + computed: '_computeShowWip(change)', + }, + _showOptionalLabels: { + type: Boolean, + value: true, + }, + }; + } - _computeRequirements(change) { - const _requirements = []; + static get observers() { + return [ + '_computeLabels(change.labels.*)', + ]; + } - if (change.requirements) { - for (const requirement of change.requirements) { - requirement.satisfied = requirement.status === 'OK'; - requirement.style = - this._computeRequirementClass(requirement.satisfied); - _requirements.push(requirement); - } - } - if (change.work_in_progress) { - _requirements.push({ - fallback_text: 'Work-in-progress', - tooltip: 'Change must not be in \'Work in Progress\' state.', - }); - } + _computeShowWip(change) { + return change.work_in_progress; + } - return _requirements; - } + _computeRequirements(change) { + const _requirements = []; - _computeRequirementClass(requirementStatus) { - return requirementStatus ? 'approved' : ''; - } - - _computeRequirementIcon(requirementStatus) { - return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass'; - } - - _computeLabels(labelsRecord) { - const labels = labelsRecord.base; - this._optionalLabels = []; - this._requiredLabels = []; - - for (const label in labels) { - if (!labels.hasOwnProperty(label)) { continue; } - - const labelInfo = labels[label]; - const icon = this._computeLabelIcon(labelInfo); - const style = this._computeLabelClass(labelInfo); - const path = labelInfo.optional ? '_optionalLabels' : '_requiredLabels'; - - this.push(path, {label, icon, style, labelInfo}); + if (change.requirements) { + for (const requirement of change.requirements) { + requirement.satisfied = requirement.status === 'OK'; + requirement.style = + this._computeRequirementClass(requirement.satisfied); + _requirements.push(requirement); } } - - /** - * @param {Object} labelInfo - * @return {string} The icon name, or undefined if no icon should - * be used. - */ - _computeLabelIcon(labelInfo) { - if (labelInfo.approved) { return 'gr-icons:check'; } - if (labelInfo.rejected) { return 'gr-icons:close'; } - return 'gr-icons:hourglass'; + if (change.work_in_progress) { + _requirements.push({ + fallback_text: 'Work-in-progress', + tooltip: 'Change must not be in \'Work in Progress\' state.', + }); } - /** - * @param {Object} labelInfo - */ - _computeLabelClass(labelInfo) { - if (labelInfo.approved) { return 'approved'; } - if (labelInfo.rejected) { return 'rejected'; } - return ''; - } + return _requirements; + } - _computeShowOptional(optionalFieldsRecord) { - return optionalFieldsRecord.base.length ? '' : 'hidden'; - } + _computeRequirementClass(requirementStatus) { + return requirementStatus ? 'approved' : ''; + } - _computeLabelValue(value) { - return (value > 0 ? '+' : '') + value; - } + _computeRequirementIcon(requirementStatus) { + return requirementStatus ? 'gr-icons:check' : 'gr-icons:hourglass'; + } - _computeShowHideIcon(showOptionalLabels) { - return showOptionalLabels ? - 'gr-icons:expand-less' : - 'gr-icons:expand-more'; - } + _computeLabels(labelsRecord) { + const labels = labelsRecord.base; + this._optionalLabels = []; + this._requiredLabels = []; - _computeSectionClass(show) { - return show ? '' : 'hidden'; - } + for (const label in labels) { + if (!labels.hasOwnProperty(label)) { continue; } - _handleShowHide(e) { - this._showOptionalLabels = !this._showOptionalLabels; + const labelInfo = labels[label]; + const icon = this._computeLabelIcon(labelInfo); + const style = this._computeLabelClass(labelInfo); + const path = labelInfo.optional ? '_optionalLabels' : '_requiredLabels'; + + this.push(path, {label, icon, style, labelInfo}); } } - customElements.define(GrChangeRequirements.is, GrChangeRequirements); -})(); + /** + * @param {Object} labelInfo + * @return {string} The icon name, or undefined if no icon should + * be used. + */ + _computeLabelIcon(labelInfo) { + if (labelInfo.approved) { return 'gr-icons:check'; } + if (labelInfo.rejected) { return 'gr-icons:close'; } + return 'gr-icons:hourglass'; + } + + /** + * @param {Object} labelInfo + */ + _computeLabelClass(labelInfo) { + if (labelInfo.approved) { return 'approved'; } + if (labelInfo.rejected) { return 'rejected'; } + return ''; + } + + _computeShowOptional(optionalFieldsRecord) { + return optionalFieldsRecord.base.length ? '' : 'hidden'; + } + + _computeLabelValue(value) { + return (value > 0 ? '+' : '') + value; + } + + _computeShowHideIcon(showOptionalLabels) { + return showOptionalLabels ? + 'gr-icons:expand-less' : + 'gr-icons:expand-more'; + } + + _computeSectionClass(show) { + return show ? '' : 'hidden'; + } + + _handleShowHide(e) { + this._showOptionalLabels = !this._showOptionalLabels; + } +} + +customElements.define(GrChangeRequirements.is, GrChangeRequirements);
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js index 372ae50..311cfe4 100644 --- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js +++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-label/gr-label.html"> -<link rel="import" href="../../shared/gr-label-info/gr-label-info.html"> -<link rel="import" href="../../shared/gr-limited-text/gr-limited-text.html"> - -<dom-module id="gr-change-requirements"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: table; @@ -91,59 +82,43 @@ height: var(--spacing-m); } </style> - <template - is="dom-repeat" - items="[[_requirements]]"> + <template is="dom-repeat" items="[[_requirements]]"> <section> <div class="title requirement"> - <span class$="status [[item.style]]"> + <span class\$="status [[item.style]]"> <iron-icon class="icon" icon="[[_computeRequirementIcon(item.satisfied)]]"></iron-icon> </span> <gr-limited-text class="name" limit="40" text="[[item.fallback_text]]"></gr-limited-text> </div> </section> </template> - <template - is="dom-repeat" - items="[[_requiredLabels]]"> + <template is="dom-repeat" items="[[_requiredLabels]]"> <section> <div class="title"> - <span class$="status [[item.style]]"> + <span class\$="status [[item.style]]"> <iron-icon class="icon" icon="[[item.icon]]"></iron-icon> </span> <gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text> </div> <div class="value"> - <gr-label-info - change="{{change}}" - account="[[account]]" - mutable="[[mutable]]" - label="[[item.label]]" - label-info="[[item.labelInfo]]"></gr-label-info> + <gr-label-info change="{{change}}" account="[[account]]" mutable="[[mutable]]" label="[[item.label]]" label-info="[[item.labelInfo]]"></gr-label-info> </div> </section> </template> <section class="spacer"></section> - <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section> - <section - show-bottom-border$="[[_showOptionalLabels]]" - on-click="_handleShowHide" - class$="showHide [[_computeShowOptional(_optionalLabels.*)]]"> + <section class\$="spacer [[_computeShowOptional(_optionalLabels.*)]]"></section> + <section show-bottom-border\$="[[_showOptionalLabels]]" on-click="_handleShowHide" class\$="showHide [[_computeShowOptional(_optionalLabels.*)]]"> <div class="title">Other labels</div> <div class="value"> - <iron-icon - id="showHide" - icon="[[_computeShowHideIcon(_showOptionalLabels)]]"> + <iron-icon id="showHide" icon="[[_computeShowHideIcon(_showOptionalLabels)]]"> </iron-icon> - </label> + </div> </section> - <template - is="dom-repeat" - items="[[_optionalLabels]]"> - <section class$="optional [[_computeSectionClass(_showOptionalLabels)]]"> + <template is="dom-repeat" items="[[_optionalLabels]]"> + <section class\$="optional [[_computeSectionClass(_showOptionalLabels)]]"> <div class="title"> - <span class$="status [[item.style]]"> + <span class\$="status [[item.style]]"> <template is="dom-if" if="[[item.icon]]"> <iron-icon class="icon" icon="[[item.icon]]"></iron-icon> </template> @@ -154,16 +129,9 @@ <gr-limited-text class="name" limit="40" text="[[item.label]]"></gr-limited-text> </div> <div class="value"> - <gr-label-info - change="{{change}}" - account="[[account]]" - mutable="[[mutable]]" - label="[[item.label]]" - label-info="[[item.labelInfo]]"></gr-label-info> + <gr-label-info change="{{change}}" account="[[account]]" mutable="[[mutable]]" label="[[item.label]]" label-info="[[item.labelInfo]]"></gr-label-info> </div> </section> </template> - <section class$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"></section> - </template> - <script src="gr-change-requirements.js"></script> -</dom-module> + <section class\$="spacer [[_computeShowOptional(_optionalLabels.*)]] [[_computeSectionClass(_showOptionalLabels)]]"></section> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html index 10466db..2883f20 100644 --- a/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-requirements/gr-change-requirements_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-requirements</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-change-requirements.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-change-requirements.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-requirements.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,204 +40,206 @@ </template> </test-fixture> -<script> - suite('gr-change-metadata tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-requirements.js'; +suite('gr-change-metadata tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); - - test('requirements computed fields', () => { - assert.isTrue(element._computeShowWip({work_in_progress: true})); - assert.isFalse(element._computeShowWip({work_in_progress: false})); - - assert.equal(element._computeRequirementClass(true), 'approved'); - assert.equal(element._computeRequirementClass(false), ''); - - assert.equal(element._computeRequirementIcon(true), 'gr-icons:check'); - assert.equal(element._computeRequirementIcon(false), - 'gr-icons:hourglass'); - }); - - test('label computed fields', () => { - assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check'); - assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close'); - assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass'); - - assert.equal(element._computeLabelClass({approved: []}), 'approved'); - assert.equal(element._computeLabelClass({rejected: []}), 'rejected'); - assert.equal(element._computeLabelClass({}), ''); - assert.equal(element._computeLabelClass({value: 0}), ''); - - assert.equal(element._computeLabelValue(1), '+1'); - assert.equal(element._computeLabelValue(-1), '-1'); - assert.equal(element._computeLabelValue(0), '0'); - }); - - test('_computeLabels', () => { - assert.equal(element._optionalLabels.length, 0); - assert.equal(element._requiredLabels.length, 0); - element._computeLabels({base: { - test: { - all: [{_account_id: 1, name: 'bojack', value: 1}], - default_value: 0, - values: [], - value: 1, - }, - opt_test: { - all: [{_account_id: 1, name: 'bojack', value: 1}], - default_value: 0, - values: [], - optional: true, - }, - }}); - assert.equal(element._optionalLabels.length, 1); - assert.equal(element._requiredLabels.length, 1); - - assert.equal(element._optionalLabels[0].label, 'opt_test'); - assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass'); - assert.equal(element._optionalLabels[0].style, ''); - assert.ok(element._optionalLabels[0].labelInfo); - }); - - test('optional show/hide', () => { - element._optionalLabels = [{label: 'test'}]; - flushAsynchronousOperations(); - - assert.ok(element.shadowRoot - .querySelector('section.optional')); - MockInteractions.tap(element.shadowRoot - .querySelector('.showHide')); - flushAsynchronousOperations(); - - assert.isFalse(element._showOptionalLabels); - assert.isTrue(isHidden(element.shadowRoot - .querySelector('section.optional'))); - }); - - test('properly converts satisfied labels', () => { - element.change = { - status: 'NEW', - labels: { - Verified: { - approved: [], - }, - }, - requirements: [], - }; - flushAsynchronousOperations(); - - assert.ok(element.shadowRoot - .querySelector('.approved')); - assert.ok(element.shadowRoot - .querySelector('.name')); - assert.equal(element.shadowRoot - .querySelector('.name').text, 'Verified'); - }); - - test('properly converts unsatisfied labels', () => { - element.change = { - status: 'NEW', - labels: { - Verified: { - approved: false, - }, - }, - }; - flushAsynchronousOperations(); - - const name = element.shadowRoot - .querySelector('.name'); - assert.ok(name); - assert.isFalse(name.hasAttribute('hidden')); - assert.equal(name.text, 'Verified'); - }); - - test('properly displays Work In Progress', () => { - element.change = { - status: 'NEW', - labels: {}, - requirements: [], - work_in_progress: true, - }; - flushAsynchronousOperations(); - - const changeIsWip = element.shadowRoot - .querySelector('.title'); - assert.ok(changeIsWip); - }); - - test('properly displays a satisfied requirement', () => { - element.change = { - status: 'NEW', - labels: {}, - requirements: [{ - fallback_text: 'Resolve all comments', - status: 'OK', - }], - }; - flushAsynchronousOperations(); - - const requirement = element.shadowRoot - .querySelector('.requirement'); - assert.ok(requirement); - assert.isFalse(requirement.hasAttribute('hidden')); - assert.ok(requirement.querySelector('.approved')); - assert.equal(requirement.querySelector('.name').text, - 'Resolve all comments'); - }); - - test('satisfied class is applied with OK', () => { - element.change = { - status: 'NEW', - labels: {}, - requirements: [{ - fallback_text: 'Resolve all comments', - status: 'OK', - }], - }; - flushAsynchronousOperations(); - - const requirement = element.shadowRoot - .querySelector('.requirement'); - assert.ok(requirement); - assert.ok(requirement.querySelector('.approved')); - }); - - test('satisfied class is not applied with NOT_READY', () => { - element.change = { - status: 'NEW', - labels: {}, - requirements: [{ - fallback_text: 'Resolve all comments', - status: 'NOT_READY', - }], - }; - flushAsynchronousOperations(); - - const requirement = element.shadowRoot - .querySelector('.requirement'); - assert.ok(requirement); - assert.strictEqual(requirement.querySelector('.approved'), null); - }); - - test('satisfied class is not applied with RULE_ERROR', () => { - element.change = { - status: 'NEW', - labels: {}, - requirements: [{ - fallback_text: 'Resolve all comments', - status: 'RULE_ERROR', - }], - }; - flushAsynchronousOperations(); - - const requirement = element.shadowRoot - .querySelector('.requirement'); - assert.ok(requirement); - assert.strictEqual(requirement.querySelector('.approved'), null); - }); + setup(() => { + element = fixture('basic'); }); + + test('requirements computed fields', () => { + assert.isTrue(element._computeShowWip({work_in_progress: true})); + assert.isFalse(element._computeShowWip({work_in_progress: false})); + + assert.equal(element._computeRequirementClass(true), 'approved'); + assert.equal(element._computeRequirementClass(false), ''); + + assert.equal(element._computeRequirementIcon(true), 'gr-icons:check'); + assert.equal(element._computeRequirementIcon(false), + 'gr-icons:hourglass'); + }); + + test('label computed fields', () => { + assert.equal(element._computeLabelIcon({approved: []}), 'gr-icons:check'); + assert.equal(element._computeLabelIcon({rejected: []}), 'gr-icons:close'); + assert.equal(element._computeLabelIcon({}), 'gr-icons:hourglass'); + + assert.equal(element._computeLabelClass({approved: []}), 'approved'); + assert.equal(element._computeLabelClass({rejected: []}), 'rejected'); + assert.equal(element._computeLabelClass({}), ''); + assert.equal(element._computeLabelClass({value: 0}), ''); + + assert.equal(element._computeLabelValue(1), '+1'); + assert.equal(element._computeLabelValue(-1), '-1'); + assert.equal(element._computeLabelValue(0), '0'); + }); + + test('_computeLabels', () => { + assert.equal(element._optionalLabels.length, 0); + assert.equal(element._requiredLabels.length, 0); + element._computeLabels({base: { + test: { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + value: 1, + }, + opt_test: { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + optional: true, + }, + }}); + assert.equal(element._optionalLabels.length, 1); + assert.equal(element._requiredLabels.length, 1); + + assert.equal(element._optionalLabels[0].label, 'opt_test'); + assert.equal(element._optionalLabels[0].icon, 'gr-icons:hourglass'); + assert.equal(element._optionalLabels[0].style, ''); + assert.ok(element._optionalLabels[0].labelInfo); + }); + + test('optional show/hide', () => { + element._optionalLabels = [{label: 'test'}]; + flushAsynchronousOperations(); + + assert.ok(element.shadowRoot + .querySelector('section.optional')); + MockInteractions.tap(element.shadowRoot + .querySelector('.showHide')); + flushAsynchronousOperations(); + + assert.isFalse(element._showOptionalLabels); + assert.isTrue(isHidden(element.shadowRoot + .querySelector('section.optional'))); + }); + + test('properly converts satisfied labels', () => { + element.change = { + status: 'NEW', + labels: { + Verified: { + approved: [], + }, + }, + requirements: [], + }; + flushAsynchronousOperations(); + + assert.ok(element.shadowRoot + .querySelector('.approved')); + assert.ok(element.shadowRoot + .querySelector('.name')); + assert.equal(element.shadowRoot + .querySelector('.name').text, 'Verified'); + }); + + test('properly converts unsatisfied labels', () => { + element.change = { + status: 'NEW', + labels: { + Verified: { + approved: false, + }, + }, + }; + flushAsynchronousOperations(); + + const name = element.shadowRoot + .querySelector('.name'); + assert.ok(name); + assert.isFalse(name.hasAttribute('hidden')); + assert.equal(name.text, 'Verified'); + }); + + test('properly displays Work In Progress', () => { + element.change = { + status: 'NEW', + labels: {}, + requirements: [], + work_in_progress: true, + }; + flushAsynchronousOperations(); + + const changeIsWip = element.shadowRoot + .querySelector('.title'); + assert.ok(changeIsWip); + }); + + test('properly displays a satisfied requirement', () => { + element.change = { + status: 'NEW', + labels: {}, + requirements: [{ + fallback_text: 'Resolve all comments', + status: 'OK', + }], + }; + flushAsynchronousOperations(); + + const requirement = element.shadowRoot + .querySelector('.requirement'); + assert.ok(requirement); + assert.isFalse(requirement.hasAttribute('hidden')); + assert.ok(requirement.querySelector('.approved')); + assert.equal(requirement.querySelector('.name').text, + 'Resolve all comments'); + }); + + test('satisfied class is applied with OK', () => { + element.change = { + status: 'NEW', + labels: {}, + requirements: [{ + fallback_text: 'Resolve all comments', + status: 'OK', + }], + }; + flushAsynchronousOperations(); + + const requirement = element.shadowRoot + .querySelector('.requirement'); + assert.ok(requirement); + assert.ok(requirement.querySelector('.approved')); + }); + + test('satisfied class is not applied with NOT_READY', () => { + element.change = { + status: 'NEW', + labels: {}, + requirements: [{ + fallback_text: 'Resolve all comments', + status: 'NOT_READY', + }], + }; + flushAsynchronousOperations(); + + const requirement = element.shadowRoot + .querySelector('.requirement'); + assert.ok(requirement); + assert.strictEqual(requirement.querySelector('.approved'), null); + }); + + test('satisfied class is not applied with RULE_ERROR', () => { + element.change = { + status: 'NEW', + labels: {}, + requirements: [{ + fallback_text: 'Resolve all comments', + status: 'RULE_ERROR', + }], + }; + flushAsynchronousOperations(); + + const requirement = element.shadowRoot + .querySelector('.requirement'); + assert.ok(requirement); + assert.strictEqual(requirement.querySelector('.approved'), null); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js index 58505ff..7a80d34 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,2064 +14,2111 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const CHANGE_ID_ERROR = { - MISMATCH: 'mismatch', - MISSING: 'missing', - }; - const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '@polymer/paper-tabs/paper-tabs.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../diff/gr-comment-api/gr-comment-api.js'; +import '../../edit/gr-edit-constants.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../shared/gr-account-link/gr-account-link.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-change-star/gr-change-star.js'; +import '../../shared/gr-change-status/gr-change-status.js'; +import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-editable-content/gr-editable-content.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../../shared/gr-linked-text/gr-linked-text.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; +import '../../shared/revision-info/revision-info.js'; +import '../gr-change-actions/gr-change-actions.js'; +import '../gr-change-metadata/gr-change-metadata.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../gr-commit-info/gr-commit-info.js'; +import '../gr-download-dialog/gr-download-dialog.js'; +import '../gr-file-list-header/gr-file-list-header.js'; +import '../gr-file-list/gr-file-list.js'; +import '../gr-included-in-dialog/gr-included-in-dialog.js'; +import '../gr-messages-list/gr-messages-list.js'; +import '../gr-related-changes-list/gr-related-changes-list.js'; +import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js'; +import '../gr-reply-dialog/gr-reply-dialog.js'; +import '../gr-thread-list/gr-thread-list.js'; +import '../gr-upload-help-dialog/gr-upload-help-dialog.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {beforeNextRender} from '@polymer/polymer/lib/utils/render-status.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-view_html.js'; - const MIN_LINES_FOR_COMMIT_COLLAPSE = 30; - const DEFAULT_NUM_FILES_SHOWN = 200; +const CHANGE_ID_ERROR = { + MISMATCH: 'mismatch', + MISSING: 'missing', +}; +const CHANGE_ID_REGEX_PATTERN = /^Change-Id\:\s(I[0-9a-f]{8,40})/gm; - const REVIEWERS_REGEX = /^(R|CC)=/gm; - const MIN_CHECK_INTERVAL_SECS = 0; +const MIN_LINES_FOR_COMMIT_COLLAPSE = 30; +const DEFAULT_NUM_FILES_SHOWN = 200; - // These are the same as the breakpoint set in CSS. Make sure both are changed - // together. - const BREAKPOINT_RELATED_SMALL = '50em'; - const BREAKPOINT_RELATED_MED = '75em'; +const REVIEWERS_REGEX = /^(R|CC)=/gm; +const MIN_CHECK_INTERVAL_SECS = 0; - // In the event that the related changes medium width calculation is too close - // to zero, provide some height. - const MINIMUM_RELATED_MAX_HEIGHT = 100; +// These are the same as the breakpoint set in CSS. Make sure both are changed +// together. +const BREAKPOINT_RELATED_SMALL = '50em'; +const BREAKPOINT_RELATED_MED = '75em'; - const SMALL_RELATED_HEIGHT = 400; +// In the event that the related changes medium width calculation is too close +// to zero, provide some height. +const MINIMUM_RELATED_MAX_HEIGHT = 100; - const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500; +const SMALL_RELATED_HEIGHT = 400; - const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm; +const REPLY_REFIT_DEBOUNCE_INTERVAL_MS = 500; - const MSG_PREFIX = '#message-'; +const TRAILING_WHITESPACE_REGEX = /[ \t]+$/gm; - const ReloadToastMessage = { - NEWER_REVISION: 'A newer patch set has been uploaded', - RESTORED: 'This change has been restored', - ABANDONED: 'This change has been abandoned', - MERGED: 'This change has been merged', - NEW_MESSAGE: 'There are new messages on this change', - }; +const MSG_PREFIX = '#message-'; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +const ReloadToastMessage = { + NEWER_REVISION: 'A newer patch set has been uploaded', + RESTORED: 'This change has been restored', + ABANDONED: 'This change has been abandoned', + MERGED: 'This change has been merged', + NEW_MESSAGE: 'There are new messages on this change', +}; - const CommentTabs = { - CHANGE_LOG: 0, - COMMENT_THREADS: 1, - ROBOT_COMMENTS: 2, - }; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; - const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded'; - const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded'; - const SEND_REPLY_TIMING_LABEL = 'SendReply'; - // Making the tab names more unique in case a plugin adds one with same name - const FILES_TAB_NAME = '__gerrit_internal_files'; - const FINDINGS_TAB_NAME = '__gerrit_internal_findings'; - const ROBOT_COMMENTS_LIMIT = 10; +const CommentTabs = { + CHANGE_LOG: 0, + COMMENT_THREADS: 1, + ROBOT_COMMENTS: 2, +}; + +const CHANGE_DATA_TIMING_LABEL = 'ChangeDataLoaded'; +const CHANGE_RELOAD_TIMING_LABEL = 'ChangeReloaded'; +const SEND_REPLY_TIMING_LABEL = 'SendReply'; +// Making the tab names more unique in case a plugin adds one with same name +const FILES_TAB_NAME = '__gerrit_internal_files'; +const FINDINGS_TAB_NAME = '__gerrit_internal_findings'; +const ROBOT_COMMENTS_LIMIT = 10; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrChangeView extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-change-view'; } + /** + * Fired when the title of the page should change. + * + * @event title-change + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired if an error occurs when fetching the change data. + * + * @event page-error */ - class GrChangeView extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-view'; } + + /** + * Fired if being logged in is required. + * + * @event show-auth-required + */ + + static get properties() { + return { /** - * Fired when the title of the page should change. - * - * @event title-change + * URL params passed from the router. */ + params: { + type: Object, + observer: '_paramsChanged', + }, + /** @type {?} */ + viewState: { + type: Object, + notify: true, + value() { return {}; }, + observer: '_viewStateChanged', + }, + backPage: String, + hasParent: Boolean, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + disableEdit: { + type: Boolean, + value: false, + }, + disableDiffPrefs: { + type: Boolean, + value: false, + }, + _diffPrefsDisabled: { + type: Boolean, + computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', + }, + _commentThreads: Array, + // TODO(taoalpha): Consider replacing diffDrafts + // with _draftCommentThreads everywhere, currently only + // replaced in reply-dialoig + _draftCommentThreads: { + type: Array, + }, + _robotCommentThreads: { + type: Array, + computed: '_computeRobotCommentThreads(_commentThreads,' + + ' _currentRobotCommentsPatchSet, _showAllRobotComments)', + }, + /** @type {?} */ + _serverConfig: { + type: Object, + observer: '_startUpdateCheckTimer', + }, + _diffPrefs: Object, + _numFilesShown: { + type: Number, + value: DEFAULT_NUM_FILES_SHOWN, + observer: '_numFilesShownChanged', + }, + _account: { + type: Object, + value: {}, + }, + _prefs: Object, + /** @type {?} */ + _changeComments: Object, + _canStartReview: { + type: Boolean, + computed: '_computeCanStartReview(_change)', + }, + _comments: Object, + /** @type {?} */ + _change: { + type: Object, + observer: '_changeChanged', + }, + _revisionInfo: { + type: Object, + computed: '_getRevisionInfo(_change)', + }, + /** @type {?} */ + _commitInfo: Object, + _currentRevision: { + type: Object, + computed: '_computeCurrentRevision(_change.current_revision, ' + + '_change.revisions)', + observer: '_handleCurrentRevisionUpdate', + }, + _files: Object, + _changeNum: String, + _diffDrafts: { + type: Object, + value() { return {}; }, + }, + _editingCommitMessage: { + type: Boolean, + value: false, + }, + _hideEditCommitMessage: { + type: Boolean, + computed: '_computeHideEditCommitMessage(_loggedIn, ' + + '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' + + '_commitCollapsible)', + }, + _diffAgainst: String, + /** @type {?string} */ + _latestCommitMessage: { + type: String, + value: '', + }, + _commentTabs: { + type: Object, + value: CommentTabs, + }, + _lineHeight: Number, + _changeIdCommitMessageError: { + type: String, + computed: + '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)', + }, + /** @type {?} */ + _patchRange: { + type: Object, + }, + _filesExpanded: String, + _basePatchNum: String, + _selectedRevision: Object, + _currentRevisionActions: Object, + _allPatchSets: { + type: Array, + computed: 'computeAllPatchSets(_change, _change.revisions.*)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _loading: Boolean, + /** @type {?} */ + _projectConfig: Object, + _rebaseOnCurrent: Boolean, + _replyButtonLabel: { + type: String, + value: 'Reply', + computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)', + }, + _selectedPatchSet: String, + _shownFileCount: Number, + _initialLoadComplete: { + type: Boolean, + value: false, + }, + _replyDisabled: { + type: Boolean, + value: true, + computed: '_computeReplyDisabled(_serverConfig)', + }, + _changeStatus: { + type: String, + computed: 'changeStatusString(_change)', + }, + _changeStatuses: { + type: String, + computed: + '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)', + }, + /** If false, then the "Show more" button was used to expand. */ + _commitCollapsed: { + type: Boolean, + value: true, + }, + /** Is the "Show more/less" button visible? */ + _commitCollapsible: { + type: Boolean, + computed: '_computeCommitCollapsible(_latestCommitMessage)', + }, + _relatedChangesCollapsed: { + type: Boolean, + value: true, + }, + /** @type {?number} */ + _updateCheckTimerHandle: Number, + _editMode: { + type: Boolean, + computed: '_computeEditMode(_patchRange.*, params.*)', + }, + _showRelatedToggle: { + type: Boolean, + value: false, + observer: '_updateToggleContainerClass', + }, + _parentIsCurrent: Boolean, + _submitEnabled: { + type: Boolean, + computed: '_isSubmitEnabled(_currentRevisionActions)', + }, - /** - * Fired if an error occurs when fetching the change data. - * - * @event page-error - */ + /** @type {?} */ + _mergeable: { + type: Boolean, + value: undefined, + }, + _currentView: { + type: Number, + value: CommentTabs.CHANGE_LOG, + }, + _showFileTabContent: { + type: Boolean, + value: true, + }, + /** @type {Array<string>} */ + _dynamicTabHeaderEndpoints: { + type: Array, + }, + /** @type {Array<string>} */ + _dynamicTabContentEndpoints: { + type: Array, + }, + // The dynamic content of the plugin added tab + _selectedTabPluginEndpoint: { + type: String, + }, + // The dynamic heading of the plugin added tab + _selectedTabPluginHeader: { + type: String, + }, + _robotCommentsPatchSetDropdownItems: { + type: Array, + value() { return []; }, + computed: '_computeRobotCommentsPatchSetDropdownItems(_change, ' + + '_commentThreads)', + }, + _currentRobotCommentsPatchSet: { + type: Number, + }, + _files_tab_name: { + type: String, + value: FILES_TAB_NAME, + }, + _findings_tab_name: { + type: String, + value: FINDINGS_TAB_NAME, + }, + _currentTabName: { + type: String, + value: FILES_TAB_NAME, + }, + _showAllRobotComments: { + type: Boolean, + value: false, + }, + _showRobotCommentsButton: { + type: Boolean, + value: false, + }, + }; + } - /** - * Fired if being logged in is required. - * - * @event show-auth-required - */ + static get observers() { + return [ + '_labelsChanged(_change.labels.*)', + '_paramsAndChangeChanged(params, _change)', + '_patchNumChanged(_patchRange.patchNum)', + ]; + } - static get properties() { - return { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - /** @type {?} */ - viewState: { - type: Object, - notify: true, - value() { return {}; }, - observer: '_viewStateChanged', - }, - backPage: String, - hasParent: Boolean, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - disableEdit: { - type: Boolean, - value: false, - }, - disableDiffPrefs: { - type: Boolean, - value: false, - }, - _diffPrefsDisabled: { - type: Boolean, - computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', - }, - _commentThreads: Array, - // TODO(taoalpha): Consider replacing diffDrafts - // with _draftCommentThreads everywhere, currently only - // replaced in reply-dialoig - _draftCommentThreads: { - type: Array, - }, - _robotCommentThreads: { - type: Array, - computed: '_computeRobotCommentThreads(_commentThreads,' - + ' _currentRobotCommentsPatchSet, _showAllRobotComments)', - }, - /** @type {?} */ - _serverConfig: { - type: Object, - observer: '_startUpdateCheckTimer', - }, - _diffPrefs: Object, - _numFilesShown: { - type: Number, - value: DEFAULT_NUM_FILES_SHOWN, - observer: '_numFilesShownChanged', - }, - _account: { - type: Object, - value: {}, - }, - _prefs: Object, - /** @type {?} */ - _changeComments: Object, - _canStartReview: { - type: Boolean, - computed: '_computeCanStartReview(_change)', - }, - _comments: Object, - /** @type {?} */ - _change: { - type: Object, - observer: '_changeChanged', - }, - _revisionInfo: { - type: Object, - computed: '_getRevisionInfo(_change)', - }, - /** @type {?} */ - _commitInfo: Object, - _currentRevision: { - type: Object, - computed: '_computeCurrentRevision(_change.current_revision, ' + - '_change.revisions)', - observer: '_handleCurrentRevisionUpdate', - }, - _files: Object, - _changeNum: String, - _diffDrafts: { - type: Object, - value() { return {}; }, - }, - _editingCommitMessage: { - type: Boolean, - value: false, - }, - _hideEditCommitMessage: { - type: Boolean, - computed: '_computeHideEditCommitMessage(_loggedIn, ' + - '_editingCommitMessage, _change, _editMode, _commitCollapsed, ' + - '_commitCollapsible)', - }, - _diffAgainst: String, - /** @type {?string} */ - _latestCommitMessage: { - type: String, - value: '', - }, - _commentTabs: { - type: Object, - value: CommentTabs, - }, - _lineHeight: Number, - _changeIdCommitMessageError: { - type: String, - computed: - '_computeChangeIdCommitMessageError(_latestCommitMessage, _change)', - }, - /** @type {?} */ - _patchRange: { - type: Object, - }, - _filesExpanded: String, - _basePatchNum: String, - _selectedRevision: Object, - _currentRevisionActions: Object, - _allPatchSets: { - type: Array, - computed: 'computeAllPatchSets(_change, _change.revisions.*)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _loading: Boolean, - /** @type {?} */ - _projectConfig: Object, - _rebaseOnCurrent: Boolean, - _replyButtonLabel: { - type: String, - value: 'Reply', - computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)', - }, - _selectedPatchSet: String, - _shownFileCount: Number, - _initialLoadComplete: { - type: Boolean, - value: false, - }, - _replyDisabled: { - type: Boolean, - value: true, - computed: '_computeReplyDisabled(_serverConfig)', - }, - _changeStatus: { - type: String, - computed: 'changeStatusString(_change)', - }, - _changeStatuses: { - type: String, - computed: - '_computeChangeStatusChips(_change, _mergeable, _submitEnabled)', - }, - /** If false, then the "Show more" button was used to expand. */ - _commitCollapsed: { - type: Boolean, - value: true, - }, - /** Is the "Show more/less" button visible? */ - _commitCollapsible: { - type: Boolean, - computed: '_computeCommitCollapsible(_latestCommitMessage)', - }, - _relatedChangesCollapsed: { - type: Boolean, - value: true, - }, - /** @type {?number} */ - _updateCheckTimerHandle: Number, - _editMode: { - type: Boolean, - computed: '_computeEditMode(_patchRange.*, params.*)', - }, - _showRelatedToggle: { - type: Boolean, - value: false, - observer: '_updateToggleContainerClass', - }, - _parentIsCurrent: Boolean, - _submitEnabled: { - type: Boolean, - computed: '_isSubmitEnabled(_currentRevisionActions)', - }, + keyboardShortcuts() { + return { + [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding + [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding + [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange', + [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog', + [this.Shortcut.OPEN_DOWNLOAD_DIALOG]: + '_handleOpenDownloadDialogShortcut', + [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode', + [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar', + [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard', + [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages', + [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages', + [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut', + [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic', + }; + } - /** @type {?} */ - _mergeable: { - type: Boolean, - value: undefined, - }, - _currentView: { - type: Number, - value: CommentTabs.CHANGE_LOG, - }, - _showFileTabContent: { - type: Boolean, - value: true, - }, - /** @type {Array<string>} */ - _dynamicTabHeaderEndpoints: { - type: Array, - }, - /** @type {Array<string>} */ - _dynamicTabContentEndpoints: { - type: Array, - }, - // The dynamic content of the plugin added tab - _selectedTabPluginEndpoint: { - type: String, - }, - // The dynamic heading of the plugin added tab - _selectedTabPluginHeader: { - type: String, - }, - _robotCommentsPatchSetDropdownItems: { - type: Array, - value() { return []; }, - computed: '_computeRobotCommentsPatchSetDropdownItems(_change, ' + - '_commentThreads)', - }, - _currentRobotCommentsPatchSet: { - type: Number, - }, - _files_tab_name: { - type: String, - value: FILES_TAB_NAME, - }, - _findings_tab_name: { - type: String, - value: FINDINGS_TAB_NAME, - }, - _currentTabName: { - type: String, - value: FILES_TAB_NAME, - }, - _showAllRobotComments: { - type: Boolean, - value: false, - }, - _showRobotCommentsButton: { - type: Boolean, - value: false, - }, - }; - } + /** @override */ + created() { + super.created(); - static get observers() { - return [ - '_labelsChanged(_change.labels.*)', - '_paramsAndChangeChanged(params, _change)', - '_patchNumChanged(_patchRange.patchNum)', - ]; - } + this.addEventListener('topic-changed', + () => this._handleTopicChanged()); - keyboardShortcuts() { - return { - [this.Shortcut.SEND_REPLY]: null, // DOC_ONLY binding - [this.Shortcut.EMOJI_DROPDOWN]: null, // DOC_ONLY binding - [this.Shortcut.REFRESH_CHANGE]: '_handleRefreshChange', - [this.Shortcut.OPEN_REPLY_DIALOG]: '_handleOpenReplyDialog', - [this.Shortcut.OPEN_DOWNLOAD_DIALOG]: - '_handleOpenDownloadDialogShortcut', - [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode', - [this.Shortcut.TOGGLE_CHANGE_STAR]: '_handleToggleChangeStar', - [this.Shortcut.UP_TO_DASHBOARD]: '_handleUpToDashboard', - [this.Shortcut.EXPAND_ALL_MESSAGES]: '_handleExpandAllMessages', - [this.Shortcut.COLLAPSE_ALL_MESSAGES]: '_handleCollapseAllMessages', - [this.Shortcut.OPEN_DIFF_PREFS]: '_handleOpenDiffPrefsShortcut', - [this.Shortcut.EDIT_TOPIC]: '_handleEditTopic', - }; - } + this.addEventListener( + // When an overlay is opened in a mobile viewport, the overlay has a full + // screen view. When it has a full screen view, we do not want the + // background to be scrollable. This will eliminate background scroll by + // hiding most of the contents on the screen upon opening, and showing + // again upon closing. + 'fullscreen-overlay-opened', + () => this._handleHideBackgroundContent()); - /** @override */ - created() { - super.created(); - - this.addEventListener('topic-changed', - () => this._handleTopicChanged()); - - this.addEventListener( - // When an overlay is opened in a mobile viewport, the overlay has a full - // screen view. When it has a full screen view, we do not want the - // background to be scrollable. This will eliminate background scroll by - // hiding most of the contents on the screen upon opening, and showing - // again upon closing. - 'fullscreen-overlay-opened', - () => this._handleHideBackgroundContent()); - - this.addEventListener('fullscreen-overlay-closed', - () => this._handleShowBackgroundContent()); - - this.addEventListener('diff-comments-modified', - () => this._handleReloadCommentThreads()); - } - - /** @override */ - attached() { - super.attached(); - this._getServerConfig().then(config => { - this._serverConfig = config; - }); - - this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - if (loggedIn) { - this.$.restAPI.getAccount().then(acct => { - this._account = acct; - }); - } - this._setDiffViewMode(); - }); - - Gerrit.awaitPluginsLoaded() - .then(() => { - this._dynamicTabHeaderEndpoints = - Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header'); - this._dynamicTabContentEndpoints = - Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content'); - if (this._dynamicTabContentEndpoints.length !== - this._dynamicTabHeaderEndpoints.length) { - console.warn('Different number of tab headers and tab content.'); - } - }) - .then(() => this._setPrimaryTab()); - - this.addEventListener('comment-save', this._handleCommentSave.bind(this)); - this.addEventListener('comment-refresh', this._reloadDrafts.bind(this)); - this.addEventListener('comment-discard', - this._handleCommentDiscard.bind(this)); - this.addEventListener('change-message-deleted', - () => this._reload()); - this.addEventListener('editable-content-save', - this._handleCommitMessageSave.bind(this)); - this.addEventListener('editable-content-cancel', - this._handleCommitMessageCancel.bind(this)); - this.addEventListener('open-fix-preview', - this._onOpenFixPreview.bind(this)); - this.addEventListener('close-fix-preview', - this._onCloseFixPreview.bind(this)); - this.listen(window, 'scroll', '_handleScroll'); - this.listen(document, 'visibilitychange', '_handleVisibilityChange'); - } - - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'scroll', '_handleScroll'); - this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); - - if (this._updateCheckTimerHandle) { - this._cancelUpdateCheckTimer(); - } - } - - get messagesList() { - return this.shadowRoot.querySelector('gr-messages-list'); - } - - get threadList() { - return this.shadowRoot.querySelector('gr-thread-list'); - } - - /** - * @param {boolean=} opt_reset - */ - _setDiffViewMode(opt_reset) { - if (!opt_reset && this.viewState.diffViewMode) { return; } - - return this._getPreferences() - .then( prefs => { - if (!this.viewState.diffMode) { - this.set('viewState.diffMode', prefs.default_diff_view); - } - }) - .then(() => { - if (!this.viewState.diffMode) { - this.set('viewState.diffMode', 'SIDE_BY_SIDE'); - } - }); - } - - _onOpenFixPreview(e) { - this.$.applyFixDialog.open(e); - } - - _onCloseFixPreview(e) { - this._reload(); - } - - _handleToggleDiffMode(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) { - this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED); - } else { - this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE); - } - } - - _handleCommentTabChange() { - this._currentView = this.$.commentTabs.selected; - const type = Object.keys(CommentTabs).find(key => CommentTabs[key] === - this._currentView); - this.$.reporting.reportInteraction('comment-tab-changed', {tabName: - type}); - } - - _isSelectedView(currentView, view) { - return currentView === view; - } - - _findIfTabMatches(currentTab, tab) { - return currentTab === tab; - } - - _handleFileTabChange(e) { - const selectedIndex = e.target.selected; - const tabs = e.target.querySelectorAll('paper-tab'); - this._currentTabName = tabs[selectedIndex] && - tabs[selectedIndex].dataset.name; - const source = e && e.type ? e.type : ''; - const pluginIndex = (this._dynamicTabHeaderEndpoints || []).indexOf( - this._currentTabName); - if (pluginIndex !== -1) { - this._selectedTabPluginEndpoint = this._dynamicTabContentEndpoints[ - pluginIndex]; - this._selectedTabPluginHeader = this._dynamicTabHeaderEndpoints[ - pluginIndex]; - } else { - this._selectedTabPluginEndpoint = ''; - this._selectedTabPluginHeader = ''; - } - this.$.reporting.reportInteraction('tab-changed', - {tabName: this._currentTabName, source}); - } - - _handleShowTab(e) { - const primaryTabs = this.shadowRoot.querySelector('#primaryTabs'); - const tabs = primaryTabs.querySelectorAll('paper-tab'); - let idx = -1; - tabs.forEach((tab, index) => { - if (tab.dataset.name === e.detail.tab) idx = index; - }); - if (idx === -1) { - console.error(e.detail.tab + ' tab not found'); - return; - } - primaryTabs.selected = idx; - primaryTabs.scrollIntoView(); - this.$.reporting.reportInteraction('show-tab', {tabName: e.detail.tab}); - } - - _handleEditCommitMessage(e) { - this._editingCommitMessage = true; - this.$.commitMessageEditor.focusTextarea(); - } - - _handleCommitMessageSave(e) { - // Trim trailing whitespace from each line. - const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, ''); - - this.$.jsAPI.handleCommitMessage(this._change, message); - - this.$.commitMessageEditor.disabled = true; - this.$.restAPI.putChangeCommitMessage( - this._changeNum, message).then(resp => { - this.$.commitMessageEditor.disabled = false; - if (!resp.ok) { return; } - - this._latestCommitMessage = this._prepareCommitMsgForLinkify( - message); - this._editingCommitMessage = false; - this._reloadWindow(); - }) - .catch(err => { - this.$.commitMessageEditor.disabled = false; - }); - } - - _reloadWindow() { - window.location.reload(); - } - - _handleCommitMessageCancel(e) { - this._editingCommitMessage = false; - } - - _computeChangeStatusChips(change, mergeable, submitEnabled) { - // Polymer 2: check for undefined - if ([ - change, - mergeable, - ].some(arg => arg === undefined)) { - // To keep consistent with Polymer 1, we are returning undefined - // if not all dependencies are defined - return undefined; - } - - // Show no chips until mergeability is loaded. - if (mergeable === null) { - return []; - } - - const options = { - includeDerived: true, - mergeable: !!mergeable, - submitEnabled: !!submitEnabled, - }; - return this.changeStatuses(change, options); - } - - _computeHideEditCommitMessage( - loggedIn, editing, change, editMode, collapsed, collapsible) { - if (!loggedIn || editing || - (change && change.status === this.ChangeStatus.MERGED) || - editMode || - (collapsed && collapsible)) { - return true; - } - - return false; - } - - _robotCommentCountPerPatchSet(threads) { - return threads.reduce((robotCommentCountMap, thread) => { - const comments = thread.comments; - const robotCommentsCount = comments.reduce((acc, comment) => - (comment.robot_id ? acc + 1 : acc), 0); - robotCommentCountMap[comments[0].patch_set] = - (robotCommentCountMap[comments[0].patch_set] || 0) + - robotCommentsCount; - return robotCommentCountMap; - }, {}); - } - - _computeText(patch, commentThreads) { - const commentCount = this._robotCommentCountPerPatchSet(commentThreads); - const commentCnt = commentCount[patch._number] || 0; - if (commentCnt === 0) return `Patchset ${patch._number}`; - const findingsText = commentCnt === 1 ? 'finding' : 'findings'; - return `Patchset ${patch._number}` - + ` (${commentCnt} ${findingsText})`; - } - - _computeRobotCommentsPatchSetDropdownItems(change, commentThreads) { - if (!change || !commentThreads || !change.revisions) return []; - - return Object.values(change.revisions) - .filter(patch => patch._number !== 'edit') - .map(patch => { - return { - text: this._computeText(patch, commentThreads), - value: patch._number, - }; - }) - .sort((a, b) => b.value - a.value); - } - - _handleCurrentRevisionUpdate(currentRevision) { - this._currentRobotCommentsPatchSet = currentRevision._number; - } - - _handleRobotCommentPatchSetChanged(e) { - const patchSet = parseInt(e.detail.value); - if (patchSet === this._currentRobotCommentsPatchSet) return; - this._currentRobotCommentsPatchSet = patchSet; - } - - _computeShowText(showAllRobotComments) { - return showAllRobotComments ? 'Show Less' : 'Show more'; - } - - _toggleShowRobotComments() { - this._showAllRobotComments = !this._showAllRobotComments; - } - - _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet, - showAllRobotComments) { - if (!commentThreads || !currentRobotCommentsPatchSet) return []; - const threads = commentThreads.filter(thread => { - const comments = thread.comments || []; - return comments.length && comments[0].robot_id && (comments[0].patch_set - === currentRobotCommentsPatchSet); - }); - this._showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT; - return threads.slice(0, showAllRobotComments ? undefined : - ROBOT_COMMENTS_LIMIT); - } - - _handleReloadCommentThreads() { - // Get any new drafts that have been saved in the diff view and show - // in the comment thread view. - this._reloadDrafts().then(() => { - this._commentThreads = this._changeComments.getAllThreadsForChange() - .map(c => Object.assign({}, c)); - Polymer.dom.flush(); - }); - } - - _handleReloadDiffComments(e) { - // Keeps the file list counts updated. - this._reloadDrafts().then(() => { - // Get any new drafts that have been saved in the thread view and show - // in the diff view. - this.$.fileList.reloadCommentsForThreadWithRootId(e.detail.rootId, - e.detail.path); - Polymer.dom.flush(); - }); - } - - _computeTotalCommentCounts(unresolvedCount, changeComments) { - if (!changeComments) return undefined; - const draftCount = changeComments.computeDraftCount(); - const unresolvedString = GrCountStringFormatter.computeString( - unresolvedCount, 'unresolved'); - const draftString = GrCountStringFormatter.computePluralString( - draftCount, 'draft'); - - return unresolvedString + - // Add a comma and space if both unresolved and draft comments exist. - (unresolvedString && draftString ? ', ' : '') + - draftString; - } - - _handleCommentSave(e) { - const draft = e.detail.comment; - if (!draft.__draft) { return; } - - draft.patch_set = draft.patch_set || this._patchRange.patchNum; - - // The use of path-based notification helpers (set, push) can’t be used - // because the paths could contain dots in them. A new object must be - // created to satisfy Polymer’s dirty checking. - // https://github.com/Polymer/polymer/issues/3127 - const diffDrafts = Object.assign({}, this._diffDrafts); - if (!diffDrafts[draft.path]) { - diffDrafts[draft.path] = [draft]; - this._diffDrafts = diffDrafts; - return; - } - for (let i = 0; i < this._diffDrafts[draft.path].length; i++) { - if (this._diffDrafts[draft.path][i].id === draft.id) { - diffDrafts[draft.path][i] = draft; - this._diffDrafts = diffDrafts; - return; - } - } - diffDrafts[draft.path].push(draft); - diffDrafts[draft.path].sort((c1, c2) => - // No line number means that it’s a file comment. Sort it above the - // others. - (c1.line || -1) - (c2.line || -1) - ); - this._diffDrafts = diffDrafts; - } - - _handleCommentDiscard(e) { - const draft = e.detail.comment; - if (!draft.__draft) { return; } - - if (!this._diffDrafts[draft.path]) { - return; - } - let index = -1; - for (let i = 0; i < this._diffDrafts[draft.path].length; i++) { - if (this._diffDrafts[draft.path][i].id === draft.id) { - index = i; - break; - } - } - if (index === -1) { - // It may be a draft that hasn’t been added to _diffDrafts since it was - // never saved. - return; - } - - draft.patch_set = draft.patch_set || this._patchRange.patchNum; - - // The use of path-based notification helpers (set, push) can’t be used - // because the paths could contain dots in them. A new object must be - // created to satisfy Polymer’s dirty checking. - // https://github.com/Polymer/polymer/issues/3127 - const diffDrafts = Object.assign({}, this._diffDrafts); - diffDrafts[draft.path].splice(index, 1); - if (diffDrafts[draft.path].length === 0) { - delete diffDrafts[draft.path]; - } - this._diffDrafts = diffDrafts; - } - - _handleReplyTap(e) { - e.preventDefault(); - this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); - } - - _handleOpenDiffPrefs() { - this.$.fileList.openDiffPrefs(); - } - - _handleOpenIncludedInDialog() { - this.$.includedInDialog.loadData().then(() => { - Polymer.dom.flush(); - this.$.includedInOverlay.refit(); - }); - this.$.includedInOverlay.open(); - } - - _handleIncludedInDialogClose(e) { - this.$.includedInOverlay.close(); - } - - _handleOpenDownloadDialog() { - this.$.downloadOverlay.open().then(() => { - this.$.downloadOverlay - .setFocusStops(this.$.downloadDialog.getFocusStops()); - this.$.downloadDialog.focus(); - }); - } - - _handleDownloadDialogClose(e) { - this.$.downloadOverlay.close(); - } - - _handleOpenUploadHelpDialog(e) { - this.$.uploadHelpOverlay.open(); - } - - _handleCloseUploadHelpDialog(e) { - this.$.uploadHelpOverlay.close(); - } - - _handleMessageReply(e) { - const msg = e.detail.message.message; - const quoteStr = msg.split('\n').map( - line => '> ' + line) - .join('\n') + '\n\n'; - this.$.replyDialog.quote = quoteStr; - this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY); - } - - _handleHideBackgroundContent() { - this.$.mainContent.classList.add('overlayOpen'); - } - - _handleShowBackgroundContent() { - this.$.mainContent.classList.remove('overlayOpen'); - } - - _handleReplySent(e) { - this.addEventListener('change-details-loaded', - () => { - this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL); - }, {once: true}); - this.$.replyOverlay.close(); - this._reload(); - } - - _handleReplyCancel(e) { - this.$.replyOverlay.close(); - } - - _handleReplyAutogrow(e) { - // If the textarea resizes, we need to re-fit the overlay. - this.debounce('reply-overlay-refit', () => { - this.$.replyOverlay.refit(); - }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS); - } - - _handleShowReplyDialog(e) { - let target = this.$.replyDialog.FocusTarget.REVIEWERS; - if (e.detail.value && e.detail.value.ccsOnly) { - target = this.$.replyDialog.FocusTarget.CCS; - } - this._openReplyDialog(target); - } - - _handleScroll() { - this.debounce('scroll', () => { - this.viewState.scrollTop = document.body.scrollTop; - }, 150); - } - - _setShownFiles(e) { - this._shownFileCount = e.detail.length; - } - - _expandAllDiffs() { - this.$.fileList.expandAllDiffs(); - } - - _collapseAllDiffs() { - this.$.fileList.collapseAllDiffs(); - } - - _paramsChanged(value) { - this._currentView = CommentTabs.CHANGE_LOG; - this._setPrimaryTab(); - if (value.view !== Gerrit.Nav.View.CHANGE) { - this._initialLoadComplete = false; - return; - } - - if (value.changeNum && value.project) { - this.$.restAPI.setInProjectLookup(value.changeNum, value.project); - } - - const patchChanged = this._patchRange && - (value.patchNum !== undefined && value.basePatchNum !== undefined) && - (this._patchRange.patchNum !== value.patchNum || - this._patchRange.basePatchNum !== value.basePatchNum); - - if (this._changeNum !== value.changeNum) { - this._initialLoadComplete = false; - } + this.addEventListener('fullscreen-overlay-closed', + () => this._handleShowBackgroundContent()); - const patchRange = { - patchNum: value.patchNum, - basePatchNum: value.basePatchNum || 'PARENT', - }; + this.addEventListener('diff-comments-modified', + () => this._handleReloadCommentThreads()); + } - this.$.fileList.collapseAllDiffs(); - this._patchRange = patchRange; + /** @override */ + attached() { + super.attached(); + this._getServerConfig().then(config => { + this._serverConfig = config; + }); - // If the change has already been loaded and the parameter change is only - // in the patch range, then don't do a full reload. - if (this._initialLoadComplete && patchChanged) { - if (patchRange.patchNum == null) { - patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets); - } - this._reloadPatchNumDependentResources().then(() => { - this._sendShowChangeEvent(); + this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + if (loggedIn) { + this.$.restAPI.getAccount().then(acct => { + this._account = acct; }); - return; } + this._setDiffViewMode(); + }); - this._changeNum = value.changeNum; - this.$.relatedChanges.clear(); - - this._reload(true).then(() => { - this._performPostLoadTasks(); - }); - } - - _sendShowChangeEvent() { - this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { - change: this._change, - patchNum: this._patchRange.patchNum, - info: {mergeable: this._mergeable}, - }); - } - - _setPrimaryTab() { - // Selected has to be set after the paper-tabs are visible, because - // the selected underline depends on calculations made by the browser. - // paper-tabs depends on iron-resizable-behavior, which only fires on - // attached() without using RenderStatus.beforeNextRender. Not changing - // this when migrating from Polymer 1 to 2 was probably an oversight by - // the paper component maintainers. - // https://polymer-library.polymer-project.org/2.0/docs/upgrade#attach-time-attached-connectedcallback - // By calling _onTabSizingChanged() we are reaching into the private API - // of paper-tabs, but we believe this workaround is acceptable for the - // time being. - Polymer.RenderStatus.beforeNextRender(this, () => { - this.$.commentTabs.selected = 0; - this.$.commentTabs._onTabSizingChanged(); - const primaryTabs = this.shadowRoot.querySelector('#primaryTabs'); - if (primaryTabs) { - primaryTabs.selected = 0; - primaryTabs._onTabSizingChanged(); - } - }); - } - - _performPostLoadTasks() { - this._maybeShowReplyDialog(); - this._maybeShowRevertDialog(); - - this._sendShowChangeEvent(); - - this.async(() => { - if (this.viewState.scrollTop) { - document.documentElement.scrollTop = - document.body.scrollTop = this.viewState.scrollTop; - } else { - this._maybeScrollToMessage(window.location.hash); - } - this._initialLoadComplete = true; - }); - } - - _paramsAndChangeChanged(value, change) { - // Polymer 2: check for undefined - if ([value, change].some(arg => arg === undefined)) { - return; - } - - // If the change number or patch range is different, then reset the - // selected file index. - const patchRangeState = this.viewState.patchRange; - if (this.viewState.changeNum !== this._changeNum || - patchRangeState.basePatchNum !== this._patchRange.basePatchNum || - patchRangeState.patchNum !== this._patchRange.patchNum) { - this._resetFileListViewState(); - } - } - - _viewStateChanged(viewState) { - this._numFilesShown = viewState.numFilesShown ? - viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN; - } - - _numFilesShownChanged(numFilesShown) { - this.viewState.numFilesShown = numFilesShown; - } - - _handleMessageAnchorTap(e) { - const hash = MSG_PREFIX + e.detail.id; - const url = Gerrit.Nav.getUrlForChange(this._change, - this._patchRange.patchNum, this._patchRange.basePatchNum, - this._editMode, hash); - history.replaceState(null, '', url); - } - - _maybeScrollToMessage(hash) { - if (hash.startsWith(MSG_PREFIX)) { - this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length)); - } - } - - _getLocationSearch() { - // Not inlining to make it easier to test. - return window.location.search; - } - - _getUrlParameter(param) { - const pageURL = this._getLocationSearch().substring(1); - const vars = pageURL.split('&'); - for (let i = 0; i < vars.length; i++) { - const name = vars[i].split('='); - if (name[0] == param) { - return name[0]; - } - } - return null; - } - - _maybeShowRevertDialog() { - Gerrit.awaitPluginsLoaded() - .then(this._getLoggedIn.bind(this)) - .then(loggedIn => { - if (!loggedIn || !this._change || - this._change.status !== this.ChangeStatus.MERGED) { - // Do not display dialog if not logged-in or the change is not - // merged. - return; - } - if (this._getUrlParameter('revert')) { - this.$.actions.showRevertDialog(); - } - }); - } - - _maybeShowReplyDialog() { - this._getLoggedIn().then(loggedIn => { - if (!loggedIn) { return; } - - if (this.viewState.showReplyDialog) { - this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); - // TODO(kaspern@): Find a better signal for when to call center. - this.async(() => { this.$.replyOverlay.center(); }, 100); - this.async(() => { this.$.replyOverlay.center(); }, 1000); - this.set('viewState.showReplyDialog', false); - } - }); - } - - _resetFileListViewState() { - this.set('viewState.selectedFileIndex', 0); - this.set('viewState.scrollTop', 0); - if (!!this.viewState.changeNum && - this.viewState.changeNum !== this._changeNum) { - // Reset the diff mode to null when navigating from one change to - // another, so that the user's preference is restored. - this._setDiffViewMode(true); - this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN); - } - this.set('viewState.changeNum', this._changeNum); - this.set('viewState.patchRange', this._patchRange); - } - - _changeChanged(change) { - if (!change || !this._patchRange || !this._allPatchSets) { return; } - - // We get the parent first so we keep the original value for basePatchNum - // and not the updated value. - const parent = this._getBasePatchNum(change, this._patchRange); - - this.set('_patchRange.patchNum', this._patchRange.patchNum || - this.computeLatestPatchNum(this._allPatchSets)); - - this.set('_patchRange.basePatchNum', parent); - - const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; - this.fire('title-change', {title}); - } - - /** - * Gets base patch number, if it is a parent try and decide from - * preference whether to default to `auto merge`, `Parent 1` or `PARENT`. - * - * @param {Object} change - * @param {Object} patchRange - * @return {number|string} - */ - _getBasePatchNum(change, patchRange) { - if (patchRange.basePatchNum && - patchRange.basePatchNum !== 'PARENT') { - return patchRange.basePatchNum; - } - - const revisionInfo = this._getRevisionInfo(change); - if (!revisionInfo) return 'PARENT'; - - const parentCounts = revisionInfo.getParentCountMap(); - // check that there is at least 2 parents otherwise fall back to 1, - // which means there is only one parent. - const parentCount = parentCounts.hasOwnProperty(1) ? - parentCounts[1] : 1; - - const preferFirst = this._prefs && - this._prefs.default_base_for_merges === 'FIRST_PARENT'; - - if (parentCount > 1 && preferFirst && !patchRange.patchNum) { - return -1; - } - - return 'PARENT'; - } - - _computeChangeUrl(change) { - return Gerrit.Nav.getUrlForChange(change); - } - - _computeShowCommitInfo(changeStatus, current_revision) { - return changeStatus === 'Merged' && current_revision; - } - - _computeMergedCommitInfo(current_revision, revisions) { - const rev = revisions[current_revision]; - if (!rev || !rev.commit) { return {}; } - // CommitInfo.commit is optional. Set commit in all cases to avoid error - // in <gr-commit-info>. @see Issue 5337 - if (!rev.commit.commit) { rev.commit.commit = current_revision; } - return rev.commit; - } - - _computeChangeIdClass(displayChangeId) { - return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : ''; - } - - _computeTitleAttributeWarning(displayChangeId) { - if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) { - return 'Change-Id mismatch'; - } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) { - return 'No Change-Id in commit message'; - } - } - - _computeChangeIdCommitMessageError(commitMessage, change) { - // Polymer 2: check for undefined - if ([commitMessage, change].some(arg => arg === undefined)) { - return undefined; - } - - if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; } - - // Find the last match in the commit message: - let changeId; - let changeIdArr; - - while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) { - changeId = changeIdArr[1]; - } - - if (changeId) { - // A change-id is detected in the commit message. - - if (changeId === change.change_id) { - // The change-id found matches the real change-id. - return null; - } - // The change-id found does not match the change-id. - return CHANGE_ID_ERROR.MISMATCH; - } - // There is no change-id in the commit message. - return CHANGE_ID_ERROR.MISSING; - } - - _computeLabelNames(labels) { - return Object.keys(labels).sort(); - } - - _computeLabelValues(labelName, labels) { - const result = []; - const t = labels[labelName]; - if (!t) { return result; } - const approvals = t.all || []; - for (const label of approvals) { - if (label.value && label.value != labels[labelName].default_value) { - let labelClassName; - let labelValPrefix = ''; - if (label.value > 0) { - labelValPrefix = '+'; - labelClassName = 'approved'; - } else if (label.value < 0) { - labelClassName = 'notApproved'; + Gerrit.awaitPluginsLoaded() + .then(() => { + this._dynamicTabHeaderEndpoints = + Gerrit._endpoints.getDynamicEndpoints('change-view-tab-header'); + this._dynamicTabContentEndpoints = + Gerrit._endpoints.getDynamicEndpoints('change-view-tab-content'); + if (this._dynamicTabContentEndpoints.length !== + this._dynamicTabHeaderEndpoints.length) { + console.warn('Different number of tab headers and tab content.'); } - result.push({ - value: labelValPrefix + label.value, - className: labelClassName, - account: label, - }); - } - } - return result; - } + }) + .then(() => this._setPrimaryTab()); - _computeReplyButtonLabel(changeRecord, canStartReview) { - // Polymer 2: check for undefined - if ([changeRecord, canStartReview].some(arg => arg === undefined)) { - return 'Reply'; - } + this.addEventListener('comment-save', this._handleCommentSave.bind(this)); + this.addEventListener('comment-refresh', this._reloadDrafts.bind(this)); + this.addEventListener('comment-discard', + this._handleCommentDiscard.bind(this)); + this.addEventListener('change-message-deleted', + () => this._reload()); + this.addEventListener('editable-content-save', + this._handleCommitMessageSave.bind(this)); + this.addEventListener('editable-content-cancel', + this._handleCommitMessageCancel.bind(this)); + this.addEventListener('open-fix-preview', + this._onOpenFixPreview.bind(this)); + this.addEventListener('close-fix-preview', + this._onCloseFixPreview.bind(this)); + this.listen(window, 'scroll', '_handleScroll'); + this.listen(document, 'visibilitychange', '_handleVisibilityChange'); + } - if (canStartReview) { - return 'Start review'; - } + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'scroll', '_handleScroll'); + this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); - const drafts = (changeRecord && changeRecord.base) || {}; - const draftCount = Object.keys(drafts) - .reduce((count, file) => count + drafts[file].length, 0); - - let label = 'Reply'; - if (draftCount > 0) { - label += ' (' + draftCount + ')'; - } - return label; - } - - _handleOpenReplyDialog(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { - return; - } - this._getLoggedIn().then(isLoggedIn => { - if (!isLoggedIn) { - this.fire('show-auth-required'); - return; - } - - e.preventDefault(); - this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); - }); - } - - _handleOpenDownloadDialogShortcut(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.downloadOverlay.open(); - } - - _handleEditTopic(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.metadata.editTopic(); - } - - _handleRefreshChange(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - e.preventDefault(); - Gerrit.Nav.navigateToChange(this._change); - } - - _handleToggleChangeStar(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.changeStar.toggleStar(); - } - - _handleUpToDashboard(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this._determinePageBack(); - } - - _handleExpandAllMessages(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.messagesList.handleExpandCollapse(true); - } - - _handleCollapseAllMessages(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.messagesList.handleExpandCollapse(false); - } - - _handleOpenDiffPrefsShortcut(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - if (this._diffPrefsDisabled) { return; } - - e.preventDefault(); - this.$.fileList.openDiffPrefs(); - } - - _determinePageBack() { - // Default backPage to root if user came to change view page - // via an email link, etc. - Gerrit.Nav.navigateToRelativeUrl(this.backPage || - Gerrit.Nav.getUrlForRoot()); - } - - _handleLabelRemoved(splices, path) { - for (const splice of splices) { - for (const removed of splice.removed) { - const changePath = path.split('.'); - const labelPath = changePath.splice(0, changePath.length - 2); - const labelDict = this.get(labelPath); - if (labelDict.approved && - labelDict.approved._account_id === removed._account_id) { - this._reload(); - return; - } - } - } - } - - _labelsChanged(changeRecord) { - if (!changeRecord) { return; } - if (changeRecord.value && changeRecord.value.indexSplices) { - this._handleLabelRemoved(changeRecord.value.indexSplices, - changeRecord.path); - } - this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, { - change: this._change, - }); - } - - /** - * @param {string=} opt_section - */ - _openReplyDialog(opt_section) { - this.$.replyOverlay.open().finally(() => { - // the following code should be executed no matter open succeed or not - this._resetReplyOverlayFocusStops(); - this.$.replyDialog.open(opt_section); - Polymer.dom.flush(); - this.$.replyOverlay.center(); - }); - } - - _handleReloadChange(e) { - return this._reload().then(() => { - // If the change was rebased or submitted, we need to reload the page - // with the latest patch. - const action = e.detail.action; - if (action === 'rebase' || action === 'submit') { - Gerrit.Nav.navigateToChange(this._change); - } - }); - } - - _handleGetChangeDetailError(response) { - this.fire('page-error', {response}); - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _getServerConfig() { - return this.$.restAPI.getConfig(); - } - - _getProjectConfig() { - if (!this._change) return; - return this.$.restAPI.getProjectConfig(this._change.project).then( - config => { - this._projectConfig = config; - }); - } - - _getPreferences() { - return this.$.restAPI.getPreferences(); - } - - _prepareCommitMsgForLinkify(msg) { - // TODO(wyatta) switch linkify sequence, see issue 5526. - // This is a zero-with space. It is added to prevent the linkify library - // from including R= or CC= as part of the email address. - return msg.replace(REVIEWERS_REGEX, '$1=\u200B'); - } - - /** - * Utility function to make the necessary modifications to a change in the - * case an edit exists. - * - * @param {!Object} change - * @param {?Object} edit - */ - _processEdit(change, edit) { - if (!edit) { return; } - change.revisions[edit.commit.commit] = { - _number: this.EDIT_NAME, - basePatchNum: edit.base_patch_set_number, - commit: edit.commit, - fetch: edit.fetch, - }; - // If the edit is based on the most recent patchset, load it by - // default, unless another patch set to load was specified in the URL. - if (!this._patchRange.patchNum && - change.current_revision === edit.base_revision) { - change.current_revision = edit.commit.commit; - this.set('_patchRange.patchNum', this.EDIT_NAME); - // Because edits are fibbed as revisions and added to the revisions - // array, and revision actions are always derived from the 'latest' - // patch set, we must copy over actions from the patch set base. - // Context: Issue 7243 - change.revisions[edit.commit.commit].actions = - change.revisions[edit.base_revision].actions; - } - } - - _getChangeDetail() { - const detailCompletes = this.$.restAPI.getChangeDetail( - this._changeNum, this._handleGetChangeDetailError.bind(this)); - const editCompletes = this._getEdit(); - const prefCompletes = this._getPreferences(); - - return Promise.all([detailCompletes, editCompletes, prefCompletes]) - .then(([change, edit, prefs]) => { - this._prefs = prefs; - - if (!change) { - return ''; - } - this._processEdit(change, edit); - // Issue 4190: Coalesce missing topics to null. - if (!change.topic) { change.topic = null; } - if (!change.reviewer_updates) { - change.reviewer_updates = null; - } - const latestRevisionSha = this._getLatestRevisionSHA(change); - const currentRevision = change.revisions[latestRevisionSha]; - if (currentRevision.commit && currentRevision.commit.message) { - this._latestCommitMessage = this._prepareCommitMsgForLinkify( - currentRevision.commit.message); - } else { - this._latestCommitMessage = null; - } - - const lineHeight = getComputedStyle(this).lineHeight; - - // Slice returns a number as a string, convert to an int. - this._lineHeight = - parseInt(lineHeight.slice(0, lineHeight.length - 2), 10); - - this._change = change; - if (!this._patchRange || !this._patchRange.patchNum || - this.patchNumEquals(this._patchRange.patchNum, - currentRevision._number)) { - // CommitInfo.commit is optional, and may need patching. - if (!currentRevision.commit.commit) { - currentRevision.commit.commit = latestRevisionSha; - } - this._commitInfo = currentRevision.commit; - this._selectedRevision = currentRevision; - // TODO: Fetch and process files. - } else { - this._selectedRevision = - Object.values(this._change.revisions).find( - revision => { - // edit patchset is a special one - const thePatchNum = this._patchRange.patchNum; - if (thePatchNum === 'edit') { - return revision._number === thePatchNum; - } - return revision._number === parseInt(thePatchNum, 10); - }); - } - }); - } - - _isSubmitEnabled(revisionActions) { - return !!(revisionActions && revisionActions.submit && - revisionActions.submit.enabled); - } - - _getEdit() { - return this.$.restAPI.getChangeEdit(this._changeNum, true); - } - - _getLatestCommitMessage() { - return this.$.restAPI.getChangeCommitInfo(this._changeNum, - this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => { - if (!commitInfo) return Promise.resolve(); - this._latestCommitMessage = - this._prepareCommitMsgForLinkify(commitInfo.message); - }); - } - - _getLatestRevisionSHA(change) { - if (change.current_revision) { - return change.current_revision; - } - // current_revision may not be present in the case where the latest rev is - // a draft and the user doesn’t have permission to view that rev. - let latestRev = null; - let latestPatchNum = -1; - for (const rev in change.revisions) { - if (!change.revisions.hasOwnProperty(rev)) { continue; } - - if (change.revisions[rev]._number > latestPatchNum) { - latestRev = rev; - latestPatchNum = change.revisions[rev]._number; - } - } - return latestRev; - } - - _getCommitInfo() { - return this.$.restAPI.getChangeCommitInfo( - this._changeNum, this._patchRange.patchNum).then( - commitInfo => { - this._commitInfo = commitInfo; - }); - } - - _reloadDraftsWithCallback(e) { - return this._reloadDrafts().then(() => e.detail.resolve()); - } - - /** - * Fetches a new changeComment object, and data for all types of comments - * (comments, robot comments, draft comments) is requested. - */ - _reloadComments() { - return this.$.commentAPI.loadAll(this._changeNum) - .then(comments => this._recomputeComments(comments)); - } - - /** - * Fetches a new changeComment object, but only updated data for drafts is - * requested. - * - * TODO(taoalpha): clean up this and _reloadComments, as single comment - * can be a thread so it does not make sense to only update drafts - * without updating threads - */ - _reloadDrafts() { - return this.$.commentAPI.reloadDrafts(this._changeNum) - .then(comments => this._recomputeComments(comments)); - } - - _recomputeComments(comments) { - this._changeComments = comments; - this._diffDrafts = Object.assign({}, this._changeComments.drafts); - this._commentThreads = this._changeComments.getAllThreadsForChange() - .map(c => Object.assign({}, c)); - this._draftCommentThreads = this._commentThreads - .filter(c => c.comments[c.comments.length - 1].__draft); - } - - /** - * Reload the change. - * - * @param {boolean=} opt_isLocationChange Reloads the related changes - * when true and ends reporting events that started on location change. - * @return {Promise} A promise that resolves when the core data has loaded. - * Some non-core data loading may still be in-flight when the core data - * promise resolves. - */ - _reload(opt_isLocationChange) { - this._loading = true; - this._relatedChangesCollapsed = true; - this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL); - this.$.reporting.time(CHANGE_DATA_TIMING_LABEL); - - // Array to house all promises related to data requests. - const allDataPromises = []; - - // Resolves when the change detail and the edit patch set (if available) - // are loaded. - const detailCompletes = this._getChangeDetail(); - allDataPromises.push(detailCompletes); - - // Resolves when the loading flag is set to false, meaning that some - // change content may start appearing. - const loadingFlagSet = detailCompletes - .then(() => { - this._loading = false; - this.dispatchEvent(new CustomEvent('change-details-loaded', - {bubbles: true, composed: true})); - }) - .then(() => { - this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL); - if (opt_isLocationChange) { - this.$.reporting.changeDisplayed(); - } - }); - - // Resolves when the project config has loaded. - const projectConfigLoaded = detailCompletes - .then(() => this._getProjectConfig()); - allDataPromises.push(projectConfigLoaded); - - // Resolves when change comments have loaded (comments, drafts and robot - // comments). - const commentsLoaded = this._reloadComments(); - allDataPromises.push(commentsLoaded); - - let coreDataPromise; - - // If the patch number is specified - if (this._patchRange && this._patchRange.patchNum) { - // Because a specific patchset is specified, reload the resources that - // are keyed by patch number or patch range. - const patchResourcesLoaded = this._reloadPatchNumDependentResources(); - allDataPromises.push(patchResourcesLoaded); - - // Promise resolves when the change detail and patch dependent resources - // have loaded. - const detailAndPatchResourcesLoaded = - Promise.all([patchResourcesLoaded, loadingFlagSet]); - - // Promise resolves when mergeability information has loaded. - const mergeabilityLoaded = detailAndPatchResourcesLoaded - .then(() => this._getMergeability()); - allDataPromises.push(mergeabilityLoaded); - - // Promise resovles when the change actions have loaded. - const actionsLoaded = detailAndPatchResourcesLoaded - .then(() => this.$.actions.reload()); - allDataPromises.push(actionsLoaded); - - // The core data is loaded when both mergeability and actions are known. - coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]); - } else { - // Resolves when the file list has loaded. - const fileListReload = loadingFlagSet - .then(() => this.$.fileList.reload()); - allDataPromises.push(fileListReload); - - const latestCommitMessageLoaded = loadingFlagSet.then(() => { - // If the latest commit message is known, there is nothing to do. - if (this._latestCommitMessage) { return Promise.resolve(); } - return this._getLatestCommitMessage(); - }); - allDataPromises.push(latestCommitMessageLoaded); - - // Promise resolves when mergeability information has loaded. - const mergeabilityLoaded = loadingFlagSet - .then(() => this._getMergeability()); - allDataPromises.push(mergeabilityLoaded); - - // Core data is loaded when mergeability has been loaded. - coreDataPromise = mergeabilityLoaded; - } - - if (opt_isLocationChange) { - const relatedChangesLoaded = coreDataPromise - .then(() => this.$.relatedChanges.reload()); - allDataPromises.push(relatedChangesLoaded); - } - - Promise.all(allDataPromises).then(() => { - this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL); - if (opt_isLocationChange) { - this.$.reporting.changeFullyLoaded(); - } - }); - - return coreDataPromise; - } - - /** - * Kicks off requests for resources that rely on the patch range - * (`this._patchRange`) being defined. - */ - _reloadPatchNumDependentResources() { - return Promise.all([ - this._getCommitInfo(), - this.$.fileList.reload(), - ]); - } - - _getMergeability() { - if (!this._change) { - this._mergeable = null; - return Promise.resolve(); - } - // If the change is closed, it is not mergeable. Note: already merged - // changes are obviously not mergeable, but the mergeability API will not - // answer for abandoned changes. - if (this._change.status === this.ChangeStatus.MERGED || - this._change.status === this.ChangeStatus.ABANDONED) { - this._mergeable = false; - return Promise.resolve(); - } - - this._mergeable = null; - return this.$.restAPI.getMergeable(this._changeNum).then(m => { - this._mergeable = m.mergeable; - }); - } - - _computeCanStartReview(change) { - return !!(change.actions && change.actions.ready && - change.actions.ready.enabled); - } - - _computeReplyDisabled() { return false; } - - _computeChangePermalinkAriaLabel(changeNum) { - return 'Change ' + changeNum; - } - - _computeCommitMessageCollapsed(collapsed, collapsible) { - return collapsible && collapsed; - } - - _computeRelatedChangesClass(collapsed) { - return collapsed ? 'collapsed' : ''; - } - - _computeCollapseText(collapsed) { - // Symbols are up and down triangles. - return collapsed ? '\u25bc Show more' : '\u25b2 Show less'; - } - - /** - * Returns the text to be copied when - * click the copy icon next to change subject - * - * @param {!Object} change - */ - _computeCopyTextForTitle(change) { - return `${change._number}: ${change.subject}` + - ` | https://${location.host}${this._computeChangeUrl(change)}`; - } - - _toggleCommitCollapsed() { - this._commitCollapsed = !this._commitCollapsed; - if (this._commitCollapsed) { - window.scrollTo(0, 0); - } - } - - _toggleRelatedChangesCollapsed() { - this._relatedChangesCollapsed = !this._relatedChangesCollapsed; - if (this._relatedChangesCollapsed) { - window.scrollTo(0, 0); - } - } - - _computeCommitCollapsible(commitMessage) { - if (!commitMessage) { return false; } - return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE; - } - - _getOffsetHeight(element) { - return element.offsetHeight; - } - - _getScrollHeight(element) { - return element.scrollHeight; - } - - /** - * Get the line height of an element to the nearest integer. - */ - _getLineHeight(element) { - const lineHeightStr = getComputedStyle(element).lineHeight; - return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2)); - } - - /** - * New max height for the related changes section, shorter than the existing - * change info height. - */ - _updateRelatedChangeMaxHeight() { - // Takes into account approximate height for the expand button and - // bottom margin. - const EXTRA_HEIGHT = 30; - let newHeight; - - if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`) - .matches) { - // In a small (mobile) view, give the relation chain some space. - newHeight = SMALL_RELATED_HEIGHT; - } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`) - .matches) { - // Since related changes are below the commit message, but still next to - // metadata, the height should be the height of the metadata minus the - // height of the commit message to reduce jank. However, if that doesn't - // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT. - // Note: extraHeight is to take into account margin/padding. - const medRelatedHeight = Math.max( - this._getOffsetHeight(this.$.mainChangeInfo) - - this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT, - MINIMUM_RELATED_MAX_HEIGHT); - newHeight = medRelatedHeight; - } else { - if (this._commitCollapsible) { - // Make sure the content is lined up if both areas have buttons. If - // the commit message is not collapsed, instead use the change info - // height. - newHeight = this._getOffsetHeight(this.$.commitMessage); - } else { - newHeight = this._getOffsetHeight(this.$.commitAndRelated) - - EXTRA_HEIGHT; - } - } - const stylesToUpdate = {}; - - // Get the line height of related changes, and convert it to the nearest - // integer. - const lineHeight = this._getLineHeight(this.$.relatedChanges); - - // Figure out a new height that is divisible by the rounded line height. - const remainder = newHeight % lineHeight; - newHeight = newHeight - remainder; - - stylesToUpdate['--relation-chain-max-height'] = newHeight + 'px'; - - // Update the max-height of the relation chain to this new height. - if (this._commitCollapsible) { - stylesToUpdate['--related-change-btn-top-padding'] = remainder + 'px'; - } - - this.updateStyles(stylesToUpdate); - } - - _computeShowRelatedToggle() { - // Make sure the max height has been applied, since there is now content - // to populate. - if (!util.getComputedStyleValue('--relation-chain-max-height', this)) { - this._updateRelatedChangeMaxHeight(); - } - // Prevents showMore from showing when click on related change, since the - // line height would be positive, but related changes height is 0. - if (!this._getScrollHeight(this.$.relatedChanges)) { - return this._showRelatedToggle = false; - } - - if (this._getScrollHeight(this.$.relatedChanges) > - (this._getOffsetHeight(this.$.relatedChanges) + - this._getLineHeight(this.$.relatedChanges))) { - return this._showRelatedToggle = true; - } - this._showRelatedToggle = false; - } - - _updateToggleContainerClass(showRelatedToggle) { - if (showRelatedToggle) { - this.$.relatedChangesToggle.classList.add('showToggle'); - } else { - this.$.relatedChangesToggle.classList.remove('showToggle'); - } - } - - _startUpdateCheckTimer() { - if (!this._serverConfig || - !this._serverConfig.change || - this._serverConfig.change.update_delay === undefined || - this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) { - return; - } - - this._updateCheckTimerHandle = this.async(() => { - this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => { - let toastMessage = null; - if (!result.isLatest) { - toastMessage = ReloadToastMessage.NEWER_REVISION; - } else if (result.newStatus === this.ChangeStatus.MERGED) { - toastMessage = ReloadToastMessage.MERGED; - } else if (result.newStatus === this.ChangeStatus.ABANDONED) { - toastMessage = ReloadToastMessage.ABANDONED; - } else if (result.newStatus === this.ChangeStatus.NEW) { - toastMessage = ReloadToastMessage.RESTORED; - } else if (result.newMessages) { - toastMessage = ReloadToastMessage.NEW_MESSAGE; - } - - if (!toastMessage) { - this._startUpdateCheckTimer(); - return; - } - - this._cancelUpdateCheckTimer(); - this.fire('show-alert', { - message: toastMessage, - // Persist this alert. - dismissOnNavigation: true, - action: 'Reload', - callback: function() { - // Load the current change without any patch range. - Gerrit.Nav.navigateToChange(this._change); - }.bind(this), - }); - }); - }, this._serverConfig.change.update_delay * 1000); - } - - _cancelUpdateCheckTimer() { - if (this._updateCheckTimerHandle) { - this.cancelAsync(this._updateCheckTimerHandle); - } - this._updateCheckTimerHandle = null; - } - - _handleVisibilityChange() { - if (document.hidden && this._updateCheckTimerHandle) { - this._cancelUpdateCheckTimer(); - } else if (!this._updateCheckTimerHandle) { - this._startUpdateCheckTimer(); - } - } - - _handleTopicChanged() { - this.$.relatedChanges.reload(); - } - - _computeHeaderClass(editMode) { - const classes = ['header']; - if (editMode) { classes.push('editMode'); } - return classes.join(' '); - } - - _computeEditMode(patchRangeRecord, paramsRecord) { - if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) { - return undefined; - } - - if (paramsRecord.base && paramsRecord.base.edit) { return true; } - - const patchRange = patchRangeRecord.base || {}; - return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); - } - - _handleFileActionTap(e) { - e.preventDefault(); - const controls = this.$.fileListHeader.$.editControls; - const path = e.detail.path; - switch (e.detail.action) { - case GrEditConstants.Actions.DELETE.id: - controls.openDeleteDialog(path); - break; - case GrEditConstants.Actions.OPEN.id: - Gerrit.Nav.navigateToRelativeUrl( - Gerrit.Nav.getEditUrlForDiff(this._change, path, - this._patchRange.patchNum)); - break; - case GrEditConstants.Actions.RENAME.id: - controls.openRenameDialog(path); - break; - case GrEditConstants.Actions.RESTORE.id: - controls.openRestoreDialog(path); - break; - } - } - - _computeCommitMessageKey(number, revision) { - return `c${number}_rev${revision}`; - } - - _patchNumChanged(patchNumStr) { - if (!this._selectedRevision) { - return; - } - - let patchNum = parseInt(patchNumStr, 10); - if (patchNumStr === 'edit') { - patchNum = patchNumStr; - } - - if (patchNum === this._selectedRevision._number) { - return; - } - this._selectedRevision = Object.values(this._change.revisions).find( - revision => revision._number === patchNum); - } - - /** - * If an edit exists already, load it. Otherwise, toggle edit mode via the - * navigation API. - */ - _handleEditTap() { - const editInfo = Object.values(this._change.revisions).find(info => - info._number === this.EDIT_NAME); - - if (editInfo) { - Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME); - return; - } - - // Avoid putting patch set in the URL unless a non-latest patch set is - // selected. - let patchNum; - if (!this.patchNumEquals(this._patchRange.patchNum, - this.computeLatestPatchNum(this._allPatchSets))) { - patchNum = this._patchRange.patchNum; - } - Gerrit.Nav.navigateToChange(this._change, patchNum, null, true); - } - - _handleStopEditTap() { - Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum); - } - - _resetReplyOverlayFocusStops() { - this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops()); - } - - _handleToggleStar(e) { - this.$.restAPI.saveChangeStarred(e.detail.change._number, - e.detail.starred); - } - - _getRevisionInfo(change) { - return new Gerrit.RevisionInfo(change); - } - - _computeCurrentRevision(currentRevision, revisions) { - return currentRevision && revisions && revisions[currentRevision]; - } - - _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { - return disableDiffPrefs || !loggedIn; + if (this._updateCheckTimerHandle) { + this._cancelUpdateCheckTimer(); } } - customElements.define(GrChangeView.is, GrChangeView); -})(); + get messagesList() { + return this.shadowRoot.querySelector('gr-messages-list'); + } + + get threadList() { + return this.shadowRoot.querySelector('gr-thread-list'); + } + + /** + * @param {boolean=} opt_reset + */ + _setDiffViewMode(opt_reset) { + if (!opt_reset && this.viewState.diffViewMode) { return; } + + return this._getPreferences() + .then( prefs => { + if (!this.viewState.diffMode) { + this.set('viewState.diffMode', prefs.default_diff_view); + } + }) + .then(() => { + if (!this.viewState.diffMode) { + this.set('viewState.diffMode', 'SIDE_BY_SIDE'); + } + }); + } + + _onOpenFixPreview(e) { + this.$.applyFixDialog.open(e); + } + + _onCloseFixPreview(e) { + this._reload(); + } + + _handleToggleDiffMode(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + if (this.viewState.diffMode === DiffViewMode.SIDE_BY_SIDE) { + this.$.fileListHeader.setDiffViewMode(DiffViewMode.UNIFIED); + } else { + this.$.fileListHeader.setDiffViewMode(DiffViewMode.SIDE_BY_SIDE); + } + } + + _handleCommentTabChange() { + this._currentView = this.$.commentTabs.selected; + const type = Object.keys(CommentTabs).find(key => CommentTabs[key] === + this._currentView); + this.$.reporting.reportInteraction('comment-tab-changed', {tabName: + type}); + } + + _isSelectedView(currentView, view) { + return currentView === view; + } + + _findIfTabMatches(currentTab, tab) { + return currentTab === tab; + } + + _handleFileTabChange(e) { + const selectedIndex = e.target.selected; + const tabs = e.target.querySelectorAll('paper-tab'); + this._currentTabName = tabs[selectedIndex] && + tabs[selectedIndex].dataset.name; + const source = e && e.type ? e.type : ''; + const pluginIndex = (this._dynamicTabHeaderEndpoints || []).indexOf( + this._currentTabName); + if (pluginIndex !== -1) { + this._selectedTabPluginEndpoint = this._dynamicTabContentEndpoints[ + pluginIndex]; + this._selectedTabPluginHeader = this._dynamicTabHeaderEndpoints[ + pluginIndex]; + } else { + this._selectedTabPluginEndpoint = ''; + this._selectedTabPluginHeader = ''; + } + this.$.reporting.reportInteraction('tab-changed', + {tabName: this._currentTabName, source}); + } + + _handleShowTab(e) { + const primaryTabs = this.shadowRoot.querySelector('#primaryTabs'); + const tabs = primaryTabs.querySelectorAll('paper-tab'); + let idx = -1; + tabs.forEach((tab, index) => { + if (tab.dataset.name === e.detail.tab) idx = index; + }); + if (idx === -1) { + console.error(e.detail.tab + ' tab not found'); + return; + } + primaryTabs.selected = idx; + primaryTabs.scrollIntoView(); + this.$.reporting.reportInteraction('show-tab', {tabName: e.detail.tab}); + } + + _handleEditCommitMessage(e) { + this._editingCommitMessage = true; + this.$.commitMessageEditor.focusTextarea(); + } + + _handleCommitMessageSave(e) { + // Trim trailing whitespace from each line. + const message = e.detail.content.replace(TRAILING_WHITESPACE_REGEX, ''); + + this.$.jsAPI.handleCommitMessage(this._change, message); + + this.$.commitMessageEditor.disabled = true; + this.$.restAPI.putChangeCommitMessage( + this._changeNum, message).then(resp => { + this.$.commitMessageEditor.disabled = false; + if (!resp.ok) { return; } + + this._latestCommitMessage = this._prepareCommitMsgForLinkify( + message); + this._editingCommitMessage = false; + this._reloadWindow(); + }) + .catch(err => { + this.$.commitMessageEditor.disabled = false; + }); + } + + _reloadWindow() { + window.location.reload(); + } + + _handleCommitMessageCancel(e) { + this._editingCommitMessage = false; + } + + _computeChangeStatusChips(change, mergeable, submitEnabled) { + // Polymer 2: check for undefined + if ([ + change, + mergeable, + ].some(arg => arg === undefined)) { + // To keep consistent with Polymer 1, we are returning undefined + // if not all dependencies are defined + return undefined; + } + + // Show no chips until mergeability is loaded. + if (mergeable === null) { + return []; + } + + const options = { + includeDerived: true, + mergeable: !!mergeable, + submitEnabled: !!submitEnabled, + }; + return this.changeStatuses(change, options); + } + + _computeHideEditCommitMessage( + loggedIn, editing, change, editMode, collapsed, collapsible) { + if (!loggedIn || editing || + (change && change.status === this.ChangeStatus.MERGED) || + editMode || + (collapsed && collapsible)) { + return true; + } + + return false; + } + + _robotCommentCountPerPatchSet(threads) { + return threads.reduce((robotCommentCountMap, thread) => { + const comments = thread.comments; + const robotCommentsCount = comments.reduce((acc, comment) => + (comment.robot_id ? acc + 1 : acc), 0); + robotCommentCountMap[comments[0].patch_set] = + (robotCommentCountMap[comments[0].patch_set] || 0) + + robotCommentsCount; + return robotCommentCountMap; + }, {}); + } + + _computeText(patch, commentThreads) { + const commentCount = this._robotCommentCountPerPatchSet(commentThreads); + const commentCnt = commentCount[patch._number] || 0; + if (commentCnt === 0) return `Patchset ${patch._number}`; + const findingsText = commentCnt === 1 ? 'finding' : 'findings'; + return `Patchset ${patch._number}` + + ` (${commentCnt} ${findingsText})`; + } + + _computeRobotCommentsPatchSetDropdownItems(change, commentThreads) { + if (!change || !commentThreads || !change.revisions) return []; + + return Object.values(change.revisions) + .filter(patch => patch._number !== 'edit') + .map(patch => { + return { + text: this._computeText(patch, commentThreads), + value: patch._number, + }; + }) + .sort((a, b) => b.value - a.value); + } + + _handleCurrentRevisionUpdate(currentRevision) { + this._currentRobotCommentsPatchSet = currentRevision._number; + } + + _handleRobotCommentPatchSetChanged(e) { + const patchSet = parseInt(e.detail.value); + if (patchSet === this._currentRobotCommentsPatchSet) return; + this._currentRobotCommentsPatchSet = patchSet; + } + + _computeShowText(showAllRobotComments) { + return showAllRobotComments ? 'Show Less' : 'Show more'; + } + + _toggleShowRobotComments() { + this._showAllRobotComments = !this._showAllRobotComments; + } + + _computeRobotCommentThreads(commentThreads, currentRobotCommentsPatchSet, + showAllRobotComments) { + if (!commentThreads || !currentRobotCommentsPatchSet) return []; + const threads = commentThreads.filter(thread => { + const comments = thread.comments || []; + return comments.length && comments[0].robot_id && (comments[0].patch_set + === currentRobotCommentsPatchSet); + }); + this._showRobotCommentsButton = threads.length > ROBOT_COMMENTS_LIMIT; + return threads.slice(0, showAllRobotComments ? undefined : + ROBOT_COMMENTS_LIMIT); + } + + _handleReloadCommentThreads() { + // Get any new drafts that have been saved in the diff view and show + // in the comment thread view. + this._reloadDrafts().then(() => { + this._commentThreads = this._changeComments.getAllThreadsForChange() + .map(c => Object.assign({}, c)); + flush(); + }); + } + + _handleReloadDiffComments(e) { + // Keeps the file list counts updated. + this._reloadDrafts().then(() => { + // Get any new drafts that have been saved in the thread view and show + // in the diff view. + this.$.fileList.reloadCommentsForThreadWithRootId(e.detail.rootId, + e.detail.path); + flush(); + }); + } + + _computeTotalCommentCounts(unresolvedCount, changeComments) { + if (!changeComments) return undefined; + const draftCount = changeComments.computeDraftCount(); + const unresolvedString = GrCountStringFormatter.computeString( + unresolvedCount, 'unresolved'); + const draftString = GrCountStringFormatter.computePluralString( + draftCount, 'draft'); + + return unresolvedString + + // Add a comma and space if both unresolved and draft comments exist. + (unresolvedString && draftString ? ', ' : '') + + draftString; + } + + _handleCommentSave(e) { + const draft = e.detail.comment; + if (!draft.__draft) { return; } + + draft.patch_set = draft.patch_set || this._patchRange.patchNum; + + // The use of path-based notification helpers (set, push) can’t be used + // because the paths could contain dots in them. A new object must be + // created to satisfy Polymer’s dirty checking. + // https://github.com/Polymer/polymer/issues/3127 + const diffDrafts = Object.assign({}, this._diffDrafts); + if (!diffDrafts[draft.path]) { + diffDrafts[draft.path] = [draft]; + this._diffDrafts = diffDrafts; + return; + } + for (let i = 0; i < this._diffDrafts[draft.path].length; i++) { + if (this._diffDrafts[draft.path][i].id === draft.id) { + diffDrafts[draft.path][i] = draft; + this._diffDrafts = diffDrafts; + return; + } + } + diffDrafts[draft.path].push(draft); + diffDrafts[draft.path].sort((c1, c2) => + // No line number means that it’s a file comment. Sort it above the + // others. + (c1.line || -1) - (c2.line || -1) + ); + this._diffDrafts = diffDrafts; + } + + _handleCommentDiscard(e) { + const draft = e.detail.comment; + if (!draft.__draft) { return; } + + if (!this._diffDrafts[draft.path]) { + return; + } + let index = -1; + for (let i = 0; i < this._diffDrafts[draft.path].length; i++) { + if (this._diffDrafts[draft.path][i].id === draft.id) { + index = i; + break; + } + } + if (index === -1) { + // It may be a draft that hasn’t been added to _diffDrafts since it was + // never saved. + return; + } + + draft.patch_set = draft.patch_set || this._patchRange.patchNum; + + // The use of path-based notification helpers (set, push) can’t be used + // because the paths could contain dots in them. A new object must be + // created to satisfy Polymer’s dirty checking. + // https://github.com/Polymer/polymer/issues/3127 + const diffDrafts = Object.assign({}, this._diffDrafts); + diffDrafts[draft.path].splice(index, 1); + if (diffDrafts[draft.path].length === 0) { + delete diffDrafts[draft.path]; + } + this._diffDrafts = diffDrafts; + } + + _handleReplyTap(e) { + e.preventDefault(); + this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); + } + + _handleOpenDiffPrefs() { + this.$.fileList.openDiffPrefs(); + } + + _handleOpenIncludedInDialog() { + this.$.includedInDialog.loadData().then(() => { + flush(); + this.$.includedInOverlay.refit(); + }); + this.$.includedInOverlay.open(); + } + + _handleIncludedInDialogClose(e) { + this.$.includedInOverlay.close(); + } + + _handleOpenDownloadDialog() { + this.$.downloadOverlay.open().then(() => { + this.$.downloadOverlay + .setFocusStops(this.$.downloadDialog.getFocusStops()); + this.$.downloadDialog.focus(); + }); + } + + _handleDownloadDialogClose(e) { + this.$.downloadOverlay.close(); + } + + _handleOpenUploadHelpDialog(e) { + this.$.uploadHelpOverlay.open(); + } + + _handleCloseUploadHelpDialog(e) { + this.$.uploadHelpOverlay.close(); + } + + _handleMessageReply(e) { + const msg = e.detail.message.message; + const quoteStr = msg.split('\n').map( + line => '> ' + line) + .join('\n') + '\n\n'; + this.$.replyDialog.quote = quoteStr; + this._openReplyDialog(this.$.replyDialog.FocusTarget.BODY); + } + + _handleHideBackgroundContent() { + this.$.mainContent.classList.add('overlayOpen'); + } + + _handleShowBackgroundContent() { + this.$.mainContent.classList.remove('overlayOpen'); + } + + _handleReplySent(e) { + this.addEventListener('change-details-loaded', + () => { + this.$.reporting.timeEnd(SEND_REPLY_TIMING_LABEL); + }, {once: true}); + this.$.replyOverlay.close(); + this._reload(); + } + + _handleReplyCancel(e) { + this.$.replyOverlay.close(); + } + + _handleReplyAutogrow(e) { + // If the textarea resizes, we need to re-fit the overlay. + this.debounce('reply-overlay-refit', () => { + this.$.replyOverlay.refit(); + }, REPLY_REFIT_DEBOUNCE_INTERVAL_MS); + } + + _handleShowReplyDialog(e) { + let target = this.$.replyDialog.FocusTarget.REVIEWERS; + if (e.detail.value && e.detail.value.ccsOnly) { + target = this.$.replyDialog.FocusTarget.CCS; + } + this._openReplyDialog(target); + } + + _handleScroll() { + this.debounce('scroll', () => { + this.viewState.scrollTop = document.body.scrollTop; + }, 150); + } + + _setShownFiles(e) { + this._shownFileCount = e.detail.length; + } + + _expandAllDiffs() { + this.$.fileList.expandAllDiffs(); + } + + _collapseAllDiffs() { + this.$.fileList.collapseAllDiffs(); + } + + _paramsChanged(value) { + this._currentView = CommentTabs.CHANGE_LOG; + this._setPrimaryTab(); + if (value.view !== Gerrit.Nav.View.CHANGE) { + this._initialLoadComplete = false; + return; + } + + if (value.changeNum && value.project) { + this.$.restAPI.setInProjectLookup(value.changeNum, value.project); + } + + const patchChanged = this._patchRange && + (value.patchNum !== undefined && value.basePatchNum !== undefined) && + (this._patchRange.patchNum !== value.patchNum || + this._patchRange.basePatchNum !== value.basePatchNum); + + if (this._changeNum !== value.changeNum) { + this._initialLoadComplete = false; + } + + const patchRange = { + patchNum: value.patchNum, + basePatchNum: value.basePatchNum || 'PARENT', + }; + + this.$.fileList.collapseAllDiffs(); + this._patchRange = patchRange; + + // If the change has already been loaded and the parameter change is only + // in the patch range, then don't do a full reload. + if (this._initialLoadComplete && patchChanged) { + if (patchRange.patchNum == null) { + patchRange.patchNum = this.computeLatestPatchNum(this._allPatchSets); + } + this._reloadPatchNumDependentResources().then(() => { + this._sendShowChangeEvent(); + }); + return; + } + + this._changeNum = value.changeNum; + this.$.relatedChanges.clear(); + + this._reload(true).then(() => { + this._performPostLoadTasks(); + }); + } + + _sendShowChangeEvent() { + this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.SHOW_CHANGE, { + change: this._change, + patchNum: this._patchRange.patchNum, + info: {mergeable: this._mergeable}, + }); + } + + _setPrimaryTab() { + // Selected has to be set after the paper-tabs are visible, because + // the selected underline depends on calculations made by the browser. + // paper-tabs depends on iron-resizable-behavior, which only fires on + // attached() without using RenderStatus.beforeNextRender. Not changing + // this when migrating from Polymer 1 to 2 was probably an oversight by + // the paper component maintainers. + // https://polymer-library.polymer-project.org/2.0/docs/upgrade#attach-time-attached-connectedcallback + // By calling _onTabSizingChanged() we are reaching into the private API + // of paper-tabs, but we believe this workaround is acceptable for the + // time being. + beforeNextRender(this, () => { + this.$.commentTabs.selected = 0; + this.$.commentTabs._onTabSizingChanged(); + const primaryTabs = this.shadowRoot.querySelector('#primaryTabs'); + if (primaryTabs) { + primaryTabs.selected = 0; + primaryTabs._onTabSizingChanged(); + } + }); + } + + _performPostLoadTasks() { + this._maybeShowReplyDialog(); + this._maybeShowRevertDialog(); + + this._sendShowChangeEvent(); + + this.async(() => { + if (this.viewState.scrollTop) { + document.documentElement.scrollTop = + document.body.scrollTop = this.viewState.scrollTop; + } else { + this._maybeScrollToMessage(window.location.hash); + } + this._initialLoadComplete = true; + }); + } + + _paramsAndChangeChanged(value, change) { + // Polymer 2: check for undefined + if ([value, change].some(arg => arg === undefined)) { + return; + } + + // If the change number or patch range is different, then reset the + // selected file index. + const patchRangeState = this.viewState.patchRange; + if (this.viewState.changeNum !== this._changeNum || + patchRangeState.basePatchNum !== this._patchRange.basePatchNum || + patchRangeState.patchNum !== this._patchRange.patchNum) { + this._resetFileListViewState(); + } + } + + _viewStateChanged(viewState) { + this._numFilesShown = viewState.numFilesShown ? + viewState.numFilesShown : DEFAULT_NUM_FILES_SHOWN; + } + + _numFilesShownChanged(numFilesShown) { + this.viewState.numFilesShown = numFilesShown; + } + + _handleMessageAnchorTap(e) { + const hash = MSG_PREFIX + e.detail.id; + const url = Gerrit.Nav.getUrlForChange(this._change, + this._patchRange.patchNum, this._patchRange.basePatchNum, + this._editMode, hash); + history.replaceState(null, '', url); + } + + _maybeScrollToMessage(hash) { + if (hash.startsWith(MSG_PREFIX)) { + this.messagesList.scrollToMessage(hash.substr(MSG_PREFIX.length)); + } + } + + _getLocationSearch() { + // Not inlining to make it easier to test. + return window.location.search; + } + + _getUrlParameter(param) { + const pageURL = this._getLocationSearch().substring(1); + const vars = pageURL.split('&'); + for (let i = 0; i < vars.length; i++) { + const name = vars[i].split('='); + if (name[0] == param) { + return name[0]; + } + } + return null; + } + + _maybeShowRevertDialog() { + Gerrit.awaitPluginsLoaded() + .then(this._getLoggedIn.bind(this)) + .then(loggedIn => { + if (!loggedIn || !this._change || + this._change.status !== this.ChangeStatus.MERGED) { + // Do not display dialog if not logged-in or the change is not + // merged. + return; + } + if (this._getUrlParameter('revert')) { + this.$.actions.showRevertDialog(); + } + }); + } + + _maybeShowReplyDialog() { + this._getLoggedIn().then(loggedIn => { + if (!loggedIn) { return; } + + if (this.viewState.showReplyDialog) { + this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); + // TODO(kaspern@): Find a better signal for when to call center. + this.async(() => { this.$.replyOverlay.center(); }, 100); + this.async(() => { this.$.replyOverlay.center(); }, 1000); + this.set('viewState.showReplyDialog', false); + } + }); + } + + _resetFileListViewState() { + this.set('viewState.selectedFileIndex', 0); + this.set('viewState.scrollTop', 0); + if (!!this.viewState.changeNum && + this.viewState.changeNum !== this._changeNum) { + // Reset the diff mode to null when navigating from one change to + // another, so that the user's preference is restored. + this._setDiffViewMode(true); + this.set('_numFilesShown', DEFAULT_NUM_FILES_SHOWN); + } + this.set('viewState.changeNum', this._changeNum); + this.set('viewState.patchRange', this._patchRange); + } + + _changeChanged(change) { + if (!change || !this._patchRange || !this._allPatchSets) { return; } + + // We get the parent first so we keep the original value for basePatchNum + // and not the updated value. + const parent = this._getBasePatchNum(change, this._patchRange); + + this.set('_patchRange.patchNum', this._patchRange.patchNum || + this.computeLatestPatchNum(this._allPatchSets)); + + this.set('_patchRange.basePatchNum', parent); + + const title = change.subject + ' (' + change.change_id.substr(0, 9) + ')'; + this.fire('title-change', {title}); + } + + /** + * Gets base patch number, if it is a parent try and decide from + * preference whether to default to `auto merge`, `Parent 1` or `PARENT`. + * + * @param {Object} change + * @param {Object} patchRange + * @return {number|string} + */ + _getBasePatchNum(change, patchRange) { + if (patchRange.basePatchNum && + patchRange.basePatchNum !== 'PARENT') { + return patchRange.basePatchNum; + } + + const revisionInfo = this._getRevisionInfo(change); + if (!revisionInfo) return 'PARENT'; + + const parentCounts = revisionInfo.getParentCountMap(); + // check that there is at least 2 parents otherwise fall back to 1, + // which means there is only one parent. + const parentCount = parentCounts.hasOwnProperty(1) ? + parentCounts[1] : 1; + + const preferFirst = this._prefs && + this._prefs.default_base_for_merges === 'FIRST_PARENT'; + + if (parentCount > 1 && preferFirst && !patchRange.patchNum) { + return -1; + } + + return 'PARENT'; + } + + _computeChangeUrl(change) { + return Gerrit.Nav.getUrlForChange(change); + } + + _computeShowCommitInfo(changeStatus, current_revision) { + return changeStatus === 'Merged' && current_revision; + } + + _computeMergedCommitInfo(current_revision, revisions) { + const rev = revisions[current_revision]; + if (!rev || !rev.commit) { return {}; } + // CommitInfo.commit is optional. Set commit in all cases to avoid error + // in <gr-commit-info>. @see Issue 5337 + if (!rev.commit.commit) { rev.commit.commit = current_revision; } + return rev.commit; + } + + _computeChangeIdClass(displayChangeId) { + return displayChangeId === CHANGE_ID_ERROR.MISMATCH ? 'warning' : ''; + } + + _computeTitleAttributeWarning(displayChangeId) { + if (displayChangeId === CHANGE_ID_ERROR.MISMATCH) { + return 'Change-Id mismatch'; + } else if (displayChangeId === CHANGE_ID_ERROR.MISSING) { + return 'No Change-Id in commit message'; + } + } + + _computeChangeIdCommitMessageError(commitMessage, change) { + // Polymer 2: check for undefined + if ([commitMessage, change].some(arg => arg === undefined)) { + return undefined; + } + + if (!commitMessage) { return CHANGE_ID_ERROR.MISSING; } + + // Find the last match in the commit message: + let changeId; + let changeIdArr; + + while (changeIdArr = CHANGE_ID_REGEX_PATTERN.exec(commitMessage)) { + changeId = changeIdArr[1]; + } + + if (changeId) { + // A change-id is detected in the commit message. + + if (changeId === change.change_id) { + // The change-id found matches the real change-id. + return null; + } + // The change-id found does not match the change-id. + return CHANGE_ID_ERROR.MISMATCH; + } + // There is no change-id in the commit message. + return CHANGE_ID_ERROR.MISSING; + } + + _computeLabelNames(labels) { + return Object.keys(labels).sort(); + } + + _computeLabelValues(labelName, labels) { + const result = []; + const t = labels[labelName]; + if (!t) { return result; } + const approvals = t.all || []; + for (const label of approvals) { + if (label.value && label.value != labels[labelName].default_value) { + let labelClassName; + let labelValPrefix = ''; + if (label.value > 0) { + labelValPrefix = '+'; + labelClassName = 'approved'; + } else if (label.value < 0) { + labelClassName = 'notApproved'; + } + result.push({ + value: labelValPrefix + label.value, + className: labelClassName, + account: label, + }); + } + } + return result; + } + + _computeReplyButtonLabel(changeRecord, canStartReview) { + // Polymer 2: check for undefined + if ([changeRecord, canStartReview].some(arg => arg === undefined)) { + return 'Reply'; + } + + if (canStartReview) { + return 'Start review'; + } + + const drafts = (changeRecord && changeRecord.base) || {}; + const draftCount = Object.keys(drafts) + .reduce((count, file) => count + drafts[file].length, 0); + + let label = 'Reply'; + if (draftCount > 0) { + label += ' (' + draftCount + ')'; + } + return label; + } + + _handleOpenReplyDialog(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { + return; + } + this._getLoggedIn().then(isLoggedIn => { + if (!isLoggedIn) { + this.fire('show-auth-required'); + return; + } + + e.preventDefault(); + this._openReplyDialog(this.$.replyDialog.FocusTarget.ANY); + }); + } + + _handleOpenDownloadDialogShortcut(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.downloadOverlay.open(); + } + + _handleEditTopic(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.metadata.editTopic(); + } + + _handleRefreshChange(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + e.preventDefault(); + Gerrit.Nav.navigateToChange(this._change); + } + + _handleToggleChangeStar(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.changeStar.toggleStar(); + } + + _handleUpToDashboard(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._determinePageBack(); + } + + _handleExpandAllMessages(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.messagesList.handleExpandCollapse(true); + } + + _handleCollapseAllMessages(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.messagesList.handleExpandCollapse(false); + } + + _handleOpenDiffPrefsShortcut(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + if (this._diffPrefsDisabled) { return; } + + e.preventDefault(); + this.$.fileList.openDiffPrefs(); + } + + _determinePageBack() { + // Default backPage to root if user came to change view page + // via an email link, etc. + Gerrit.Nav.navigateToRelativeUrl(this.backPage || + Gerrit.Nav.getUrlForRoot()); + } + + _handleLabelRemoved(splices, path) { + for (const splice of splices) { + for (const removed of splice.removed) { + const changePath = path.split('.'); + const labelPath = changePath.splice(0, changePath.length - 2); + const labelDict = this.get(labelPath); + if (labelDict.approved && + labelDict.approved._account_id === removed._account_id) { + this._reload(); + return; + } + } + } + } + + _labelsChanged(changeRecord) { + if (!changeRecord) { return; } + if (changeRecord.value && changeRecord.value.indexSplices) { + this._handleLabelRemoved(changeRecord.value.indexSplices, + changeRecord.path); + } + this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.LABEL_CHANGE, { + change: this._change, + }); + } + + /** + * @param {string=} opt_section + */ + _openReplyDialog(opt_section) { + this.$.replyOverlay.open().finally(() => { + // the following code should be executed no matter open succeed or not + this._resetReplyOverlayFocusStops(); + this.$.replyDialog.open(opt_section); + flush(); + this.$.replyOverlay.center(); + }); + } + + _handleReloadChange(e) { + return this._reload().then(() => { + // If the change was rebased or submitted, we need to reload the page + // with the latest patch. + const action = e.detail.action; + if (action === 'rebase' || action === 'submit') { + Gerrit.Nav.navigateToChange(this._change); + } + }); + } + + _handleGetChangeDetailError(response) { + this.fire('page-error', {response}); + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _getServerConfig() { + return this.$.restAPI.getConfig(); + } + + _getProjectConfig() { + if (!this._change) return; + return this.$.restAPI.getProjectConfig(this._change.project).then( + config => { + this._projectConfig = config; + }); + } + + _getPreferences() { + return this.$.restAPI.getPreferences(); + } + + _prepareCommitMsgForLinkify(msg) { + // TODO(wyatta) switch linkify sequence, see issue 5526. + // This is a zero-with space. It is added to prevent the linkify library + // from including R= or CC= as part of the email address. + return msg.replace(REVIEWERS_REGEX, '$1=\u200B'); + } + + /** + * Utility function to make the necessary modifications to a change in the + * case an edit exists. + * + * @param {!Object} change + * @param {?Object} edit + */ + _processEdit(change, edit) { + if (!edit) { return; } + change.revisions[edit.commit.commit] = { + _number: this.EDIT_NAME, + basePatchNum: edit.base_patch_set_number, + commit: edit.commit, + fetch: edit.fetch, + }; + // If the edit is based on the most recent patchset, load it by + // default, unless another patch set to load was specified in the URL. + if (!this._patchRange.patchNum && + change.current_revision === edit.base_revision) { + change.current_revision = edit.commit.commit; + this.set('_patchRange.patchNum', this.EDIT_NAME); + // Because edits are fibbed as revisions and added to the revisions + // array, and revision actions are always derived from the 'latest' + // patch set, we must copy over actions from the patch set base. + // Context: Issue 7243 + change.revisions[edit.commit.commit].actions = + change.revisions[edit.base_revision].actions; + } + } + + _getChangeDetail() { + const detailCompletes = this.$.restAPI.getChangeDetail( + this._changeNum, this._handleGetChangeDetailError.bind(this)); + const editCompletes = this._getEdit(); + const prefCompletes = this._getPreferences(); + + return Promise.all([detailCompletes, editCompletes, prefCompletes]) + .then(([change, edit, prefs]) => { + this._prefs = prefs; + + if (!change) { + return ''; + } + this._processEdit(change, edit); + // Issue 4190: Coalesce missing topics to null. + if (!change.topic) { change.topic = null; } + if (!change.reviewer_updates) { + change.reviewer_updates = null; + } + const latestRevisionSha = this._getLatestRevisionSHA(change); + const currentRevision = change.revisions[latestRevisionSha]; + if (currentRevision.commit && currentRevision.commit.message) { + this._latestCommitMessage = this._prepareCommitMsgForLinkify( + currentRevision.commit.message); + } else { + this._latestCommitMessage = null; + } + + const lineHeight = getComputedStyle(this).lineHeight; + + // Slice returns a number as a string, convert to an int. + this._lineHeight = + parseInt(lineHeight.slice(0, lineHeight.length - 2), 10); + + this._change = change; + if (!this._patchRange || !this._patchRange.patchNum || + this.patchNumEquals(this._patchRange.patchNum, + currentRevision._number)) { + // CommitInfo.commit is optional, and may need patching. + if (!currentRevision.commit.commit) { + currentRevision.commit.commit = latestRevisionSha; + } + this._commitInfo = currentRevision.commit; + this._selectedRevision = currentRevision; + // TODO: Fetch and process files. + } else { + this._selectedRevision = + Object.values(this._change.revisions).find( + revision => { + // edit patchset is a special one + const thePatchNum = this._patchRange.patchNum; + if (thePatchNum === 'edit') { + return revision._number === thePatchNum; + } + return revision._number === parseInt(thePatchNum, 10); + }); + } + }); + } + + _isSubmitEnabled(revisionActions) { + return !!(revisionActions && revisionActions.submit && + revisionActions.submit.enabled); + } + + _getEdit() { + return this.$.restAPI.getChangeEdit(this._changeNum, true); + } + + _getLatestCommitMessage() { + return this.$.restAPI.getChangeCommitInfo(this._changeNum, + this.computeLatestPatchNum(this._allPatchSets)).then(commitInfo => { + if (!commitInfo) return Promise.resolve(); + this._latestCommitMessage = + this._prepareCommitMsgForLinkify(commitInfo.message); + }); + } + + _getLatestRevisionSHA(change) { + if (change.current_revision) { + return change.current_revision; + } + // current_revision may not be present in the case where the latest rev is + // a draft and the user doesn’t have permission to view that rev. + let latestRev = null; + let latestPatchNum = -1; + for (const rev in change.revisions) { + if (!change.revisions.hasOwnProperty(rev)) { continue; } + + if (change.revisions[rev]._number > latestPatchNum) { + latestRev = rev; + latestPatchNum = change.revisions[rev]._number; + } + } + return latestRev; + } + + _getCommitInfo() { + return this.$.restAPI.getChangeCommitInfo( + this._changeNum, this._patchRange.patchNum).then( + commitInfo => { + this._commitInfo = commitInfo; + }); + } + + _reloadDraftsWithCallback(e) { + return this._reloadDrafts().then(() => e.detail.resolve()); + } + + /** + * Fetches a new changeComment object, and data for all types of comments + * (comments, robot comments, draft comments) is requested. + */ + _reloadComments() { + return this.$.commentAPI.loadAll(this._changeNum) + .then(comments => this._recomputeComments(comments)); + } + + /** + * Fetches a new changeComment object, but only updated data for drafts is + * requested. + * + * TODO(taoalpha): clean up this and _reloadComments, as single comment + * can be a thread so it does not make sense to only update drafts + * without updating threads + */ + _reloadDrafts() { + return this.$.commentAPI.reloadDrafts(this._changeNum) + .then(comments => this._recomputeComments(comments)); + } + + _recomputeComments(comments) { + this._changeComments = comments; + this._diffDrafts = Object.assign({}, this._changeComments.drafts); + this._commentThreads = this._changeComments.getAllThreadsForChange() + .map(c => Object.assign({}, c)); + this._draftCommentThreads = this._commentThreads + .filter(c => c.comments[c.comments.length - 1].__draft); + } + + /** + * Reload the change. + * + * @param {boolean=} opt_isLocationChange Reloads the related changes + * when true and ends reporting events that started on location change. + * @return {Promise} A promise that resolves when the core data has loaded. + * Some non-core data loading may still be in-flight when the core data + * promise resolves. + */ + _reload(opt_isLocationChange) { + this._loading = true; + this._relatedChangesCollapsed = true; + this.$.reporting.time(CHANGE_RELOAD_TIMING_LABEL); + this.$.reporting.time(CHANGE_DATA_TIMING_LABEL); + + // Array to house all promises related to data requests. + const allDataPromises = []; + + // Resolves when the change detail and the edit patch set (if available) + // are loaded. + const detailCompletes = this._getChangeDetail(); + allDataPromises.push(detailCompletes); + + // Resolves when the loading flag is set to false, meaning that some + // change content may start appearing. + const loadingFlagSet = detailCompletes + .then(() => { + this._loading = false; + this.dispatchEvent(new CustomEvent('change-details-loaded', + {bubbles: true, composed: true})); + }) + .then(() => { + this.$.reporting.timeEnd(CHANGE_RELOAD_TIMING_LABEL); + if (opt_isLocationChange) { + this.$.reporting.changeDisplayed(); + } + }); + + // Resolves when the project config has loaded. + const projectConfigLoaded = detailCompletes + .then(() => this._getProjectConfig()); + allDataPromises.push(projectConfigLoaded); + + // Resolves when change comments have loaded (comments, drafts and robot + // comments). + const commentsLoaded = this._reloadComments(); + allDataPromises.push(commentsLoaded); + + let coreDataPromise; + + // If the patch number is specified + if (this._patchRange && this._patchRange.patchNum) { + // Because a specific patchset is specified, reload the resources that + // are keyed by patch number or patch range. + const patchResourcesLoaded = this._reloadPatchNumDependentResources(); + allDataPromises.push(patchResourcesLoaded); + + // Promise resolves when the change detail and patch dependent resources + // have loaded. + const detailAndPatchResourcesLoaded = + Promise.all([patchResourcesLoaded, loadingFlagSet]); + + // Promise resolves when mergeability information has loaded. + const mergeabilityLoaded = detailAndPatchResourcesLoaded + .then(() => this._getMergeability()); + allDataPromises.push(mergeabilityLoaded); + + // Promise resovles when the change actions have loaded. + const actionsLoaded = detailAndPatchResourcesLoaded + .then(() => this.$.actions.reload()); + allDataPromises.push(actionsLoaded); + + // The core data is loaded when both mergeability and actions are known. + coreDataPromise = Promise.all([mergeabilityLoaded, actionsLoaded]); + } else { + // Resolves when the file list has loaded. + const fileListReload = loadingFlagSet + .then(() => this.$.fileList.reload()); + allDataPromises.push(fileListReload); + + const latestCommitMessageLoaded = loadingFlagSet.then(() => { + // If the latest commit message is known, there is nothing to do. + if (this._latestCommitMessage) { return Promise.resolve(); } + return this._getLatestCommitMessage(); + }); + allDataPromises.push(latestCommitMessageLoaded); + + // Promise resolves when mergeability information has loaded. + const mergeabilityLoaded = loadingFlagSet + .then(() => this._getMergeability()); + allDataPromises.push(mergeabilityLoaded); + + // Core data is loaded when mergeability has been loaded. + coreDataPromise = mergeabilityLoaded; + } + + if (opt_isLocationChange) { + const relatedChangesLoaded = coreDataPromise + .then(() => this.$.relatedChanges.reload()); + allDataPromises.push(relatedChangesLoaded); + } + + Promise.all(allDataPromises).then(() => { + this.$.reporting.timeEnd(CHANGE_DATA_TIMING_LABEL); + if (opt_isLocationChange) { + this.$.reporting.changeFullyLoaded(); + } + }); + + return coreDataPromise; + } + + /** + * Kicks off requests for resources that rely on the patch range + * (`this._patchRange`) being defined. + */ + _reloadPatchNumDependentResources() { + return Promise.all([ + this._getCommitInfo(), + this.$.fileList.reload(), + ]); + } + + _getMergeability() { + if (!this._change) { + this._mergeable = null; + return Promise.resolve(); + } + // If the change is closed, it is not mergeable. Note: already merged + // changes are obviously not mergeable, but the mergeability API will not + // answer for abandoned changes. + if (this._change.status === this.ChangeStatus.MERGED || + this._change.status === this.ChangeStatus.ABANDONED) { + this._mergeable = false; + return Promise.resolve(); + } + + this._mergeable = null; + return this.$.restAPI.getMergeable(this._changeNum).then(m => { + this._mergeable = m.mergeable; + }); + } + + _computeCanStartReview(change) { + return !!(change.actions && change.actions.ready && + change.actions.ready.enabled); + } + + _computeReplyDisabled() { return false; } + + _computeChangePermalinkAriaLabel(changeNum) { + return 'Change ' + changeNum; + } + + _computeCommitMessageCollapsed(collapsed, collapsible) { + return collapsible && collapsed; + } + + _computeRelatedChangesClass(collapsed) { + return collapsed ? 'collapsed' : ''; + } + + _computeCollapseText(collapsed) { + // Symbols are up and down triangles. + return collapsed ? '\u25bc Show more' : '\u25b2 Show less'; + } + + /** + * Returns the text to be copied when + * click the copy icon next to change subject + * + * @param {!Object} change + */ + _computeCopyTextForTitle(change) { + return `${change._number}: ${change.subject}` + + ` | https://${location.host}${this._computeChangeUrl(change)}`; + } + + _toggleCommitCollapsed() { + this._commitCollapsed = !this._commitCollapsed; + if (this._commitCollapsed) { + window.scrollTo(0, 0); + } + } + + _toggleRelatedChangesCollapsed() { + this._relatedChangesCollapsed = !this._relatedChangesCollapsed; + if (this._relatedChangesCollapsed) { + window.scrollTo(0, 0); + } + } + + _computeCommitCollapsible(commitMessage) { + if (!commitMessage) { return false; } + return commitMessage.split('\n').length >= MIN_LINES_FOR_COMMIT_COLLAPSE; + } + + _getOffsetHeight(element) { + return element.offsetHeight; + } + + _getScrollHeight(element) { + return element.scrollHeight; + } + + /** + * Get the line height of an element to the nearest integer. + */ + _getLineHeight(element) { + const lineHeightStr = getComputedStyle(element).lineHeight; + return Math.round(lineHeightStr.slice(0, lineHeightStr.length - 2)); + } + + /** + * New max height for the related changes section, shorter than the existing + * change info height. + */ + _updateRelatedChangeMaxHeight() { + // Takes into account approximate height for the expand button and + // bottom margin. + const EXTRA_HEIGHT = 30; + let newHeight; + + if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_SMALL})`) + .matches) { + // In a small (mobile) view, give the relation chain some space. + newHeight = SMALL_RELATED_HEIGHT; + } else if (window.matchMedia(`(max-width: ${BREAKPOINT_RELATED_MED})`) + .matches) { + // Since related changes are below the commit message, but still next to + // metadata, the height should be the height of the metadata minus the + // height of the commit message to reduce jank. However, if that doesn't + // result in enough space, instead use the MINIMUM_RELATED_MAX_HEIGHT. + // Note: extraHeight is to take into account margin/padding. + const medRelatedHeight = Math.max( + this._getOffsetHeight(this.$.mainChangeInfo) - + this._getOffsetHeight(this.$.commitMessage) - 2 * EXTRA_HEIGHT, + MINIMUM_RELATED_MAX_HEIGHT); + newHeight = medRelatedHeight; + } else { + if (this._commitCollapsible) { + // Make sure the content is lined up if both areas have buttons. If + // the commit message is not collapsed, instead use the change info + // height. + newHeight = this._getOffsetHeight(this.$.commitMessage); + } else { + newHeight = this._getOffsetHeight(this.$.commitAndRelated) - + EXTRA_HEIGHT; + } + } + const stylesToUpdate = {}; + + // Get the line height of related changes, and convert it to the nearest + // integer. + const lineHeight = this._getLineHeight(this.$.relatedChanges); + + // Figure out a new height that is divisible by the rounded line height. + const remainder = newHeight % lineHeight; + newHeight = newHeight - remainder; + + stylesToUpdate['--relation-chain-max-height'] = newHeight + 'px'; + + // Update the max-height of the relation chain to this new height. + if (this._commitCollapsible) { + stylesToUpdate['--related-change-btn-top-padding'] = remainder + 'px'; + } + + this.updateStyles(stylesToUpdate); + } + + _computeShowRelatedToggle() { + // Make sure the max height has been applied, since there is now content + // to populate. + if (!util.getComputedStyleValue('--relation-chain-max-height', this)) { + this._updateRelatedChangeMaxHeight(); + } + // Prevents showMore from showing when click on related change, since the + // line height would be positive, but related changes height is 0. + if (!this._getScrollHeight(this.$.relatedChanges)) { + return this._showRelatedToggle = false; + } + + if (this._getScrollHeight(this.$.relatedChanges) > + (this._getOffsetHeight(this.$.relatedChanges) + + this._getLineHeight(this.$.relatedChanges))) { + return this._showRelatedToggle = true; + } + this._showRelatedToggle = false; + } + + _updateToggleContainerClass(showRelatedToggle) { + if (showRelatedToggle) { + this.$.relatedChangesToggle.classList.add('showToggle'); + } else { + this.$.relatedChangesToggle.classList.remove('showToggle'); + } + } + + _startUpdateCheckTimer() { + if (!this._serverConfig || + !this._serverConfig.change || + this._serverConfig.change.update_delay === undefined || + this._serverConfig.change.update_delay <= MIN_CHECK_INTERVAL_SECS) { + return; + } + + this._updateCheckTimerHandle = this.async(() => { + this.fetchChangeUpdates(this._change, this.$.restAPI).then(result => { + let toastMessage = null; + if (!result.isLatest) { + toastMessage = ReloadToastMessage.NEWER_REVISION; + } else if (result.newStatus === this.ChangeStatus.MERGED) { + toastMessage = ReloadToastMessage.MERGED; + } else if (result.newStatus === this.ChangeStatus.ABANDONED) { + toastMessage = ReloadToastMessage.ABANDONED; + } else if (result.newStatus === this.ChangeStatus.NEW) { + toastMessage = ReloadToastMessage.RESTORED; + } else if (result.newMessages) { + toastMessage = ReloadToastMessage.NEW_MESSAGE; + } + + if (!toastMessage) { + this._startUpdateCheckTimer(); + return; + } + + this._cancelUpdateCheckTimer(); + this.fire('show-alert', { + message: toastMessage, + // Persist this alert. + dismissOnNavigation: true, + action: 'Reload', + callback: function() { + // Load the current change without any patch range. + Gerrit.Nav.navigateToChange(this._change); + }.bind(this), + }); + }); + }, this._serverConfig.change.update_delay * 1000); + } + + _cancelUpdateCheckTimer() { + if (this._updateCheckTimerHandle) { + this.cancelAsync(this._updateCheckTimerHandle); + } + this._updateCheckTimerHandle = null; + } + + _handleVisibilityChange() { + if (document.hidden && this._updateCheckTimerHandle) { + this._cancelUpdateCheckTimer(); + } else if (!this._updateCheckTimerHandle) { + this._startUpdateCheckTimer(); + } + } + + _handleTopicChanged() { + this.$.relatedChanges.reload(); + } + + _computeHeaderClass(editMode) { + const classes = ['header']; + if (editMode) { classes.push('editMode'); } + return classes.join(' '); + } + + _computeEditMode(patchRangeRecord, paramsRecord) { + if ([patchRangeRecord, paramsRecord].some(arg => arg === undefined)) { + return undefined; + } + + if (paramsRecord.base && paramsRecord.base.edit) { return true; } + + const patchRange = patchRangeRecord.base || {}; + return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); + } + + _handleFileActionTap(e) { + e.preventDefault(); + const controls = this.$.fileListHeader.$.editControls; + const path = e.detail.path; + switch (e.detail.action) { + case GrEditConstants.Actions.DELETE.id: + controls.openDeleteDialog(path); + break; + case GrEditConstants.Actions.OPEN.id: + Gerrit.Nav.navigateToRelativeUrl( + Gerrit.Nav.getEditUrlForDiff(this._change, path, + this._patchRange.patchNum)); + break; + case GrEditConstants.Actions.RENAME.id: + controls.openRenameDialog(path); + break; + case GrEditConstants.Actions.RESTORE.id: + controls.openRestoreDialog(path); + break; + } + } + + _computeCommitMessageKey(number, revision) { + return `c${number}_rev${revision}`; + } + + _patchNumChanged(patchNumStr) { + if (!this._selectedRevision) { + return; + } + + let patchNum = parseInt(patchNumStr, 10); + if (patchNumStr === 'edit') { + patchNum = patchNumStr; + } + + if (patchNum === this._selectedRevision._number) { + return; + } + this._selectedRevision = Object.values(this._change.revisions).find( + revision => revision._number === patchNum); + } + + /** + * If an edit exists already, load it. Otherwise, toggle edit mode via the + * navigation API. + */ + _handleEditTap() { + const editInfo = Object.values(this._change.revisions).find(info => + info._number === this.EDIT_NAME); + + if (editInfo) { + Gerrit.Nav.navigateToChange(this._change, this.EDIT_NAME); + return; + } + + // Avoid putting patch set in the URL unless a non-latest patch set is + // selected. + let patchNum; + if (!this.patchNumEquals(this._patchRange.patchNum, + this.computeLatestPatchNum(this._allPatchSets))) { + patchNum = this._patchRange.patchNum; + } + Gerrit.Nav.navigateToChange(this._change, patchNum, null, true); + } + + _handleStopEditTap() { + Gerrit.Nav.navigateToChange(this._change, this._patchRange.patchNum); + } + + _resetReplyOverlayFocusStops() { + this.$.replyOverlay.setFocusStops(this.$.replyDialog.getFocusStops()); + } + + _handleToggleStar(e) { + this.$.restAPI.saveChangeStarred(e.detail.change._number, + e.detail.starred); + } + + _getRevisionInfo(change) { + return new Gerrit.RevisionInfo(change); + } + + _computeCurrentRevision(currentRevision, revisions) { + return currentRevision && revisions && revisions[currentRevision]; + } + + _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { + return disableDiffPrefs || !loggedIn; + } +} + +customElements.define(GrChangeView.is, GrChangeView);
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js index 9a53342..b7fdbb7 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_html.js
@@ -1,63 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html"> -<link rel="import" href="../../edit/gr-edit-constants.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../shared/gr-account-link/gr-account-link.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-change-star/gr-change-star.html"> -<link rel="import" href="../../shared/gr-change-status/gr-change-status.html"> -<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-editable-content/gr-editable-content.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../../shared/revision-info/revision-info.html"> -<link rel="import" href="../gr-change-actions/gr-change-actions.html"> -<link rel="import" href="../gr-change-metadata/gr-change-metadata.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../gr-commit-info/gr-commit-info.html"> -<link rel="import" href="../gr-download-dialog/gr-download-dialog.html"> -<link rel="import" href="../gr-file-list-header/gr-file-list-header.html"> -<link rel="import" href="../gr-file-list/gr-file-list.html"> -<link rel="import" href="../gr-included-in-dialog/gr-included-in-dialog.html"> -<link rel="import" href="../gr-messages-list/gr-messages-list.html"> -<link rel="import" href="../gr-related-changes-list/gr-related-changes-list.html"> -<link rel="import" href="../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.html"> -<link rel="import" href="../gr-reply-dialog/gr-reply-dialog.html"> -<link rel="import" href="../gr-thread-list/gr-thread-list.html"> -<link rel="import" href="../gr-upload-help-dialog/gr-upload-help-dialog.html"> - -<dom-module id="gr-change-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .container:not(.loading) { background-color: var(--background-color-tertiary); @@ -374,136 +333,61 @@ margin: var(--spacing-m); } </style> - <div class="container loading" hidden$="[[!_loading]]">Loading...</div> - <div - id="mainContent" - class="container" - on-show-checks-table="_handleShowTab" - hidden$="{{_loading}}"> + <div class="container loading" hidden\$="[[!_loading]]">Loading...</div> + <div id="mainContent" class="container" on-show-checks-table="_handleShowTab" hidden\$="{{_loading}}"> <section class="changeInfoSection"> - <div class$="[[_computeHeaderClass(_editMode)]]"> + <div class\$="[[_computeHeaderClass(_editMode)]]"> <div class="headerTitle"> <div class="changeStatuses"> <template is="dom-repeat" items="[[_changeStatuses]]" as="status"> - <gr-change-status - max-width="100" - status="[[status]]"></gr-change-status> + <gr-change-status max-width="100" status="[[status]]"></gr-change-status> </template> </div> <div class="statusText"> - <template - is="dom-if" - if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]"> + <template is="dom-if" if="[[_computeShowCommitInfo(_changeStatus, _change.current_revision)]]"> <span class="text"> as </span> - <gr-commit-info - change="[[_change]]" - commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]" - server-config="[[_serverConfig]]"></gr-commit-info> + <gr-commit-info change="[[_change]]" commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]" server-config="[[_serverConfig]]"></gr-commit-info> </template> </div> - <gr-change-star - id="changeStar" - change="{{_change}}" - on-toggle-star="_handleToggleStar" - hidden$="[[!_loggedIn]]"></gr-change-star> + <gr-change-star id="changeStar" change="{{_change}}" on-toggle-star="_handleToggleStar" hidden\$="[[!_loggedIn]]"></gr-change-star> - <a aria-label$="[[_computeChangePermalinkAriaLabel(_change._number)]]" - href$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a> + <a aria-label\$="[[_computeChangePermalinkAriaLabel(_change._number)]]" href\$="[[_computeChangeUrl(_change)]]">[[_change._number]]</a> <span class="changeNumberColon">: </span> <span class="headerSubject">[[_change.subject]]</span> - <gr-copy-clipboard - class="changeCopyClipboard" - hide-input - text="[[_computeCopyTextForTitle(_change)]]"> + <gr-copy-clipboard class="changeCopyClipboard" hide-input="" text="[[_computeCopyTextForTitle(_change)]]"> </gr-copy-clipboard> </div><!-- end headerTitle --> - <div class="commitActions" hidden$="[[!_loggedIn]]"> - <gr-change-actions - id="actions" - change="[[_change]]" - disable-edit="[[disableEdit]]" - has-parent="[[hasParent]]" - actions="[[_change.actions]]" - revision-actions="{{_currentRevisionActions}}" - change-num="[[_changeNum]]" - change-status="[[_change.status]]" - commit-num="[[_commitInfo.commit]]" - latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]" - commit-message="[[_latestCommitMessage]]" - edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]" - edit-mode="[[_editMode]]" - edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]" - private-by-default="[[_projectConfig.private_by_default]]" - on-reload-change="_handleReloadChange" - on-edit-tap="_handleEditTap" - on-stop-edit-tap="_handleStopEditTap" - on-download-tap="_handleOpenDownloadDialog"></gr-change-actions> + <div class="commitActions" hidden\$="[[!_loggedIn]]"> + <gr-change-actions id="actions" change="[[_change]]" disable-edit="[[disableEdit]]" has-parent="[[hasParent]]" actions="[[_change.actions]]" revision-actions="{{_currentRevisionActions}}" change-num="[[_changeNum]]" change-status="[[_change.status]]" commit-num="[[_commitInfo.commit]]" latest-patch-num="[[computeLatestPatchNum(_allPatchSets)]]" commit-message="[[_latestCommitMessage]]" edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]" edit-mode="[[_editMode]]" edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]" private-by-default="[[_projectConfig.private_by_default]]" on-reload-change="_handleReloadChange" on-edit-tap="_handleEditTap" on-stop-edit-tap="_handleStopEditTap" on-download-tap="_handleOpenDownloadDialog"></gr-change-actions> </div><!-- end commit actions --> </div><!-- end header --> <div class="changeInfo"> <div class="changeInfo-column changeMetadata hideOnMobileOverlay"> - <gr-change-metadata - id="metadata" - change="{{_change}}" - account="[[_account]]" - revision="[[_selectedRevision]]" - commit-info="[[_commitInfo]]" - server-config="[[_serverConfig]]" - parent-is-current="[[_parentIsCurrent]]" - on-show-reply-dialog="_handleShowReplyDialog"> + <gr-change-metadata id="metadata" change="{{_change}}" account="[[_account]]" revision="[[_selectedRevision]]" commit-info="[[_commitInfo]]" server-config="[[_serverConfig]]" parent-is-current="[[_parentIsCurrent]]" on-show-reply-dialog="_handleShowReplyDialog"> </gr-change-metadata> </div> <div id="mainChangeInfo" class="changeInfo-column mainChangeInfo"> <div id="commitAndRelated" class="hideOnMobileOverlay"> <div class="commitContainer"> <div> - <gr-button - id="replyBtn" - class="reply" - title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG, - ShortcutSection.ACTIONS)]]" - hidden$="[[!_loggedIn]]" - primary - disabled="[[_replyDisabled]]" - on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button> + <gr-button id="replyBtn" class="reply" title="[[createTitle(Shortcut.OPEN_REPLY_DIALOG, + ShortcutSection.ACTIONS)]]" hidden\$="[[!_loggedIn]]" primary="" disabled="[[_replyDisabled]]" on-click="_handleReplyTap">[[_replyButtonLabel]]</gr-button> </div> - <div - id="commitMessage" - class="commitMessage"> - <gr-editable-content id="commitMessageEditor" - editing="[[_editingCommitMessage]]" - content="{{_latestCommitMessage}}" - storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]" - remove-zero-width-space - collapsed$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]"> - <gr-linked-text pre - content="[[_latestCommitMessage]]" - config="[[_projectConfig.commentlinks]]" - remove-zero-width-space></gr-linked-text> + <div id="commitMessage" class="commitMessage"> + <gr-editable-content id="commitMessageEditor" editing="[[_editingCommitMessage]]" content="{{_latestCommitMessage}}" storage-key="[[_computeCommitMessageKey(_change._number, _change.current_revision)]]" remove-zero-width-space="" collapsed\$="[[_computeCommitMessageCollapsed(_commitCollapsed, _commitCollapsible)]]"> + <gr-linked-text pre="" content="[[_latestCommitMessage]]" config="[[_projectConfig.commentlinks]]" remove-zero-width-space=""></gr-linked-text> </gr-editable-content> - <gr-button link - class="editCommitMessage" - on-click="_handleEditCommitMessage" - hidden$="[[_hideEditCommitMessage]]">Edit</gr-button> - <div class="changeId" hidden$="[[!_changeIdCommitMessageError]]"> + <gr-button link="" class="editCommitMessage" on-click="_handleEditCommitMessage" hidden\$="[[_hideEditCommitMessage]]">Edit</gr-button> + <div class="changeId" hidden\$="[[!_changeIdCommitMessageError]]"> <hr> Change-Id: - <span - class$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]" - title$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]"> + <span class\$="[[_computeChangeIdClass(_changeIdCommitMessageError)]]" title\$="[[_computeTitleAttributeWarning(_changeIdCommitMessageError)]]"> [[_change.change_id]] </span> </div> </div> - <div - id="commitCollapseToggle" - class="collapseToggleContainer" - hidden$="[[!_commitCollapsible]]"> - <gr-button - link - id="commitCollapseToggleButton" - class="collapseToggleButton" - on-click="_toggleCommitCollapsed"> + <div id="commitCollapseToggle" class="collapseToggleContainer" hidden\$="[[!_commitCollapsible]]"> + <gr-button link="" id="commitCollapseToggleButton" class="collapseToggleButton" on-click="_toggleCommitCollapsed"> [[_computeCollapseText(_commitCollapsed)]] </gr-button> </div> @@ -515,23 +399,10 @@ </gr-endpoint-decorator> </div> <div class="relatedChanges"> - <gr-related-changes-list id="relatedChanges" - class$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]" - change="[[_change]]" - mergeable="[[_mergeable]]" - has-parent="{{hasParent}}" - on-update="_updateRelatedChangeMaxHeight" - patch-num="[[computeLatestPatchNum(_allPatchSets)]]" - on-new-section-loaded="_computeShowRelatedToggle"> + <gr-related-changes-list id="relatedChanges" class\$="[[_computeRelatedChangesClass(_relatedChangesCollapsed)]]" change="[[_change]]" mergeable="[[_mergeable]]" has-parent="{{hasParent}}" on-update="_updateRelatedChangeMaxHeight" patch-num="[[computeLatestPatchNum(_allPatchSets)]]" on-new-section-loaded="_computeShowRelatedToggle"> </gr-related-changes-list> - <div - id="relatedChangesToggle" - class="collapseToggleContainer"> - <gr-button - link - id="relatedChangesToggleButton" - class="collapseToggleButton" - on-click="_toggleRelatedChangesCollapsed"> + <div id="relatedChangesToggle" class="collapseToggleContainer"> + <gr-button link="" id="relatedChangesToggleButton" class="collapseToggleButton" on-click="_toggleRelatedChangesCollapsed"> [[_computeCollapseText(_relatedChangesCollapsed)]] </gr-button> </div> @@ -542,11 +413,10 @@ </section> <paper-tabs id="primaryTabs" on-selected-changed="_handleFileTabChange"> - <paper-tab data-name$="[[_files_tab_name]]">Files</paper-tab> - <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]" - as="tabHeader"> - <paper-tab data-name$="[[tabHeader]]"> - <gr-endpoint-decorator name$="[[tabHeader]]"> + <paper-tab data-name\$="[[_files_tab_name]]">Files</paper-tab> + <template is="dom-repeat" items="[[_dynamicTabHeaderEndpoints]]" as="tabHeader"> + <paper-tab data-name\$="[[tabHeader]]"> + <gr-endpoint-decorator name\$="[[tabHeader]]"> <gr-endpoint-param name="change" value="[[_change]]"> </gr-endpoint-param> <gr-endpoint-param name="revision" value="[[_selectedRevision]]"> @@ -554,78 +424,23 @@ </gr-endpoint-decorator> </paper-tab> </template> - <paper-tab data-name$="[[_findings_tab_name]]"> + <paper-tab data-name\$="[[_findings_tab_name]]"> Findings </paper-tab> </paper-tabs> <section class="patchInfo"> - <div hidden$="[[!_findIfTabMatches(_currentTabName, _files_tab_name)]]"> - <gr-file-list-header - id="fileListHeader" - account="[[_account]]" - all-patch-sets="[[_allPatchSets]]" - change="[[_change]]" - change-num="[[_changeNum]]" - revision-info="[[_revisionInfo]]" - change-comments="[[_changeComments]]" - commit-info="[[_commitInfo]]" - change-url="[[_computeChangeUrl(_change)]]" - edit-mode="[[_editMode]]" - logged-in="[[_loggedIn]]" - server-config="[[_serverConfig]]" - shown-file-count="[[_shownFileCount]]" - diff-prefs="[[_diffPrefs]]" - diff-view-mode="{{viewState.diffMode}}" - patch-num="{{_patchRange.patchNum}}" - base-patch-num="{{_patchRange.basePatchNum}}" - files-expanded="[[_filesExpanded]]" - diff-prefs-disabled="[[_diffPrefsDisabled]]" - on-open-diff-prefs="_handleOpenDiffPrefs" - on-open-download-dialog="_handleOpenDownloadDialog" - on-open-upload-help-dialog="_handleOpenUploadHelpDialog" - on-open-included-in-dialog="_handleOpenIncludedInDialog" - on-expand-diffs="_expandAllDiffs" - on-collapse-diffs="_collapseAllDiffs"> + <div hidden\$="[[!_findIfTabMatches(_currentTabName, _files_tab_name)]]"> + <gr-file-list-header id="fileListHeader" account="[[_account]]" all-patch-sets="[[_allPatchSets]]" change="[[_change]]" change-num="[[_changeNum]]" revision-info="[[_revisionInfo]]" change-comments="[[_changeComments]]" commit-info="[[_commitInfo]]" change-url="[[_computeChangeUrl(_change)]]" edit-mode="[[_editMode]]" logged-in="[[_loggedIn]]" server-config="[[_serverConfig]]" shown-file-count="[[_shownFileCount]]" diff-prefs="[[_diffPrefs]]" diff-view-mode="{{viewState.diffMode}}" patch-num="{{_patchRange.patchNum}}" base-patch-num="{{_patchRange.basePatchNum}}" files-expanded="[[_filesExpanded]]" diff-prefs-disabled="[[_diffPrefsDisabled]]" on-open-diff-prefs="_handleOpenDiffPrefs" on-open-download-dialog="_handleOpenDownloadDialog" on-open-upload-help-dialog="_handleOpenUploadHelpDialog" on-open-included-in-dialog="_handleOpenIncludedInDialog" on-expand-diffs="_expandAllDiffs" on-collapse-diffs="_collapseAllDiffs"> </gr-file-list-header> - <gr-file-list - id="fileList" - class="hideOnMobileOverlay" - diff-prefs="{{_diffPrefs}}" - change="[[_change]]" - change-num="[[_changeNum]]" - patch-range="{{_patchRange}}" - change-comments="[[_changeComments]]" - drafts="[[_diffDrafts]]" - revisions="[[_change.revisions]]" - project-config="[[_projectConfig]]" - selected-index="{{viewState.selectedFileIndex}}" - diff-view-mode="[[viewState.diffMode]]" - edit-mode="[[_editMode]]" - num-files-shown="{{_numFilesShown}}" - files-expanded="{{_filesExpanded}}" - file-list-increment="{{_numFilesShown}}" - on-files-shown-changed="_setShownFiles" - on-file-action-tap="_handleFileActionTap" - on-reload-drafts="_reloadDraftsWithCallback"> + <gr-file-list id="fileList" class="hideOnMobileOverlay" diff-prefs="{{_diffPrefs}}" change="[[_change]]" change-num="[[_changeNum]]" patch-range="{{_patchRange}}" change-comments="[[_changeComments]]" drafts="[[_diffDrafts]]" revisions="[[_change.revisions]]" project-config="[[_projectConfig]]" selected-index="{{viewState.selectedFileIndex}}" diff-view-mode="[[viewState.diffMode]]" edit-mode="[[_editMode]]" num-files-shown="{{_numFilesShown}}" files-expanded="{{_filesExpanded}}" file-list-increment="{{_numFilesShown}}" on-files-shown-changed="_setShownFiles" on-file-action-tap="_handleFileActionTap" on-reload-drafts="_reloadDraftsWithCallback"> </gr-file-list> </div> <template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _findings_tab_name)]]"> - <gr-dropdown-list - class="patch-set-dropdown" - items="[[_robotCommentsPatchSetDropdownItems]]" - on-value-change="_handleRobotCommentPatchSetChanged" - value="[[_currentRobotCommentsPatchSet]]"> + <gr-dropdown-list class="patch-set-dropdown" items="[[_robotCommentsPatchSetDropdownItems]]" on-value-change="_handleRobotCommentPatchSetChanged" value="[[_currentRobotCommentsPatchSet]]"> </gr-dropdown-list> - <gr-thread-list - threads="[[_robotCommentThreads]]" - change="[[_change]]" - change-num="[[_changeNum]]" - logged-in="[[_loggedIn]]" - tab="[[_findings_tab_name]]" - hide-toggle-buttons - on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list> + <gr-thread-list threads="[[_robotCommentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" tab="[[_findings_tab_name]]" hide-toggle-buttons="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list> <template is="dom-if" if="[[_showRobotCommentsButton]]"> <gr-button class="show-robot-comments" on-click="_toggleShowRobotComments"> [[_computeShowText(_showAllRobotComments)]] @@ -634,7 +449,7 @@ </template> <template is="dom-if" if="[[_findIfTabMatches(_currentTabName, _selectedTabPluginHeader)]]"> - <gr-endpoint-decorator name$="[[_selectedTabPluginEndpoint]]"> + <gr-endpoint-decorator name\$="[[_selectedTabPluginEndpoint]]"> <gr-endpoint-param name="change" value="[[_change]]"> </gr-endpoint-param> <gr-endpoint-param name="revision" value="[[_selectedRevision]]"> @@ -650,94 +465,41 @@ </gr-endpoint-param> </gr-endpoint-decorator> - <paper-tabs - id="commentTabs" - on-selected-changed="_handleCommentTabChange"> + <paper-tabs id="commentTabs" on-selected-changed="_handleCommentTabChange"> <paper-tab class="changeLog">Change Log</paper-tab> - <paper-tab - class="commentThreads"> - <gr-tooltip-content - has-tooltip - title$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"> + <paper-tab class="commentThreads"> + <gr-tooltip-content has-tooltip="" title\$="[[_computeTotalCommentCounts(_change.unresolved_comment_count, _changeComments)]]"> <span>Comment Threads</span></gr-tooltip-content> </paper-tab> </paper-tabs> <section class="changeLog"> <template is="dom-if" if="[[_isSelectedView(_currentView, _commentTabs.CHANGE_LOG)]]"> - <gr-messages-list - class="hideOnMobileOverlay" - change-num="[[_changeNum]]" - labels="[[_change.labels]]" - messages="[[_change.messages]]" - reviewer-updates="[[_change.reviewer_updates]]" - change-comments="[[_changeComments]]" - project-name="[[_change.project]]" - show-reply-buttons="[[_loggedIn]]" - on-message-anchor-tap="_handleMessageAnchorTap" - on-reply="_handleMessageReply"></gr-messages-list> + <gr-messages-list class="hideOnMobileOverlay" change-num="[[_changeNum]]" labels="[[_change.labels]]" messages="[[_change.messages]]" reviewer-updates="[[_change.reviewer_updates]]" change-comments="[[_changeComments]]" project-name="[[_change.project]]" show-reply-buttons="[[_loggedIn]]" on-message-anchor-tap="_handleMessageAnchorTap" on-reply="_handleMessageReply"></gr-messages-list> </template> <template is="dom-if" if="[[_isSelectedView(_currentView, _commentTabs.COMMENT_THREADS)]]"> - <gr-thread-list - threads="[[_commentThreads]]" - change="[[_change]]" - change-num="[[_changeNum]]" - logged-in="[[_loggedIn]]" - only-show-robot-comments-with-human-reply - on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list> + <gr-thread-list threads="[[_commentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" only-show-robot-comments-with-human-reply="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list> </template> </section> </div> - <gr-apply-fix-dialog - id="applyFixDialog" - prefs="[[_diffPrefs]]" - change="[[_change]]" - change-num="[[_changeNum]]"></gr-apply-fix-dialog> - <gr-overlay id="downloadOverlay" with-backdrop> - <gr-download-dialog - id="downloadDialog" - change="[[_change]]" - patch-num="[[_patchRange.patchNum]]" - config="[[_serverConfig.download]]" - on-close="_handleDownloadDialogClose"></gr-download-dialog> + <gr-apply-fix-dialog id="applyFixDialog" prefs="[[_diffPrefs]]" change="[[_change]]" change-num="[[_changeNum]]"></gr-apply-fix-dialog> + <gr-overlay id="downloadOverlay" with-backdrop=""> + <gr-download-dialog id="downloadDialog" change="[[_change]]" patch-num="[[_patchRange.patchNum]]" config="[[_serverConfig.download]]" on-close="_handleDownloadDialogClose"></gr-download-dialog> </gr-overlay> - <gr-overlay id="uploadHelpOverlay" with-backdrop> - <gr-upload-help-dialog - revision="[[_currentRevision]]" - target-branch="[[_change.branch]]" - on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog> + <gr-overlay id="uploadHelpOverlay" with-backdrop=""> + <gr-upload-help-dialog revision="[[_currentRevision]]" target-branch="[[_change.branch]]" on-close="_handleCloseUploadHelpDialog"></gr-upload-help-dialog> </gr-overlay> - <gr-overlay id="includedInOverlay" with-backdrop> - <gr-included-in-dialog - id="includedInDialog" - change-num="[[_changeNum]]" - on-close="_handleIncludedInDialogClose"></gr-included-in-dialog> + <gr-overlay id="includedInOverlay" with-backdrop=""> + <gr-included-in-dialog id="includedInDialog" change-num="[[_changeNum]]" on-close="_handleIncludedInDialogClose"></gr-included-in-dialog> </gr-overlay> - <gr-overlay id="replyOverlay" - class="scrollable" - no-cancel-on-outside-click - no-cancel-on-esc-key - with-backdrop> - <gr-reply-dialog id="replyDialog" - change="{{_change}}" - patch-num="[[computeLatestPatchNum(_allPatchSets)]]" - permitted-labels="[[_change.permitted_labels]]" - draft-comment-threads="[[_draftCommentThreads]]" - project-config="[[_projectConfig]]" - can-be-started="[[_canStartReview]]" - on-send="_handleReplySent" - on-cancel="_handleReplyCancel" - on-autogrow="_handleReplyAutogrow" - on-send-disabled-changed="_resetReplyOverlayFocusStops" - hidden$="[[!_loggedIn]]"> + <gr-overlay id="replyOverlay" class="scrollable" no-cancel-on-outside-click="" no-cancel-on-esc-key="" with-backdrop=""> + <gr-reply-dialog id="replyDialog" change="{{_change}}" patch-num="[[computeLatestPatchNum(_allPatchSets)]]" permitted-labels="[[_change.permitted_labels]]" draft-comment-threads="[[_draftCommentThreads]]" project-config="[[_projectConfig]]" can-be-started="[[_canStartReview]]" on-send="_handleReplySent" on-cancel="_handleReplyCancel" on-autogrow="_handleReplyAutogrow" on-send-disabled-changed="_resetReplyOverlayFocusStops" hidden\$="[[!_loggedIn]]"> </gr-reply-dialog> </gr-overlay> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-comment-api id="commentAPI"></gr-comment-api> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-change-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html index 26bcbf2..4dc0e9b 100644 --- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html +++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -19,18 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="../../edit/gr-edit-constants.html"> -<link rel="import" href="gr-change-view.html"> +<script type="module" src="../../edit/gr-edit-constants.js"></script> +<script type="module" src="./gr-change-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../edit/gr-edit-constants.js'; +import './gr-change-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -44,887 +50,667 @@ </template> </test-fixture> -<script> - suite('gr-change-view tests', async () => { - await readyToTest(); - const kb = window.Gerrit.KeyboardShortcutBinder; - kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter'); - kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r'); - kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a'); - kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd'); - kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm'); - kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's'); - kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u'); - kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x'); - kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z'); - kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ','); - kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't'); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../edit/gr-edit-constants.js'; +import './gr-change-view.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-change-view tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + kb.bindShortcut(kb.Shortcut.SEND_REPLY, 'ctrl+enter'); + kb.bindShortcut(kb.Shortcut.REFRESH_CHANGE, 'shift+r'); + kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a'); + kb.bindShortcut(kb.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd'); + kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm'); + kb.bindShortcut(kb.Shortcut.TOGGLE_CHANGE_STAR, 's'); + kb.bindShortcut(kb.Shortcut.UP_TO_DASHBOARD, 'u'); + kb.bindShortcut(kb.Shortcut.EXPAND_ALL_MESSAGES, 'x'); + kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_MESSAGES, 'z'); + kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ','); + kb.bindShortcut(kb.Shortcut.EDIT_TOPIC, 't'); - let element; - let sandbox; - let navigateToChangeStub; - const TEST_SCROLL_TOP_PX = 100; + let element; + let sandbox; + let navigateToChangeStub; + const TEST_SCROLL_TOP_PX = 100; - const CommentTabs = { - CHANGE_LOG: 0, - COMMENT_THREADS: 1, + const CommentTabs = { + CHANGE_LOG: 0, + COMMENT_THREADS: 1, + }; + const ROBOT_COMMENTS_LIMIT = 10; + + const THREADS = [ + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 2, + robot_id: 'rb1', + id: 'ecf9fa_fe1a5f62', + line: 5, + updated: '2018-02-08 18:49:18.000000000', + message: 'test', + unresolved: true, + }, + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 4, + id: 'ecf0b9fa_fe1a5f62', + line: 5, + updated: '2018-02-08 18:49:18.000000000', + message: 'test', + unresolved: true, + }, + { + id: '503008e2_0ab203ee', + path: '/COMMIT_MSG', + line: 5, + in_reply_to: 'ecf0b9fa_fe1a5f62', + updated: '2018-02-13 22:48:48.018000000', + message: 'draft', + unresolved: false, + __draft: true, + __draftID: '0.m683trwff68', + __editing: false, + patch_set: '2', + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 5, + rootId: 'ecf0b9fa_fe1a5f62', + start_datetime: '2018-02-08 18:49:18.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 3, + id: 'ecf0b9fa_fe5f62', + robot_id: 'rb2', + line: 5, + updated: '2018-02-08 18:49:18.000000000', + message: 'test', + unresolved: true, + }, + { + __path: 'test.txt', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 3, + id: '09a9fb0a_1484e6cf', + side: 'PARENT', + updated: '2018-02-13 22:47:19.000000000', + message: 'Some comment on another patchset.', + unresolved: false, + }, + ], + patchNum: 3, + path: 'test.txt', + rootId: '09a9fb0a_1484e6cf', + start_datetime: '2018-02-13 22:47:19.000000000', + commentSide: 'PARENT', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 2, + id: '8caddf38_44770ec1', + line: 4, + updated: '2018-02-13 22:48:40.000000000', + message: 'Another unresolved comment', + unresolved: true, + }, + ], + patchNum: 2, + path: '/COMMIT_MSG', + line: 4, + rootId: '8caddf38_44770ec1', + start_datetime: '2018-02-13 22:48:40.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 2, + id: 'scaddf38_44770ec1', + line: 4, + updated: '2018-02-14 22:48:40.000000000', + message: 'Yet another unresolved comment', + unresolved: true, + }, + ], + patchNum: 2, + path: '/COMMIT_MSG', + line: 4, + rootId: 'scaddf38_44770ec1', + start_datetime: '2018-02-14 22:48:40.000000000', + }, + { + comments: [ + { + id: 'zcf0b9fa_fe1a5f62', + path: '/COMMIT_MSG', + line: 6, + updated: '2018-02-15 22:48:48.018000000', + message: 'resolved draft', + unresolved: false, + __draft: true, + __draftID: '0.m683trwff68', + __editing: false, + patch_set: '2', + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 6, + rootId: 'zcf0b9fa_fe1a5f62', + start_datetime: '2018-02-09 18:49:18.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 4, + id: 'rc1', + line: 5, + updated: '2019-02-08 18:49:18.000000000', + message: 'test', + unresolved: true, + robot_id: 'rc1', + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 5, + rootId: 'rc1', + start_datetime: '2019-02-08 18:49:18.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 4, + id: 'rc2', + line: 5, + updated: '2019-03-08 18:49:18.000000000', + message: 'test', + unresolved: true, + robot_id: 'rc2', + }, + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', + }, + patch_set: 4, + id: 'c2_1', + line: 5, + updated: '2019-03-08 18:49:18.000000000', + message: 'test', + unresolved: true, + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 5, + rootId: 'rc2', + start_datetime: '2019-03-08 18:49:18.000000000', + }, + ]; + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-endpoint-decorator', { + _import: sandbox.stub().returns(Promise.resolve()), + }); + // Since _endpoints are global, must reset state. + Gerrit._endpoints = new GrPluginEndpoints(); + navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({test: 'config'}); }, + getAccount() { return Promise.resolve(null); }, + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + _fetchSharedCacheURL() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve()); + Gerrit._loadPlugins([]); + Gerrit.install( + plugin => { + plugin.registerDynamicCustomComponent( + 'change-view-tab-header', + 'gr-checks-change-view-tab-header-view' + ); + plugin.registerDynamicCustomComponent( + 'change-view-tab-content', + 'gr-checks-view' + ); + }, + '0.1', + 'http://some/plugins/url.html' + ); + }); + + teardown(done => { + flush(() => { + sandbox.restore(); + done(); + }); + }); + + const getCustomCssValue = + cssParam => util.getComputedStyleValue(cssParam, element); + + test('_handleMessageAnchorTap', () => { + element._changeNum = '1'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, }; - const ROBOT_COMMENTS_LIMIT = 10; + const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange'); + const replaceStateStub = sandbox.stub(history, 'replaceState'); + element._handleMessageAnchorTap({detail: {id: 'a12345'}}); - const THREADS = [ - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 2, - robot_id: 'rb1', - id: 'ecf9fa_fe1a5f62', - line: 5, - updated: '2018-02-08 18:49:18.000000000', - message: 'test', - unresolved: true, - }, - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'ecf0b9fa_fe1a5f62', - line: 5, - updated: '2018-02-08 18:49:18.000000000', - message: 'test', - unresolved: true, - }, - { - id: '503008e2_0ab203ee', - path: '/COMMIT_MSG', - line: 5, - in_reply_to: 'ecf0b9fa_fe1a5f62', - updated: '2018-02-13 22:48:48.018000000', - message: 'draft', - unresolved: false, - __draft: true, - __draftID: '0.m683trwff68', - __editing: false, - patch_set: '2', - }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 5, - rootId: 'ecf0b9fa_fe1a5f62', - start_datetime: '2018-02-08 18:49:18.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 3, - id: 'ecf0b9fa_fe5f62', - robot_id: 'rb2', - line: 5, - updated: '2018-02-08 18:49:18.000000000', - message: 'test', - unresolved: true, - }, - { - __path: 'test.txt', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 3, - id: '09a9fb0a_1484e6cf', - side: 'PARENT', - updated: '2018-02-13 22:47:19.000000000', - message: 'Some comment on another patchset.', - unresolved: false, - }, - ], - patchNum: 3, - path: 'test.txt', - rootId: '09a9fb0a_1484e6cf', - start_datetime: '2018-02-13 22:47:19.000000000', - commentSide: 'PARENT', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 2, - id: '8caddf38_44770ec1', - line: 4, - updated: '2018-02-13 22:48:40.000000000', - message: 'Another unresolved comment', - unresolved: true, - }, - ], - patchNum: 2, - path: '/COMMIT_MSG', - line: 4, - rootId: '8caddf38_44770ec1', - start_datetime: '2018-02-13 22:48:40.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 2, - id: 'scaddf38_44770ec1', - line: 4, - updated: '2018-02-14 22:48:40.000000000', - message: 'Yet another unresolved comment', - unresolved: true, - }, - ], - patchNum: 2, - path: '/COMMIT_MSG', - line: 4, - rootId: 'scaddf38_44770ec1', - start_datetime: '2018-02-14 22:48:40.000000000', - }, - { - comments: [ - { - id: 'zcf0b9fa_fe1a5f62', - path: '/COMMIT_MSG', - line: 6, - updated: '2018-02-15 22:48:48.018000000', - message: 'resolved draft', - unresolved: false, - __draft: true, - __draftID: '0.m683trwff68', - __editing: false, - patch_set: '2', - }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 6, - rootId: 'zcf0b9fa_fe1a5f62', - start_datetime: '2018-02-09 18:49:18.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'rc1', - line: 5, - updated: '2019-02-08 18:49:18.000000000', - message: 'test', - unresolved: true, - robot_id: 'rc1', - }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 5, - rootId: 'rc1', - start_datetime: '2019-02-08 18:49:18.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'rc2', - line: 5, - updated: '2019-03-08 18:49:18.000000000', - message: 'test', - unresolved: true, - robot_id: 'rc2', - }, - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'c2_1', - line: 5, - updated: '2019-03-08 18:49:18.000000000', - message: 'test', - unresolved: true, - }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 5, - rootId: 'rc2', - start_datetime: '2019-03-08 18:49:18.000000000', - }, - ]; + assert.equal(getUrlStub.lastCall.args[4], '#message-a12345'); + assert.isTrue(replaceStateStub.called); + }); - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-endpoint-decorator', { - _import: sandbox.stub().returns(Promise.resolve()), - }); - // Since _endpoints are global, must reset state. - Gerrit._endpoints = new GrPluginEndpoints(); - navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({test: 'config'}); }, - getAccount() { return Promise.resolve(null); }, - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - _fetchSharedCacheURL() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - sandbox.stub(element.$.actions, 'reload').returns(Promise.resolve()); - Gerrit._loadPlugins([]); - Gerrit.install( - plugin => { - plugin.registerDynamicCustomComponent( - 'change-view-tab-header', - 'gr-checks-change-view-tab-header-view' - ); - plugin.registerDynamicCustomComponent( - 'change-view-tab-content', - 'gr-checks-view' - ); - }, - '0.1', - 'http://some/plugins/url.html' - ); + suite('plugins adding to file tab', () => { + setup(done => { + // Resolving it here instead of during setup() as other tests depend + // on flush() not being called during setup. + flush(() => done()); }); - teardown(done => { + test('plugin added tab shows up as a dynamic endpoint', () => { + assert(element._dynamicTabHeaderEndpoints.includes( + 'change-view-tab-header-url')); + const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); + // 3 Tabs are : Files, Plugin, Findings + assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3); + assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name, + 'change-view-tab-header-url'); + }); + + test('handleShowTab switched tab correctly', done => { + const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); + assert.equal(paperTabs.selected, 0); + element._handleShowTab({detail: + {tab: 'change-view-tab-header-url'}}); flush(() => { - sandbox.restore(); + assert.equal(paperTabs.selected, 1); done(); }); }); - const getCustomCssValue = - cssParam => util.getComputedStyleValue(cssParam, element); + test('switching tab sets _selectedTabPluginEndpoint', done => { + const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); + MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]); + flush(() => { + assert.equal(element._selectedTabPluginEndpoint, + 'change-view-tab-content-url'); + done(); + }); + }); + }); - test('_handleMessageAnchorTap', () => { + suite('keyboard shortcuts', () => { + test('t to add topic', () => { + const editStub = sandbox.stub(element.$.metadata, 'editTopic'); + MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't'); + assert(editStub.called); + }); + + test('S should toggle the CL star', () => { + const starStub = sandbox.stub(element.$.changeStar, 'toggleStar'); + MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's'); + assert(starStub.called); + }); + + test('U should navigate to root if no backPage set', () => { + const relativeNavStub = sandbox.stub(Gerrit.Nav, + 'navigateToRelativeUrl'); + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert.isTrue(relativeNavStub.called); + assert.isTrue(relativeNavStub.lastCall.calledWithExactly( + Gerrit.Nav.getUrlForRoot())); + }); + + test('U should navigate to backPage if set', () => { + const relativeNavStub = sandbox.stub(Gerrit.Nav, + 'navigateToRelativeUrl'); + element.backPage = '/dashboard/self'; + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert.isTrue(relativeNavStub.called); + assert.isTrue(relativeNavStub.lastCall.calledWithExactly( + '/dashboard/self')); + }); + + test('A fires an error event when not logged in', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false)); + const loggedInErrorSpy = sandbox.spy(); + element.addEventListener('show-auth-required', loggedInErrorSpy); + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + flush(() => { + assert.isFalse(element.$.replyOverlay.opened); + assert.isTrue(loggedInErrorSpy.called); + done(); + }); + }); + + test('shift A does not open reply overlay', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); + flush(() => { + assert.isFalse(element.$.replyOverlay.opened); + done(); + }); + }); + + test('A toggles overlay when logged in', done => { + sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); + sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates') + .returns(Promise.resolve({isLatest: true})); + element._change = {labels: {}}; + const openSpy = sandbox.spy(element, '_openReplyDialog'); + + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + flush(() => { + assert.isTrue(element.$.replyOverlay.opened); + element.$.replyOverlay.close(); + assert.isFalse(element.$.replyOverlay.opened); + assert(openSpy.lastCall.calledWithExactly( + element.$.replyDialog.FocusTarget.ANY), + '_openReplyDialog should have been passed ANY'); + assert.equal(openSpy.callCount, 1); + done(); + }); + }); + + test('fullscreen-overlay-opened hides content', () => { + element._loggedIn = true; + element._loading = false; + element._change = { + owner: {_account_id: 1}, + labels: {}, + actions: { + abandon: { + enabled: true, + label: 'Abandon', + method: 'POST', + title: 'Abandon', + }, + }, + }; + sandbox.spy(element, '_handleHideBackgroundContent'); + element.$.replyDialog.fire('fullscreen-overlay-opened'); + assert.isTrue(element._handleHideBackgroundContent.called); + assert.isTrue(element.$.mainContent.classList.contains('overlayOpen')); + assert.equal(getComputedStyle(element.$.actions).display, 'flex'); + }); + + test('fullscreen-overlay-closed shows content', () => { + element._loggedIn = true; + element._loading = false; + element._change = { + owner: {_account_id: 1}, + labels: {}, + actions: { + abandon: { + enabled: true, + label: 'Abandon', + method: 'POST', + title: 'Abandon', + }, + }, + }; + sandbox.spy(element, '_handleShowBackgroundContent'); + element.$.replyDialog.fire('fullscreen-overlay-closed'); + assert.isTrue(element._handleShowBackgroundContent.called); + assert.isFalse(element.$.mainContent.classList.contains('overlayOpen')); + }); + + test('expand all messages when expand-diffs fired', () => { + const handleExpand = + sandbox.stub(element.$.fileList, 'expandAllDiffs'); + element.$.fileListHeader.fire('expand-diffs'); + assert.isTrue(handleExpand.called); + }); + + test('collapse all messages when collapse-diffs fired', () => { + const handleCollapse = + sandbox.stub(element.$.fileList, 'collapseAllDiffs'); + element.$.fileListHeader.fire('collapse-diffs'); + assert.isTrue(handleCollapse.called); + }); + + test('X should expand all messages', done => { + flush(() => { + const handleExpand = sandbox.stub(element.messagesList, + 'handleExpandCollapse'); + MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x'); + assert(handleExpand.calledWith(true)); + done(); + }); + }); + + test('Z should collapse all messages', done => { + flush(() => { + const handleExpand = sandbox.stub(element.messagesList, + 'handleExpandCollapse'); + MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z'); + assert(handleExpand.calledWith(false)); + done(); + }); + }); + + test('shift + R should fetch and navigate to the latest patch set', + done => { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + _number: 42, + revisions: { + rev1: {_number: 1, commit: {parents: []}}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + }; + + navigateToChangeStub.restore(); + navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange', + (change, patchNum, basePatchNum) => { + assert.equal(change, element._change); + assert.isUndefined(patchNum); + assert.isUndefined(basePatchNum); + done(); + }); + + MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); + }); + + test('d should open download overlay', () => { + const stub = sandbox.stub(element.$.downloadOverlay, 'open'); + MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd'); + assert.isTrue(stub.called); + }); + + test(', should open diff preferences', () => { + const stub = sandbox.stub( + element.$.fileList.$.diffPreferencesDialog, 'open'); + element._loggedIn = false; + element.disableDiffPrefs = true; + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); + assert.isFalse(stub.called); + + element._loggedIn = true; + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); + assert.isFalse(stub.called); + + element.disableDiffPrefs = false; + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); + assert.isTrue(stub.called); + }); + + test('m should toggle diff mode', () => { + sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); + const setModeStub = sandbox.stub(element.$.fileListHeader, + 'setDiffViewMode'); + const e = {preventDefault: () => {}}; + flushAsynchronousOperations(); + + element.viewState.diffMode = 'SIDE_BY_SIDE'; + element._handleToggleDiffMode(e); + assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF')); + + element.viewState.diffMode = 'UNIFIED_DIFF'; + element._handleToggleDiffMode(e); + assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE')); + }); + }); + + suite('reloading drafts', () => { + let reloadStub; + const drafts = { + 'testfile.txt': [ + { + patch_set: 5, + id: 'dd2982f5_c01c9e6a', + line: 1, + updated: '2017-11-08 18:47:45.000000000', + message: 'test', + unresolved: true, + }, + ], + }; + setup(() => { + // Fake computeDraftCount as its required for ChangeComments, + // see gr-comment-api#reloadDrafts. + reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts') + .returns(Promise.resolve({ + drafts, + getAllThreadsForChange: () => ([]), + computeDraftCount: () => 1, + })); + }); + + test('drafts are reloaded when reload-drafts fired', done => { + element.$.fileList.fire('reload-drafts', { + resolve: () => { + assert.isTrue(reloadStub.called); + assert.deepEqual(element._diffDrafts, drafts); + done(); + }, + }); + }); + + test('drafts are reloaded when comment-refresh fired', () => { + element.fire('comment-refresh'); + assert.isTrue(reloadStub.called); + }); + }); + + test('diff comments modified', () => { + sandbox.spy(element, '_handleReloadCommentThreads'); + return element._reloadComments().then(() => { + element.fire('diff-comments-modified'); + assert.isTrue(element._handleReloadCommentThreads.called); + }); + }); + + test('thread list modified', () => { + sandbox.spy(element, '_handleReloadDiffComments'); + element._currentView = CommentTabs.COMMENT_THREADS; + flushAsynchronousOperations(); + + return element._reloadComments().then(() => { + element.threadList.fire('thread-list-modified'); + assert.isTrue(element._handleReloadDiffComments.called); + + let draftStub = sinon.stub(element._changeComments, 'computeDraftCount') + .returns(1); + assert.equal(element._computeTotalCommentCounts(5, + element._changeComments), '5 unresolved, 1 draft'); + assert.equal(element._computeTotalCommentCounts(0, + element._changeComments), '1 draft'); + draftStub.restore(); + draftStub = sinon.stub(element._changeComments, 'computeDraftCount') + .returns(0); + assert.equal(element._computeTotalCommentCounts(0, + element._changeComments), ''); + assert.equal(element._computeTotalCommentCounts(1, + element._changeComments), '1 unresolved'); + draftStub.restore(); + draftStub = sinon.stub(element._changeComments, 'computeDraftCount') + .returns(2); + assert.equal(element._computeTotalCommentCounts(1, + element._changeComments), '1 unresolved, 2 drafts'); + draftStub.restore(); + }); + }); + + suite('thread list and change log tabs', () => { + setup(() => { element._changeNum = '1'; element._patchRange = { basePatchNum: 'PARENT', patchNum: 1, }; - const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChange'); - const replaceStateStub = sandbox.stub(history, 'replaceState'); - element._handleMessageAnchorTap({detail: {id: 'a12345'}}); - - assert.equal(getUrlStub.lastCall.args[4], '#message-a12345'); - assert.isTrue(replaceStateStub.called); - }); - - suite('plugins adding to file tab', () => { - setup(done => { - // Resolving it here instead of during setup() as other tests depend - // on flush() not being called during setup. - flush(() => done()); - }); - - test('plugin added tab shows up as a dynamic endpoint', () => { - assert(element._dynamicTabHeaderEndpoints.includes( - 'change-view-tab-header-url')); - const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); - // 3 Tabs are : Files, Plugin, Findings - assert.equal(paperTabs.querySelectorAll('paper-tab').length, 3); - assert.equal(paperTabs.querySelectorAll('paper-tab')[1].dataset.name, - 'change-view-tab-header-url'); - }); - - test('handleShowTab switched tab correctly', done => { - const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); - assert.equal(paperTabs.selected, 0); - element._handleShowTab({detail: - {tab: 'change-view-tab-header-url'}}); - flush(() => { - assert.equal(paperTabs.selected, 1); - done(); - }); - }); - - test('switching tab sets _selectedTabPluginEndpoint', done => { - const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); - MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[1]); - flush(() => { - assert.equal(element._selectedTabPluginEndpoint, - 'change-view-tab-content-url'); - done(); - }); - }); - }); - - suite('keyboard shortcuts', () => { - test('t to add topic', () => { - const editStub = sandbox.stub(element.$.metadata, 'editTopic'); - MockInteractions.pressAndReleaseKeyOn(element, 83, null, 't'); - assert(editStub.called); - }); - - test('S should toggle the CL star', () => { - const starStub = sandbox.stub(element.$.changeStar, 'toggleStar'); - MockInteractions.pressAndReleaseKeyOn(element, 83, null, 's'); - assert(starStub.called); - }); - - test('U should navigate to root if no backPage set', () => { - const relativeNavStub = sandbox.stub(Gerrit.Nav, - 'navigateToRelativeUrl'); - MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); - assert.isTrue(relativeNavStub.called); - assert.isTrue(relativeNavStub.lastCall.calledWithExactly( - Gerrit.Nav.getUrlForRoot())); - }); - - test('U should navigate to backPage if set', () => { - const relativeNavStub = sandbox.stub(Gerrit.Nav, - 'navigateToRelativeUrl'); - element.backPage = '/dashboard/self'; - MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); - assert.isTrue(relativeNavStub.called); - assert.isTrue(relativeNavStub.lastCall.calledWithExactly( - '/dashboard/self')); - }); - - test('A fires an error event when not logged in', done => { - sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(false)); - const loggedInErrorSpy = sandbox.spy(); - element.addEventListener('show-auth-required', loggedInErrorSpy); - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - flush(() => { - assert.isFalse(element.$.replyOverlay.opened); - assert.isTrue(loggedInErrorSpy.called); - done(); - }); - }); - - test('shift A does not open reply overlay', done => { - sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); - MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); - flush(() => { - assert.isFalse(element.$.replyOverlay.opened); - done(); - }); - }); - - test('A toggles overlay when logged in', done => { - sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true)); - sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates') - .returns(Promise.resolve({isLatest: true})); - element._change = {labels: {}}; - const openSpy = sandbox.spy(element, '_openReplyDialog'); - - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - flush(() => { - assert.isTrue(element.$.replyOverlay.opened); - element.$.replyOverlay.close(); - assert.isFalse(element.$.replyOverlay.opened); - assert(openSpy.lastCall.calledWithExactly( - element.$.replyDialog.FocusTarget.ANY), - '_openReplyDialog should have been passed ANY'); - assert.equal(openSpy.callCount, 1); - done(); - }); - }); - - test('fullscreen-overlay-opened hides content', () => { - element._loggedIn = true; - element._loading = false; - element._change = { - owner: {_account_id: 1}, - labels: {}, - actions: { - abandon: { - enabled: true, - label: 'Abandon', - method: 'POST', - title: 'Abandon', - }, - }, - }; - sandbox.spy(element, '_handleHideBackgroundContent'); - element.$.replyDialog.fire('fullscreen-overlay-opened'); - assert.isTrue(element._handleHideBackgroundContent.called); - assert.isTrue(element.$.mainContent.classList.contains('overlayOpen')); - assert.equal(getComputedStyle(element.$.actions).display, 'flex'); - }); - - test('fullscreen-overlay-closed shows content', () => { - element._loggedIn = true; - element._loading = false; - element._change = { - owner: {_account_id: 1}, - labels: {}, - actions: { - abandon: { - enabled: true, - label: 'Abandon', - method: 'POST', - title: 'Abandon', - }, - }, - }; - sandbox.spy(element, '_handleShowBackgroundContent'); - element.$.replyDialog.fire('fullscreen-overlay-closed'); - assert.isTrue(element._handleShowBackgroundContent.called); - assert.isFalse(element.$.mainContent.classList.contains('overlayOpen')); - }); - - test('expand all messages when expand-diffs fired', () => { - const handleExpand = - sandbox.stub(element.$.fileList, 'expandAllDiffs'); - element.$.fileListHeader.fire('expand-diffs'); - assert.isTrue(handleExpand.called); - }); - - test('collapse all messages when collapse-diffs fired', () => { - const handleCollapse = - sandbox.stub(element.$.fileList, 'collapseAllDiffs'); - element.$.fileListHeader.fire('collapse-diffs'); - assert.isTrue(handleCollapse.called); - }); - - test('X should expand all messages', done => { - flush(() => { - const handleExpand = sandbox.stub(element.messagesList, - 'handleExpandCollapse'); - MockInteractions.pressAndReleaseKeyOn(element, 88, null, 'x'); - assert(handleExpand.calledWith(true)); - done(); - }); - }); - - test('Z should collapse all messages', done => { - flush(() => { - const handleExpand = sandbox.stub(element.messagesList, - 'handleExpandCollapse'); - MockInteractions.pressAndReleaseKeyOn(element, 90, null, 'z'); - assert(handleExpand.calledWith(false)); - done(); - }); - }); - - test('shift + R should fetch and navigate to the latest patch set', - done => { - element._changeNum = '42'; - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - _number: 42, - revisions: { - rev1: {_number: 1, commit: {parents: []}}, - }, - current_revision: 'rev1', - status: 'NEW', - labels: {}, - actions: {}, - }; - - navigateToChangeStub.restore(); - navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange', - (change, patchNum, basePatchNum) => { - assert.equal(change, element._change); - assert.isUndefined(patchNum); - assert.isUndefined(basePatchNum); - done(); - }); - - MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); - }); - - test('d should open download overlay', () => { - const stub = sandbox.stub(element.$.downloadOverlay, 'open'); - MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd'); - assert.isTrue(stub.called); - }); - - test(', should open diff preferences', () => { - const stub = sandbox.stub( - element.$.fileList.$.diffPreferencesDialog, 'open'); - element._loggedIn = false; - element.disableDiffPrefs = true; - MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); - assert.isFalse(stub.called); - - element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); - assert.isFalse(stub.called); - - element.disableDiffPrefs = false; - MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); - assert.isTrue(stub.called); - }); - - test('m should toggle diff mode', () => { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); - const setModeStub = sandbox.stub(element.$.fileListHeader, - 'setDiffViewMode'); - const e = {preventDefault: () => {}}; - flushAsynchronousOperations(); - - element.viewState.diffMode = 'SIDE_BY_SIDE'; - element._handleToggleDiffMode(e); - assert.isTrue(setModeStub.calledWith('UNIFIED_DIFF')); - - element.viewState.diffMode = 'UNIFIED_DIFF'; - element._handleToggleDiffMode(e); - assert.isTrue(setModeStub.calledWith('SIDE_BY_SIDE')); - }); - }); - - suite('reloading drafts', () => { - let reloadStub; - const drafts = { - 'testfile.txt': [ - { - patch_set: 5, - id: 'dd2982f5_c01c9e6a', - line: 1, - updated: '2017-11-08 18:47:45.000000000', - message: 'test', - unresolved: true, - }, - ], - }; - setup(() => { - // Fake computeDraftCount as its required for ChangeComments, - // see gr-comment-api#reloadDrafts. - reloadStub = sandbox.stub(element.$.commentAPI, 'reloadDrafts') - .returns(Promise.resolve({ - drafts, - getAllThreadsForChange: () => ([]), - computeDraftCount: () => 1, - })); - }); - - test('drafts are reloaded when reload-drafts fired', done => { - element.$.fileList.fire('reload-drafts', { - resolve: () => { - assert.isTrue(reloadStub.called); - assert.deepEqual(element._diffDrafts, drafts); - done(); - }, - }); - }); - - test('drafts are reloaded when comment-refresh fired', () => { - element.fire('comment-refresh'); - assert.isTrue(reloadStub.called); - }); - }); - - test('diff comments modified', () => { - sandbox.spy(element, '_handleReloadCommentThreads'); - return element._reloadComments().then(() => { - element.fire('diff-comments-modified'); - assert.isTrue(element._handleReloadCommentThreads.called); - }); - }); - - test('thread list modified', () => { - sandbox.spy(element, '_handleReloadDiffComments'); - element._currentView = CommentTabs.COMMENT_THREADS; - flushAsynchronousOperations(); - - return element._reloadComments().then(() => { - element.threadList.fire('thread-list-modified'); - assert.isTrue(element._handleReloadDiffComments.called); - - let draftStub = sinon.stub(element._changeComments, 'computeDraftCount') - .returns(1); - assert.equal(element._computeTotalCommentCounts(5, - element._changeComments), '5 unresolved, 1 draft'); - assert.equal(element._computeTotalCommentCounts(0, - element._changeComments), '1 draft'); - draftStub.restore(); - draftStub = sinon.stub(element._changeComments, 'computeDraftCount') - .returns(0); - assert.equal(element._computeTotalCommentCounts(0, - element._changeComments), ''); - assert.equal(element._computeTotalCommentCounts(1, - element._changeComments), '1 unresolved'); - draftStub.restore(); - draftStub = sinon.stub(element._changeComments, 'computeDraftCount') - .returns(2); - assert.equal(element._computeTotalCommentCounts(1, - element._changeComments), '1 unresolved, 2 drafts'); - draftStub.restore(); - }); - }); - - suite('thread list and change log tabs', () => { - setup(() => { - element._changeNum = '1'; - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - revisions: { - rev2: {_number: 2, commit: {parents: []}}, - rev1: {_number: 1, commit: {parents: []}}, - rev13: {_number: 13, commit: {parents: []}}, - rev3: {_number: 3, commit: {parents: []}}, - }, - current_revision: 'rev3', - status: 'NEW', - labels: { - test: { - all: [], - default_value: 0, - values: [], - approved: {}, - }, - }, - }; - sandbox.stub(element.$.relatedChanges, 'reload'); - sandbox.stub(element, '_reload').returns(Promise.resolve()); - sandbox.spy(element, '_paramsChanged'); - element.params = {view: 'change', changeNum: '1'}; - }); - - test('tab switch works correctly', done => { - assert.isTrue(element._paramsChanged.called); - assert.equal(element.$.commentTabs.selected, CommentTabs.CHANGE_LOG); - assert.equal(element._currentView, CommentTabs.CHANGE_LOG); - - const commentTab = element.shadowRoot.querySelector( - 'paper-tab.commentThreads' - ); - // Switch to comment thread tab - MockInteractions.tap(commentTab); - const commentTabs = element.$.commentTabs; - assert.equal(commentTabs.selected, - CommentTabs.COMMENT_THREADS); - assert.equal(element._currentView, CommentTabs.COMMENT_THREADS); - - // Switch back to 'Change Log' tab - element._paramsChanged(element.params); - flush(() => { - assert.equal(commentTabs.selected, - CommentTabs.CHANGE_LOG); - assert.equal(element._currentView, CommentTabs.CHANGE_LOG); - done(); - }); - }); - }); - - suite('Findings comment tab', () => { - setup(done => { - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - revisions: { - rev2: {_number: 2, commit: {parents: []}}, - rev1: {_number: 1, commit: {parents: []}}, - rev13: {_number: 13, commit: {parents: []}}, - rev3: {_number: 3, commit: {parents: []}}, - rev4: {_number: 4, commit: {parents: []}}, - }, - current_revision: 'rev4', - }; - element._commentThreads = THREADS; - const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); - MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]); - flush(() => { - done(); - }); - }); - - test('robot comments count per patchset', () => { - const count = element._robotCommentCountPerPatchSet(THREADS); - const expectedCount = { - 2: 1, - 3: 1, - 4: 2, - }; - assert.deepEqual(count, expectedCount); - assert.equal(element._computeText({_number: 2}, THREADS), - 'Patchset 2 (1 finding)'); - assert.equal(element._computeText({_number: 4}, THREADS), - 'Patchset 4 (2 findings)'); - assert.equal(element._computeText({_number: 5}, THREADS), - 'Patchset 5'); - }); - - test('only robot comments are rendered', () => { - assert.equal(element._robotCommentThreads.length, 2); - assert.equal(element._robotCommentThreads[0].comments[0].robot_id, - 'rc1'); - assert.equal(element._robotCommentThreads[1].comments[0].robot_id, - 'rc2'); - }); - - test('changing patchsets resets robot comments', done => { - element.set('_change.current_revision', 'rev3'); - flush(() => { - assert.equal(element._robotCommentThreads.length, 1); - done(); - }); - }); - - test('Show more button is hidden', () => { - assert.isNull(element.shadowRoot.querySelector('.show-robot-comments')); - }); - - suite('robot comments show more button', () => { - setup(done => { - const arr = []; - for (let i = 0; i <= 30; i++) { - arr.push(...THREADS); - } - element._commentThreads = arr; - flush(() => { - done(); - }); - }); - - test('Show more button is rendered', () => { - assert.isOk(element.shadowRoot.querySelector('.show-robot-comments')); - assert.equal(element._robotCommentThreads.length, - ROBOT_COMMENTS_LIMIT); - }); - - test('Clicking show more button renders all comments', done => { - MockInteractions.tap(element.shadowRoot.querySelector( - '.show-robot-comments')); - flush(() => { - assert.equal(element._robotCommentThreads.length, 62); - done(); - }); - }); - }); - }); - - test('reply button is not visible when logged out', () => { - assert.equal(getComputedStyle(element.$.replyBtn).display, 'none'); - element._loggedIn = true; - assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none'); - }); - - test('download tap calls _handleOpenDownloadDialog', () => { - sandbox.stub(element, '_handleOpenDownloadDialog'); - element.$.actions.fire('download-tap'); - assert.isTrue(element._handleOpenDownloadDialog.called); - }); - - test('fetches the server config on attached', done => { - flush(() => { - assert.equal(element._serverConfig.test, 'config'); - done(); - }); - }); - - test('_changeStatuses', () => { - sandbox.stub(element, 'changeStatuses').returns( - ['Merged', 'WIP']); - element._loading = false; element._change = { change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', revisions: { - rev2: {_number: 2}, - rev1: {_number: 1}, - rev13: {_number: 13}, - rev3: {_number: 3}, - }, - current_revision: 'rev3', - labels: { - test: { - all: [], - default_value: 0, - values: [], - approved: {}, - }, - }, - }; - element._mergeable = true; - const expectedStatuses = ['Merged', 'WIP']; - assert.deepEqual(element._changeStatuses, expectedStatuses); - assert.equal(element._changeStatus, expectedStatuses.join(', ')); - flushAsynchronousOperations(); - const statusChips = Polymer.dom(element.root) - .querySelectorAll('gr-change-status'); - assert.equal(statusChips.length, 2); - }); - - test('diff preferences open when open-diff-prefs is fired', () => { - const overlayOpenStub = sandbox.stub(element.$.fileList, - 'openDiffPrefs'); - element.$.fileListHeader.fire('open-diff-prefs'); - assert.isTrue(overlayOpenStub.called); - }); - - test('_prepareCommitMsgForLinkify', () => { - let commitMessage = 'R=test@google.com'; - let result = element._prepareCommitMsgForLinkify(commitMessage); - assert.equal(result, 'R=\u200Btest@google.com'); - - commitMessage = 'R=test@google.com\nR=test@google.com'; - result = element._prepareCommitMsgForLinkify(commitMessage); - assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com'); - - commitMessage = 'CC=test@google.com'; - result = element._prepareCommitMsgForLinkify(commitMessage); - assert.equal(result, 'CC=\u200Btest@google.com'); - }), - - test('_isSubmitEnabled', () => { - assert.isFalse(element._isSubmitEnabled({})); - assert.isFalse(element._isSubmitEnabled({submit: {}})); - assert.isTrue(element._isSubmitEnabled( - {submit: {enabled: true}})); - }); - - test('_reload is called when an approved label is removed', () => { - const vote = {_account_id: 1, name: 'bojack', value: 1}; - element._changeNum = '42'; - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - owner: {email: 'abc@def'}, - revisions: { rev2: {_number: 2, commit: {parents: []}}, rev1: {_number: 1, commit: {parents: []}}, rev13: {_number: 13, commit: {parents: []}}, @@ -934,1312 +720,1536 @@ status: 'NEW', labels: { test: { - all: [vote], + all: [], default_value: 0, values: [], approved: {}, }, }, }; - flushAsynchronousOperations(); - const reloadStub = sandbox.stub(element, '_reload'); - element.splice('_change.labels.test.all', 0, 1); - assert.isFalse(reloadStub.called); - element._change.labels.test.all.push(vote); - element._change.labels.test.all.push(vote); - element._change.labels.test.approved = vote; - flushAsynchronousOperations(); - element.splice('_change.labels.test.all', 0, 2); - assert.isTrue(reloadStub.called); - assert.isTrue(reloadStub.calledOnce); - }); - - test('reply button has updated count when there are drafts', () => { - const getLabel = element._computeReplyButtonLabel; - - assert.equal(getLabel(null, false), 'Reply'); - assert.equal(getLabel(null, true), 'Start review'); - - const changeRecord = {base: null}; - assert.equal(getLabel(changeRecord, false), 'Reply'); - - changeRecord.base = {}; - assert.equal(getLabel(changeRecord, false), 'Reply'); - - changeRecord.base = { - 'file1.txt': [{}], - 'file2.txt': [{}, {}], - }; - assert.equal(getLabel(changeRecord, false), 'Reply (3)'); - }); - - test('start review button when owner of WIP change', () => { - assert.equal( - element._computeReplyButtonLabel(null, true), - 'Start review'); - }); - - test('comment events properly update diff drafts', () => { - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 2, - }; - const draft = { - __draft: true, - id: 'id1', - path: '/foo/bar.txt', - text: 'hello', - }; - element._handleCommentSave({detail: {comment: draft}}); - draft.patch_set = 2; - assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]}); - draft.patch_set = null; - draft.text = 'hello, there'; - element._handleCommentSave({detail: {comment: draft}}); - draft.patch_set = 2; - assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]}); - const draft2 = { - __draft: true, - id: 'id2', - path: '/foo/bar.txt', - text: 'hola', - }; - element._handleCommentSave({detail: {comment: draft2}}); - draft2.patch_set = 2; - assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]}); - draft.patch_set = null; - element._handleCommentDiscard({detail: {comment: draft}}); - draft.patch_set = 2; - assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]}); - element._handleCommentDiscard({detail: {comment: draft2}}); - assert.deepEqual(element._diffDrafts, {}); - }); - - test('change num change', () => { - element._changeNum = null; - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 2, - }; - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - labels: {}, - }; - element.viewState.changeNum = null; - element.viewState.diffMode = 'UNIFIED'; - assert.equal(element.viewState.numFilesShown, 200); - assert.equal(element._numFilesShown, 200); - element._numFilesShown = 150; - flushAsynchronousOperations(); - assert.equal(element.viewState.diffMode, 'UNIFIED'); - assert.equal(element.viewState.numFilesShown, 150); - - element._changeNum = '1'; - element.params = {changeNum: '1'}; - element._change.newProp = '1'; - flushAsynchronousOperations(); - assert.equal(element.viewState.diffMode, 'UNIFIED'); - assert.equal(element.viewState.changeNum, '1'); - - element._changeNum = '2'; - element.params = {changeNum: '2'}; - element._change.newProp = '2'; - flushAsynchronousOperations(); - assert.equal(element.viewState.diffMode, 'UNIFIED'); - assert.equal(element.viewState.changeNum, '2'); - assert.equal(element.viewState.numFilesShown, 200); - assert.equal(element._numFilesShown, 200); - }); - - test('_setDiffViewMode is called with reset when new change is loaded', - () => { - sandbox.stub(element, '_setDiffViewMode'); - element.viewState = {changeNum: 1}; - element._changeNum = 2; - element._resetFileListViewState(); - assert.isTrue( - element._setDiffViewMode.lastCall.calledWithExactly(true)); - }); - - test('diffViewMode is propagated from file list header', () => { - element.viewState = {diffMode: 'UNIFIED'}; - element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE'; - assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE'); - }); - - test('diffMode defaults to side by side without preferences', done => { - sandbox.stub(element.$.restAPI, 'getPreferences').returns( - Promise.resolve({})); - // No user prefs or diff view mode set. - - element._setDiffViewMode().then(() => { - assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE'); - done(); - }); - }); - - test('diffMode defaults to preference when not already set', done => { - sandbox.stub(element.$.restAPI, 'getPreferences').returns( - Promise.resolve({default_diff_view: 'UNIFIED'})); - - element._setDiffViewMode().then(() => { - assert.equal(element.viewState.diffMode, 'UNIFIED'); - done(); - }); - }); - - test('existing diffMode overrides preference', done => { - element.viewState.diffMode = 'SIDE_BY_SIDE'; - sandbox.stub(element.$.restAPI, 'getPreferences').returns( - Promise.resolve({default_diff_view: 'UNIFIED'})); - element._setDiffViewMode().then(() => { - assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE'); - done(); - }); - }); - - test('don’t reload entire page when patchRange changes', () => { - const reloadStub = sandbox.stub(element, '_reload', - () => Promise.resolve()); - const reloadPatchDependentStub = sandbox.stub(element, - '_reloadPatchNumDependentResources', - () => Promise.resolve()); - const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear'); - const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs'); - - const value = { - view: Gerrit.Nav.View.CHANGE, - patchNum: '1', - }; - element._paramsChanged(value); - assert.isTrue(reloadStub.calledOnce); - assert.isTrue(relatedClearSpy.calledOnce); - - element._initialLoadComplete = true; - - value.basePatchNum = '1'; - value.patchNum = '2'; - element._paramsChanged(value); - assert.isFalse(reloadStub.calledTwice); - assert.isTrue(reloadPatchDependentStub.calledOnce); - assert.isTrue(relatedClearSpy.calledOnce); - assert.isTrue(collapseStub.calledTwice); - }); - - test('reload entire page when patchRange doesnt change', () => { - const reloadStub = sandbox.stub(element, '_reload', - () => Promise.resolve()); - const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs'); - const value = { - view: Gerrit.Nav.View.CHANGE, - }; - element._paramsChanged(value); - assert.isTrue(reloadStub.calledOnce); - element._initialLoadComplete = true; - element._paramsChanged(value); - assert.isTrue(reloadStub.calledTwice); - assert.isTrue(collapseStub.calledTwice); - }); - - test('related changes are updated and new patch selected after rebase', - done => { - element._changeNum = '42'; - sandbox.stub(element, 'computeLatestPatchNum', () => 1); - sandbox.stub(element, '_reload', - () => Promise.resolve()); - const e = {detail: {action: 'rebase'}}; - element._handleReloadChange(e).then(() => { - assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly( - element._change)); - done(); - }); - }); - - test('related changes are not updated after other action', done => { - sandbox.stub(element, '_reload', () => Promise.resolve()); - sandbox.stub(element.$.relatedChanges, 'reload'); - const e = {detail: {action: 'abandon'}}; - element._handleReloadChange(e).then(() => { - assert.isFalse(navigateToChangeStub.called); - done(); - }); - }); - - test('_computeMergedCommitInfo', () => { - const dummyRevs = { - 1: {commit: {commit: 1}}, - 2: {commit: {}}, - }; - assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {}); - assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs), - dummyRevs[1].commit); - - // Regression test for issue 5337. - const commit = element._computeMergedCommitInfo(2, dummyRevs); - assert.notDeepEqual(commit, dummyRevs[2]); - assert.deepEqual(commit, {commit: 2}); - }); - - test('_computeCopyTextForTitle', () => { - const change = { - _number: 123, - subject: 'test subject', - revisions: { - rev1: {_number: 1}, - rev3: {_number: 3}, - }, - current_revision: 'rev3', - }; - sandbox.stub(Gerrit.Nav, 'getUrlForChange') - .returns('/change/123'); - assert.equal( - element._computeCopyTextForTitle(change), - '123: test subject | https://localhost:8081/change/123' - ); - }); - - test('get latest revision', () => { - let change = { - revisions: { - rev1: {_number: 1}, - rev3: {_number: 3}, - }, - current_revision: 'rev3', - }; - assert.equal(element._getLatestRevisionSHA(change), 'rev3'); - change = { - revisions: { - rev1: {_number: 1}, - }, - }; - assert.equal(element._getLatestRevisionSHA(change), 'rev1'); - }); - - test('show commit message edit button', () => { - const _change = { - status: element.ChangeStatus.MERGED, - }; - assert.isTrue(element._computeHideEditCommitMessage(false, false, {})); - assert.isTrue(element._computeHideEditCommitMessage(true, true, {})); - assert.isTrue(element._computeHideEditCommitMessage(false, true, {})); - assert.isFalse(element._computeHideEditCommitMessage(true, false, {})); - assert.isTrue(element._computeHideEditCommitMessage(true, false, - _change)); - assert.isTrue(element._computeHideEditCommitMessage(true, false, {}, - true)); - assert.isFalse(element._computeHideEditCommitMessage(true, false, {}, - false)); - }); - - test('_handleCommitMessageSave trims trailing whitespace', () => { - const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage') - .returns(Promise.resolve({})); - - const mockEvent = content => { return {detail: {content}}; }; - - element._handleCommitMessageSave(mockEvent('test \n test ')); - assert.equal(putStub.lastCall.args[1], 'test\n test'); - - element._handleCommitMessageSave(mockEvent(' test\ntest')); - assert.equal(putStub.lastCall.args[1], ' test\ntest'); - - element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n')); - assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n'); - }); - - test('_computeChangeIdCommitMessageError', () => { - let commitMessage = - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483'; - let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - null); - - change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'mismatch'); - - commitMessage = 'This is the greatest change.'; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'missing'); - }); - - test('multiple change Ids in commit message picks last', () => { - const commitMessage = [ - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', - ].join('\n'); - let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - null); - change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'mismatch'); - }); - - test('does not count change Id that starts mid line', () => { - const commitMessage = [ - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', - 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', - ].join(' and '); - let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - null); - change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; - assert.equal( - element._computeChangeIdCommitMessageError(commitMessage, change), - 'mismatch'); - }); - - test('_computeTitleAttributeWarning', () => { - let changeIdCommitMessageError = 'missing'; - assert.equal( - element._computeTitleAttributeWarning(changeIdCommitMessageError), - 'No Change-Id in commit message'); - - changeIdCommitMessageError = 'mismatch'; - assert.equal( - element._computeTitleAttributeWarning(changeIdCommitMessageError), - 'Change-Id mismatch'); - }); - - test('_computeChangeIdClass', () => { - let changeIdCommitMessageError = 'missing'; - assert.equal( - element._computeChangeIdClass(changeIdCommitMessageError), ''); - - changeIdCommitMessageError = 'mismatch'; - assert.equal( - element._computeChangeIdClass(changeIdCommitMessageError), 'warning'); - }); - - test('topic is coalesced to null', done => { - sandbox.stub(element, '_changeChanged'); - sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({ - id: '123456789', - labels: {}, - current_revision: 'foo', - revisions: {foo: {commit: {}}}, - })); - - element._getChangeDetail().then(() => { - assert.isNull(element._change.topic); - done(); - }); - }); - - test('commit sha is populated from getChangeDetail', done => { - sandbox.stub(element, '_changeChanged'); - sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({ - id: '123456789', - labels: {}, - current_revision: 'foo', - revisions: {foo: {commit: {}}}, - })); - - element._getChangeDetail().then(() => { - assert.equal('foo', element._commitInfo.commit); - done(); - }); - }); - - test('edit is added to change', () => { - sandbox.stub(element, '_changeChanged'); - sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({ - id: '123456789', - labels: {}, - current_revision: 'foo', - revisions: {foo: {commit: {}}}, - })); - sandbox.stub(element, '_getEdit', () => Promise.resolve({ - base_patch_set_number: 1, - commit: {commit: 'bar'}, - })); - element._patchRange = {}; - - return element._getChangeDetail().then(() => { - const revs = element._change.revisions; - assert.equal(Object.keys(revs).length, 2); - assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}}); - assert.deepEqual(revs['bar'], { - _number: element.EDIT_NAME, - basePatchNum: 1, - commit: {commit: 'bar'}, - fetch: undefined, - }); - }); - }); - - test('_getBasePatchNum', () => { - const _change = { - _number: 42, - revisions: { - '98da160735fb81604b4c40e93c368f380539dd0e': { - _number: 1, - commit: { - parents: [], - }, - }, - }, - }; - const _patchRange = { - basePatchNum: 'PARENT', - }; - assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT'); - - element._prefs = { - default_base_for_merges: 'FIRST_PARENT', - }; - - const _change2 = { - _number: 42, - revisions: { - '98da160735fb81604b4c40e93c368f380539dd0e': { - _number: 1, - commit: { - parents: [ - { - commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8', - subject: 'test', - }, - { - commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841', - subject: 'test3', - }, - ], - }, - }, - }, - }; - assert.equal(element._getBasePatchNum(_change2, _patchRange), -1); - - _patchRange.patchNum = 1; - assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT'); - }); - - test('_openReplyDialog called with `ANY` when coming from tap event', - () => { - const openStub = sandbox.stub(element, '_openReplyDialog'); - element._serverConfig = {}; - MockInteractions.tap(element.$.replyBtn); - assert(openStub.lastCall.calledWithExactly( - element.$.replyDialog.FocusTarget.ANY), - '_openReplyDialog should have been passed ANY'); - assert.equal(openStub.callCount, 1); - }); - - test('_openReplyDialog called with `BODY` when coming from message reply' + - 'event', done => { - flush(() => { - const openStub = sandbox.stub(element, '_openReplyDialog'); - element.messagesList.fire('reply', - {message: {message: 'text'}}); - assert(openStub.lastCall.calledWithExactly( - element.$.replyDialog.FocusTarget.BODY), - '_openReplyDialog should have been passed BODY'); - assert.equal(openStub.callCount, 1); - done(); - }); - }); - - test('reply dialog focus can be controlled', () => { - const FocusTarget = element.$.replyDialog.FocusTarget; - const openStub = sandbox.stub(element, '_openReplyDialog'); - - const e = {detail: {}}; - element._handleShowReplyDialog(e); - assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS), - '_openReplyDialog should have been passed REVIEWERS'); - assert.equal(openStub.callCount, 1); - - e.detail.value = {ccsOnly: true}; - element._handleShowReplyDialog(e); - assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS), - '_openReplyDialog should have been passed CCS'); - assert.equal(openStub.callCount, 2); - }); - - test('getUrlParameter functionality', () => { - const locationStub = sandbox.stub(element, '_getLocationSearch'); - - locationStub.returns('?test'); - assert.equal(element._getUrlParameter('test'), 'test'); - locationStub.returns('?test2=12&test=3'); - assert.equal(element._getUrlParameter('test'), 'test'); - locationStub.returns(''); - assert.isNull(element._getUrlParameter('test')); - locationStub.returns('?'); - assert.isNull(element._getUrlParameter('test')); - locationStub.returns('?test2'); - assert.isNull(element._getUrlParameter('test')); - }); - - test('revert dialog opened with revert param', done => { - sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true)); - sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => Promise.resolve()); - - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 2, - }; - element._change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - revisions: { - rev1: {_number: 1, commit: {parents: []}}, - rev2: {_number: 2, commit: {parents: []}}, - }, - current_revision: 'rev1', - status: element.ChangeStatus.MERGED, - labels: {}, - actions: {}, - }; - - sandbox.stub(element, '_getUrlParameter', - param => { - assert.equal(param, 'revert'); - return param; - }); - - sandbox.stub(element.$.actions, 'showRevertDialog', - done); - - element._maybeShowRevertDialog(); - assert.isTrue(Gerrit.awaitPluginsLoaded.called); - }); - - suite('scroll related tests', () => { - test('document scrolling calls function to set scroll height', done => { - const originalHeight = document.body.scrollHeight; - const scrollStub = sandbox.stub(element, '_handleScroll', - () => { - assert.isTrue(scrollStub.called); - document.body.style.height = originalHeight + 'px'; - scrollStub.restore(); - done(); - }); - document.body.style.height = '10000px'; - element._handleScroll(); - }); - - test('scrollTop is set correctly', () => { - element.viewState = {scrollTop: TEST_SCROLL_TOP_PX}; - - sandbox.stub(element, '_reload', () => { - // When element is reloaded, ensure that the history - // state has the scrollTop set earlier. This will then - // be reset. - assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX); - return Promise.resolve({}); - }); - - // simulate reloading component, which is done when route - // changes to match a regex of change view type. - element._paramsChanged({view: Gerrit.Nav.View.CHANGE}); - }); - - test('scrollTop is reset when new change is loaded', () => { - element._resetFileListViewState(); - assert.equal(element.viewState.scrollTop, 0); - }); - }); - - suite('reply dialog tests', () => { - setup(() => { - sandbox.stub(element.$.replyDialog, '_draftChanged'); - sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates', - () => Promise.resolve({isLatest: true})); - element._change = {labels: {}}; - }); - - test('reply from comment adds quote text', () => { - const e = {detail: {message: {message: 'quote text'}}}; - element._handleMessageReply(e); - assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); - }); - - test('reply from comment replaces quote text', () => { - element.$.replyDialog.draft = '> old quote text\n\n some draft text'; - element.$.replyDialog.quote = '> old quote text\n\n'; - const e = {detail: {message: {message: 'quote text'}}}; - element._handleMessageReply(e); - assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); - }); - - test('reply from same comment preserves quote text', () => { - element.$.replyDialog.draft = '> quote text\n\n some draft text'; - element.$.replyDialog.quote = '> quote text\n\n'; - const e = {detail: {message: {message: 'quote text'}}}; - element._handleMessageReply(e); - assert.equal(element.$.replyDialog.draft, - '> quote text\n\n some draft text'); - assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); - }); - - test('reply from top of page contains previous draft', () => { - const div = document.createElement('div'); - element.$.replyDialog.draft = '> quote text\n\n some draft text'; - element.$.replyDialog.quote = '> quote text\n\n'; - const e = {target: div, preventDefault: sandbox.spy()}; - element._handleReplyTap(e); - assert.equal(element.$.replyDialog.draft, - '> quote text\n\n some draft text'); - assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); - }); - }); - - test('reply button is disabled until server config is loaded', () => { - assert.isTrue(element._replyDisabled); - element._serverConfig = {}; - assert.isFalse(element._replyDisabled); - }); - - suite('commit message expand/collapse', () => { - setup(() => { - sandbox.stub(element, 'fetchChangeUpdates', - () => Promise.resolve({isLatest: false})); - }); - - test('commitCollapseToggle hidden for short commit message', () => { - element._latestCommitMessage = ''; - assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden')); - }); - - test('commitCollapseToggle shown for long commit message', () => { - element._latestCommitMessage = _.times(31, String).join('\n'); - assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden')); - }); - - test('commitCollapseToggle functions', () => { - element._latestCommitMessage = _.times(35, String).join('\n'); - assert.isTrue(element._commitCollapsed); - assert.isTrue(element._commitCollapsible); - assert.isTrue( - element.$.commitMessageEditor.hasAttribute('collapsed')); - MockInteractions.tap(element.$.commitCollapseToggleButton); - assert.isFalse(element._commitCollapsed); - assert.isTrue(element._commitCollapsible); - assert.isFalse( - element.$.commitMessageEditor.hasAttribute('collapsed')); - }); - }); - - suite('related changes expand/collapse', () => { - let updateHeightSpy; - setup(() => { - updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight'); - }); - - test('relatedChangesToggle shown height greater than changeInfo height', - () => { - assert.isFalse(element.$.relatedChangesToggle.classList - .contains('showToggle')); - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(element, '_getScrollHeight', () => 60); - sandbox.stub(element, '_getLineHeight', () => 5); - sandbox.stub(window, 'matchMedia', () => { return {matches: true}; }); - element.$.relatedChanges.dispatchEvent( - new CustomEvent('new-section-loaded')); - assert.isTrue(element.$.relatedChangesToggle.classList - .contains('showToggle')); - assert.equal(updateHeightSpy.callCount, 1); - }); - - test('relatedChangesToggle hidden height less than changeInfo height', - () => { - assert.isFalse(element.$.relatedChangesToggle.classList - .contains('showToggle')); - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(element, '_getScrollHeight', () => 40); - sandbox.stub(element, '_getLineHeight', () => 5); - sandbox.stub(window, 'matchMedia', () => { return {matches: true}; }); - element.$.relatedChanges.dispatchEvent( - new CustomEvent('new-section-loaded')); - assert.isFalse(element.$.relatedChangesToggle.classList - .contains('showToggle')); - assert.equal(updateHeightSpy.callCount, 1); - }); - - test('relatedChangesToggle functions', () => { - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(window, 'matchMedia', () => { return {matches: false}; }); - element._relatedChangesLoading = false; - assert.isTrue(element._relatedChangesCollapsed); - assert.isTrue( - element.$.relatedChanges.classList.contains('collapsed')); - MockInteractions.tap(element.$.relatedChangesToggleButton); - assert.isFalse(element._relatedChangesCollapsed); - assert.isFalse( - element.$.relatedChanges.classList.contains('collapsed')); - }); - - test('_updateRelatedChangeMaxHeight without commit toggle', () => { - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(element, '_getLineHeight', () => 12); - sandbox.stub(window, 'matchMedia', () => { return {matches: false}; }); - - // 50 (existing height) - 30 (extra height) = 20 (adjusted height). - // 20 (max existing height) % 12 (line height) = 6 (remainder). - // 20 (adjusted height) - 8 (remainder) = 12 (max height to set). - - element._updateRelatedChangeMaxHeight(); - assert.equal(getCustomCssValue('--relation-chain-max-height'), - '12px'); - assert.equal(getCustomCssValue('--related-change-btn-top-padding'), - ''); - }); - - test('_updateRelatedChangeMaxHeight with commit toggle', () => { - element._latestCommitMessage = _.times(31, String).join('\n'); - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(element, '_getLineHeight', () => 12); - sandbox.stub(window, 'matchMedia', () => { return {matches: false}; }); - - // 50 (existing height) % 12 (line height) = 2 (remainder). - // 50 (existing height) - 2 (remainder) = 48 (max height to set). - - element._updateRelatedChangeMaxHeight(); - assert.equal(getCustomCssValue('--relation-chain-max-height'), - '48px'); - assert.equal(getCustomCssValue('--related-change-btn-top-padding'), - '2px'); - }); - - test('_updateRelatedChangeMaxHeight in small screen mode', () => { - element._latestCommitMessage = _.times(31, String).join('\n'); - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(element, '_getLineHeight', () => 12); - sandbox.stub(window, 'matchMedia', () => { return {matches: true}; }); - - element._updateRelatedChangeMaxHeight(); - - // 400 (new height) % 12 (line height) = 4 (remainder). - // 400 (new height) - 4 (remainder) = 396. - - assert.equal(getCustomCssValue('--relation-chain-max-height'), - '396px'); - }); - - test('_updateRelatedChangeMaxHeight in medium screen mode', () => { - element._latestCommitMessage = _.times(31, String).join('\n'); - sandbox.stub(element, '_getOffsetHeight', () => 50); - sandbox.stub(element, '_getLineHeight', () => 12); - sandbox.stub(window, 'matchMedia', () => { - if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') { - return {matches: true}; - } else { - return {matches: false}; - } - }); - - // 100 (new height) % 12 (line height) = 4 (remainder). - // 100 (new height) - 4 (remainder) = 96. - element._updateRelatedChangeMaxHeight(); - assert.equal(getCustomCssValue('--relation-chain-max-height'), - '96px'); - }); - - suite('update checks', () => { - setup(() => { - sandbox.spy(element, '_startUpdateCheckTimer'); - sandbox.stub(element, 'async', f => { - // Only fire the async callback one time. - if (element.async.callCount > 1) { return; } - f.call(element); - }); - }); - - test('_startUpdateCheckTimer negative delay', () => { - sandbox.stub(element, 'fetchChangeUpdates'); - - element._serverConfig = {change: {update_delay: -1}}; - - assert.isTrue(element._startUpdateCheckTimer.called); - assert.isFalse(element.fetchChangeUpdates.called); - }); - - test('_startUpdateCheckTimer up-to-date', () => { - sandbox.stub(element, 'fetchChangeUpdates', - () => Promise.resolve({isLatest: true})); - - element._serverConfig = {change: {update_delay: 12345}}; - - assert.isTrue(element._startUpdateCheckTimer.called); - assert.isTrue(element.fetchChangeUpdates.called); - assert.equal(element.async.lastCall.args[1], 12345 * 1000); - }); - - test('_startUpdateCheckTimer out-of-date shows an alert', done => { - sandbox.stub(element, 'fetchChangeUpdates', - () => Promise.resolve({isLatest: false})); - element.addEventListener('show-alert', e => { - assert.equal(e.detail.message, - 'A newer patch set has been uploaded'); - done(); - }); - element._serverConfig = {change: {update_delay: 12345}}; - }); - - test('_startUpdateCheckTimer new status shows an alert', done => { - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({ - isLatest: true, - newStatus: element.ChangeStatus.MERGED, - })); - element.addEventListener('show-alert', e => { - assert.equal(e.detail.message, 'This change has been merged'); - done(); - }); - element._serverConfig = {change: {update_delay: 12345}}; - }); - - test('_startUpdateCheckTimer new messages shows an alert', done => { - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({ - isLatest: true, - newMessages: true, - })); - element.addEventListener('show-alert', e => { - assert.equal(e.detail.message, - 'There are new messages on this change'); - done(); - }); - element._serverConfig = {change: {update_delay: 12345}}; - }); - }); - - test('canStartReview computation', () => { - const change1 = {}; - const change2 = { - actions: { - ready: { - enabled: true, - }, - }, - }; - const change3 = { - actions: { - ready: { - label: 'Ready for Review', - }, - }, - }; - assert.isFalse(element._computeCanStartReview(change1)); - assert.isTrue(element._computeCanStartReview(change2)); - assert.isFalse(element._computeCanStartReview(change3)); - }); - }); - - test('header class computation', () => { - assert.equal(element._computeHeaderClass(), 'header'); - assert.equal(element._computeHeaderClass(true), 'header editMode'); - }); - - test('_maybeScrollToMessage', done => { - flush(() => { - const scrollStub = sandbox.stub(element.messagesList, - 'scrollToMessage'); - - element._maybeScrollToMessage(''); - assert.isFalse(scrollStub.called); - element._maybeScrollToMessage('message'); - assert.isFalse(scrollStub.called); - element._maybeScrollToMessage('#message-TEST'); - assert.isTrue(scrollStub.called); - assert.equal(scrollStub.lastCall.args[0], 'TEST'); - done(); - }); - }); - - test('topic update reloads related changes', () => { - sandbox.stub(element.$.relatedChanges, 'reload'); - element.dispatchEvent(new CustomEvent('topic-changed')); - assert.isTrue(element.$.relatedChanges.reload.calledOnce); - }); - - test('_computeEditMode', () => { - const callCompute = (range, params) => - element._computeEditMode({base: range}, {base: params}); - assert.isFalse(callCompute({}, {})); - assert.isTrue(callCompute({}, {edit: true})); - assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {})); - assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {})); - assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {})); - }); - - test('_processEdit', () => { - element._patchRange = {}; - const change = { - current_revision: 'foo', - revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}}, - }; - let mockChange; - - // With no edit, mockChange should be unmodified. - element._processEdit(mockChange = _.cloneDeep(change), null); - assert.deepEqual(mockChange, change); - - // When edit is not based on the latest PS, current_revision should be - // unmodified. - const edit = { - base_patch_set_number: 1, - commit: {commit: 'bar'}, - fetch: true, - }; - element._processEdit(mockChange = _.cloneDeep(change), edit); - assert.notDeepEqual(mockChange, change); - assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME); - assert.equal(mockChange.current_revision, change.current_revision); - assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'}); - assert.notOk(mockChange.revisions.bar.actions); - - edit.base_revision = 'foo'; - element._processEdit(mockChange = _.cloneDeep(change), edit); - assert.notDeepEqual(mockChange, change); - assert.equal(mockChange.current_revision, 'bar'); - assert.deepEqual(mockChange.revisions.bar.actions, - mockChange.revisions.foo.actions); - - // If _patchRange.patchNum is defined, do not load edit. - element._patchRange.patchNum = 'baz'; - change.current_revision = 'baz'; - element._processEdit(mockChange = _.cloneDeep(change), edit); - assert.equal(element._patchRange.patchNum, 'baz'); - assert.notOk(mockChange.revisions.bar.actions); - }); - - test('file-action-tap handling', () => { - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; - const fileList = element.$.fileList; - const Actions = GrEditConstants.Actions; - const controls = element.$.fileListHeader.$.editControls; - sandbox.stub(controls, 'openDeleteDialog'); - sandbox.stub(controls, 'openRenameDialog'); - sandbox.stub(controls, 'openRestoreDialog'); - sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'); - sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); - - // Delete - fileList.dispatchEvent(new CustomEvent('file-action-tap', { - detail: {action: Actions.DELETE.id, path: 'foo'}, - bubbles: true, - composed: true, - })); - flushAsynchronousOperations(); - - assert.isTrue(controls.openDeleteDialog.called); - assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo'); - - // Restore - fileList.dispatchEvent(new CustomEvent('file-action-tap', { - detail: {action: Actions.RESTORE.id, path: 'foo'}, - bubbles: true, - composed: true, - })); - flushAsynchronousOperations(); - - assert.isTrue(controls.openRestoreDialog.called); - assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo'); - - // Rename - fileList.dispatchEvent(new CustomEvent('file-action-tap', { - detail: {action: Actions.RENAME.id, path: 'foo'}, - bubbles: true, - composed: true, - })); - flushAsynchronousOperations(); - - assert.isTrue(controls.openRenameDialog.called); - assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo'); - - // Open - fileList.dispatchEvent(new CustomEvent('file-action-tap', { - detail: {action: Actions.OPEN.id, path: 'foo'}, - bubbles: true, - composed: true, - })); - flushAsynchronousOperations(); - - assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called); - assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo'); - assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[2], '1'); - assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called); - }); - - test('_selectedRevision updates when patchNum is changed', () => { - const revision1 = {_number: 1, commit: {parents: []}}; - const revision2 = {_number: 2, commit: {parents: []}}; - sandbox.stub(element.$.restAPI, 'getChangeDetail').returns( - Promise.resolve({ - revisions: { - aaa: revision1, - bbb: revision2, - }, - labels: {}, - actions: {}, - current_revision: 'bbb', - change_id: 'loremipsumdolorsitamet', - })); - sandbox.stub(element, '_getEdit').returns(Promise.resolve()); - sandbox.stub(element, '_getPreferences').returns(Promise.resolve({})); - element._patchRange = {patchNum: '2'}; - return element._getChangeDetail().then(() => { - assert.strictEqual(element._selectedRevision, revision2); - - element.set('_patchRange.patchNum', '1'); - assert.strictEqual(element._selectedRevision, revision1); - }); - }); - - test('_selectedRevision is assigned when patchNum is edit', () => { - const revision1 = {_number: 1, commit: {parents: []}}; - const revision2 = {_number: 2, commit: {parents: []}}; - const revision3 = {_number: 'edit', commit: {parents: []}}; - sandbox.stub(element.$.restAPI, 'getChangeDetail').returns( - Promise.resolve({ - revisions: { - aaa: revision1, - bbb: revision2, - ccc: revision3, - }, - labels: {}, - actions: {}, - current_revision: 'ccc', - change_id: 'loremipsumdolorsitamet', - })); - sandbox.stub(element, '_getEdit').returns(Promise.resolve()); - sandbox.stub(element, '_getPreferences').returns(Promise.resolve({})); - element._patchRange = {patchNum: 'edit'}; - return element._getChangeDetail().then(() => { - assert.strictEqual(element._selectedRevision, revision3); - }); - }); - - test('_sendShowChangeEvent', () => { - element._change = {labels: {}}; - element._patchRange = {patchNum: 4}; - element._mergeable = true; - const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent'); - element._sendShowChangeEvent(); - assert.isTrue(showStub.calledOnce); - assert.equal( - showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE); - assert.deepEqual(showStub.lastCall.args[1], { - change: {labels: {}}, - patchNum: 4, - info: {mergeable: true}, - }); - }); - - suite('_handleEditTap', () => { - let fireEdit; - - setup(() => { - fireEdit = () => { - element.$.actions.dispatchEvent(new CustomEvent('edit-tap')); - }; - navigateToChangeStub.restore(); - - element._change = {revisions: {rev1: {_number: 1}}}; - }); - - test('edit exists in revisions', done => { - sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { - assert.equal(args.length, 2); - assert.equal(args[1], element.EDIT_NAME); // patchNum - done(); - }); - - element.set('_change.revisions.rev2', {_number: element.EDIT_NAME}); - flushAsynchronousOperations(); - - fireEdit(); - }); - - test('no edit exists in revisions, non-latest patchset', done => { - sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { - assert.equal(args.length, 4); - assert.equal(args[1], 1); // patchNum - assert.equal(args[3], true); // opt_isEdit - done(); - }); - - element.set('_change.revisions.rev2', {_number: 2}); - element._patchRange = {patchNum: 1}; - flushAsynchronousOperations(); - - fireEdit(); - }); - - test('no edit exists in revisions, latest patchset', done => { - sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { - assert.equal(args.length, 4); - // No patch should be specified when patchNum == latest. - assert.isNotOk(args[1]); // patchNum - assert.equal(args[3], true); // opt_isEdit - done(); - }); - - element.set('_change.revisions.rev2', {_number: 2}); - element._patchRange = {patchNum: 2}; - flushAsynchronousOperations(); - - fireEdit(); - }); - }); - - test('_handleStopEditTap', done => { - sandbox.stub(element.$.metadata, '_computeLabelNames'); - navigateToChangeStub.restore(); - sandbox.stub(element, 'computeLatestPatchNum').returns(1); - sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { - assert.equal(args.length, 2); - assert.equal(args[1], 1); // patchNum - done(); - }); - - element._patchRange = {patchNum: 1}; - element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap', - {bubbles: false})); - }); - - suite('plugin endpoints', () => { - test('endpoint params', done => { - element._change = {labels: {}}; - element._selectedRevision = {}; - let hookEl; - let plugin; - Gerrit.install( - p => { - plugin = p; - plugin.hook('change-view-integration').getLastAttached() - .then( - el => hookEl = el); - }, - '0.1', - 'http://some/plugins/url.html'); - flush(() => { - assert.strictEqual(hookEl.plugin, plugin); - assert.strictEqual(hookEl.change, element._change); - assert.strictEqual(hookEl.revision, element._selectedRevision); - done(); - }); - }); - }); - - suite('_getMergeability', () => { - let getMergeableStub; - - setup(() => { - element._change = {labels: {}}; - getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable') - .returns(Promise.resolve({mergeable: true})); - }); - - test('merged change', () => { - element._mergeable = null; - element._change.status = element.ChangeStatus.MERGED; - return element._getMergeability().then(() => { - assert.isFalse(element._mergeable); - assert.isFalse(getMergeableStub.called); - }); - }); - - test('abandoned change', () => { - element._mergeable = null; - element._change.status = element.ChangeStatus.ABANDONED; - return element._getMergeability().then(() => { - assert.isFalse(element._mergeable); - assert.isFalse(getMergeableStub.called); - }); - }); - - test('open change', () => { - element._mergeable = null; - return element._getMergeability().then(() => { - assert.isTrue(element._mergeable); - assert.isTrue(getMergeableStub.called); - }); - }); - }); - - test('_paramsChanged sets in projectLookup', () => { sandbox.stub(element.$.relatedChanges, 'reload'); sandbox.stub(element, '_reload').returns(Promise.resolve()); - const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - element._paramsChanged({ - view: Gerrit.Nav.View.CHANGE, - changeNum: 101, - project: 'test-project', - }); - assert.isTrue(setStub.calledOnce); - assert.isTrue(setStub.calledWith(101, 'test-project')); + sandbox.spy(element, '_paramsChanged'); + element.params = {view: 'change', changeNum: '1'}; }); - test('_handleToggleStar called when star is tapped', () => { + test('tab switch works correctly', done => { + assert.isTrue(element._paramsChanged.called); + assert.equal(element.$.commentTabs.selected, CommentTabs.CHANGE_LOG); + assert.equal(element._currentView, CommentTabs.CHANGE_LOG); + + const commentTab = element.shadowRoot.querySelector( + 'paper-tab.commentThreads' + ); + // Switch to comment thread tab + MockInteractions.tap(commentTab); + const commentTabs = element.$.commentTabs; + assert.equal(commentTabs.selected, + CommentTabs.COMMENT_THREADS); + assert.equal(element._currentView, CommentTabs.COMMENT_THREADS); + + // Switch back to 'Change Log' tab + element._paramsChanged(element.params); + flush(() => { + assert.equal(commentTabs.selected, + CommentTabs.CHANGE_LOG); + assert.equal(element._currentView, CommentTabs.CHANGE_LOG); + done(); + }); + }); + }); + + suite('Findings comment tab', () => { + setup(done => { element._change = { - owner: {_account_id: 1}, - starred: false, + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2, commit: {parents: []}}, + rev1: {_number: 1, commit: {parents: []}}, + rev13: {_number: 13, commit: {parents: []}}, + rev3: {_number: 3, commit: {parents: []}}, + rev4: {_number: 4, commit: {parents: []}}, + }, + current_revision: 'rev4', }; - element._loggedIn = true; - const stub = sandbox.stub(element, '_handleToggleStar'); - flushAsynchronousOperations(); - - MockInteractions.tap(element.$.changeStar.shadowRoot - .querySelector('button')); - assert.isTrue(stub.called); + element._commentThreads = THREADS; + const paperTabs = element.shadowRoot.querySelector('#primaryTabs'); + MockInteractions.tap(paperTabs.querySelectorAll('paper-tab')[2]); + flush(() => { + done(); + }); }); - suite('gr-reporting tests', () => { - setup(() => { - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: 1, - }; - sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve()); - sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve()); - sandbox.stub(element, '_reloadComments').returns(Promise.resolve()); - sandbox.stub(element, '_getMergeability').returns(Promise.resolve()); - sandbox.stub(element, '_getLatestCommitMessage') - .returns(Promise.resolve()); - }); + test('robot comments count per patchset', () => { + const count = element._robotCommentCountPerPatchSet(THREADS); + const expectedCount = { + 2: 1, + 3: 1, + 4: 2, + }; + assert.deepEqual(count, expectedCount); + assert.equal(element._computeText({_number: 2}, THREADS), + 'Patchset 2 (1 finding)'); + assert.equal(element._computeText({_number: 4}, THREADS), + 'Patchset 4 (2 findings)'); + assert.equal(element._computeText({_number: 5}, THREADS), + 'Patchset 5'); + }); - test('don\'t report changedDisplayed on reply', done => { - const changeDisplayStub = - sandbox.stub(element.$.reporting, 'changeDisplayed'); - const changeFullyLoadedStub = - sandbox.stub(element.$.reporting, 'changeFullyLoaded'); - element._handleReplySent(); + test('only robot comments are rendered', () => { + assert.equal(element._robotCommentThreads.length, 2); + assert.equal(element._robotCommentThreads[0].comments[0].robot_id, + 'rc1'); + assert.equal(element._robotCommentThreads[1].comments[0].robot_id, + 'rc2'); + }); + + test('changing patchsets resets robot comments', done => { + element.set('_change.current_revision', 'rev3'); + flush(() => { + assert.equal(element._robotCommentThreads.length, 1); + done(); + }); + }); + + test('Show more button is hidden', () => { + assert.isNull(element.shadowRoot.querySelector('.show-robot-comments')); + }); + + suite('robot comments show more button', () => { + setup(done => { + const arr = []; + for (let i = 0; i <= 30; i++) { + arr.push(...THREADS); + } + element._commentThreads = arr; flush(() => { - assert.isFalse(changeDisplayStub.called); - assert.isFalse(changeFullyLoadedStub.called); done(); }); }); - test('report changedDisplayed on _paramsChanged', done => { - const changeDisplayStub = - sandbox.stub(element.$.reporting, 'changeDisplayed'); - const changeFullyLoadedStub = - sandbox.stub(element.$.reporting, 'changeFullyLoaded'); - element._paramsChanged({ - view: Gerrit.Nav.View.CHANGE, - changeNum: 101, - project: 'test-project', - }); + test('Show more button is rendered', () => { + assert.isOk(element.shadowRoot.querySelector('.show-robot-comments')); + assert.equal(element._robotCommentThreads.length, + ROBOT_COMMENTS_LIMIT); + }); + + test('Clicking show more button renders all comments', done => { + MockInteractions.tap(element.shadowRoot.querySelector( + '.show-robot-comments')); flush(() => { - assert.isTrue(changeDisplayStub.called); - assert.isTrue(changeFullyLoadedStub.called); + assert.equal(element._robotCommentThreads.length, 62); done(); }); }); }); }); + + test('reply button is not visible when logged out', () => { + assert.equal(getComputedStyle(element.$.replyBtn).display, 'none'); + element._loggedIn = true; + assert.notEqual(getComputedStyle(element.$.replyBtn).display, 'none'); + }); + + test('download tap calls _handleOpenDownloadDialog', () => { + sandbox.stub(element, '_handleOpenDownloadDialog'); + element.$.actions.fire('download-tap'); + assert.isTrue(element._handleOpenDownloadDialog.called); + }); + + test('fetches the server config on attached', done => { + flush(() => { + assert.equal(element._serverConfig.test, 'config'); + done(); + }); + }); + + test('_changeStatuses', () => { + sandbox.stub(element, 'changeStatuses').returns( + ['Merged', 'WIP']); + element._loading = false; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2}, + rev1: {_number: 1}, + rev13: {_number: 13}, + rev3: {_number: 3}, + }, + current_revision: 'rev3', + labels: { + test: { + all: [], + default_value: 0, + values: [], + approved: {}, + }, + }, + }; + element._mergeable = true; + const expectedStatuses = ['Merged', 'WIP']; + assert.deepEqual(element._changeStatuses, expectedStatuses); + assert.equal(element._changeStatus, expectedStatuses.join(', ')); + flushAsynchronousOperations(); + const statusChips = dom(element.root) + .querySelectorAll('gr-change-status'); + assert.equal(statusChips.length, 2); + }); + + test('diff preferences open when open-diff-prefs is fired', () => { + const overlayOpenStub = sandbox.stub(element.$.fileList, + 'openDiffPrefs'); + element.$.fileListHeader.fire('open-diff-prefs'); + assert.isTrue(overlayOpenStub.called); + }); + + test('_prepareCommitMsgForLinkify', () => { + let commitMessage = 'R=test@google.com'; + let result = element._prepareCommitMsgForLinkify(commitMessage); + assert.equal(result, 'R=\u200Btest@google.com'); + + commitMessage = 'R=test@google.com\nR=test@google.com'; + result = element._prepareCommitMsgForLinkify(commitMessage); + assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com'); + + commitMessage = 'CC=test@google.com'; + result = element._prepareCommitMsgForLinkify(commitMessage); + assert.equal(result, 'CC=\u200Btest@google.com'); + }), + + test('_isSubmitEnabled', () => { + assert.isFalse(element._isSubmitEnabled({})); + assert.isFalse(element._isSubmitEnabled({submit: {}})); + assert.isTrue(element._isSubmitEnabled( + {submit: {enabled: true}})); + }); + + test('_reload is called when an approved label is removed', () => { + const vote = {_account_id: 1, name: 'bojack', value: 1}; + element._changeNum = '42'; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + owner: {email: 'abc@def'}, + revisions: { + rev2: {_number: 2, commit: {parents: []}}, + rev1: {_number: 1, commit: {parents: []}}, + rev13: {_number: 13, commit: {parents: []}}, + rev3: {_number: 3, commit: {parents: []}}, + }, + current_revision: 'rev3', + status: 'NEW', + labels: { + test: { + all: [vote], + default_value: 0, + values: [], + approved: {}, + }, + }, + }; + flushAsynchronousOperations(); + const reloadStub = sandbox.stub(element, '_reload'); + element.splice('_change.labels.test.all', 0, 1); + assert.isFalse(reloadStub.called); + element._change.labels.test.all.push(vote); + element._change.labels.test.all.push(vote); + element._change.labels.test.approved = vote; + flushAsynchronousOperations(); + element.splice('_change.labels.test.all', 0, 2); + assert.isTrue(reloadStub.called); + assert.isTrue(reloadStub.calledOnce); + }); + + test('reply button has updated count when there are drafts', () => { + const getLabel = element._computeReplyButtonLabel; + + assert.equal(getLabel(null, false), 'Reply'); + assert.equal(getLabel(null, true), 'Start review'); + + const changeRecord = {base: null}; + assert.equal(getLabel(changeRecord, false), 'Reply'); + + changeRecord.base = {}; + assert.equal(getLabel(changeRecord, false), 'Reply'); + + changeRecord.base = { + 'file1.txt': [{}], + 'file2.txt': [{}, {}], + }; + assert.equal(getLabel(changeRecord, false), 'Reply (3)'); + }); + + test('start review button when owner of WIP change', () => { + assert.equal( + element._computeReplyButtonLabel(null, true), + 'Start review'); + }); + + test('comment events properly update diff drafts', () => { + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 2, + }; + const draft = { + __draft: true, + id: 'id1', + path: '/foo/bar.txt', + text: 'hello', + }; + element._handleCommentSave({detail: {comment: draft}}); + draft.patch_set = 2; + assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]}); + draft.patch_set = null; + draft.text = 'hello, there'; + element._handleCommentSave({detail: {comment: draft}}); + draft.patch_set = 2; + assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft]}); + const draft2 = { + __draft: true, + id: 'id2', + path: '/foo/bar.txt', + text: 'hola', + }; + element._handleCommentSave({detail: {comment: draft2}}); + draft2.patch_set = 2; + assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft, draft2]}); + draft.patch_set = null; + element._handleCommentDiscard({detail: {comment: draft}}); + draft.patch_set = 2; + assert.deepEqual(element._diffDrafts, {'/foo/bar.txt': [draft2]}); + element._handleCommentDiscard({detail: {comment: draft2}}); + assert.deepEqual(element._diffDrafts, {}); + }); + + test('change num change', () => { + element._changeNum = null; + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 2, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + labels: {}, + }; + element.viewState.changeNum = null; + element.viewState.diffMode = 'UNIFIED'; + assert.equal(element.viewState.numFilesShown, 200); + assert.equal(element._numFilesShown, 200); + element._numFilesShown = 150; + flushAsynchronousOperations(); + assert.equal(element.viewState.diffMode, 'UNIFIED'); + assert.equal(element.viewState.numFilesShown, 150); + + element._changeNum = '1'; + element.params = {changeNum: '1'}; + element._change.newProp = '1'; + flushAsynchronousOperations(); + assert.equal(element.viewState.diffMode, 'UNIFIED'); + assert.equal(element.viewState.changeNum, '1'); + + element._changeNum = '2'; + element.params = {changeNum: '2'}; + element._change.newProp = '2'; + flushAsynchronousOperations(); + assert.equal(element.viewState.diffMode, 'UNIFIED'); + assert.equal(element.viewState.changeNum, '2'); + assert.equal(element.viewState.numFilesShown, 200); + assert.equal(element._numFilesShown, 200); + }); + + test('_setDiffViewMode is called with reset when new change is loaded', + () => { + sandbox.stub(element, '_setDiffViewMode'); + element.viewState = {changeNum: 1}; + element._changeNum = 2; + element._resetFileListViewState(); + assert.isTrue( + element._setDiffViewMode.lastCall.calledWithExactly(true)); + }); + + test('diffViewMode is propagated from file list header', () => { + element.viewState = {diffMode: 'UNIFIED'}; + element.$.fileListHeader.diffViewMode = 'SIDE_BY_SIDE'; + assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE'); + }); + + test('diffMode defaults to side by side without preferences', done => { + sandbox.stub(element.$.restAPI, 'getPreferences').returns( + Promise.resolve({})); + // No user prefs or diff view mode set. + + element._setDiffViewMode().then(() => { + assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE'); + done(); + }); + }); + + test('diffMode defaults to preference when not already set', done => { + sandbox.stub(element.$.restAPI, 'getPreferences').returns( + Promise.resolve({default_diff_view: 'UNIFIED'})); + + element._setDiffViewMode().then(() => { + assert.equal(element.viewState.diffMode, 'UNIFIED'); + done(); + }); + }); + + test('existing diffMode overrides preference', done => { + element.viewState.diffMode = 'SIDE_BY_SIDE'; + sandbox.stub(element.$.restAPI, 'getPreferences').returns( + Promise.resolve({default_diff_view: 'UNIFIED'})); + element._setDiffViewMode().then(() => { + assert.equal(element.viewState.diffMode, 'SIDE_BY_SIDE'); + done(); + }); + }); + + test('don’t reload entire page when patchRange changes', () => { + const reloadStub = sandbox.stub(element, '_reload', + () => Promise.resolve()); + const reloadPatchDependentStub = sandbox.stub(element, + '_reloadPatchNumDependentResources', + () => Promise.resolve()); + const relatedClearSpy = sandbox.spy(element.$.relatedChanges, 'clear'); + const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs'); + + const value = { + view: Gerrit.Nav.View.CHANGE, + patchNum: '1', + }; + element._paramsChanged(value); + assert.isTrue(reloadStub.calledOnce); + assert.isTrue(relatedClearSpy.calledOnce); + + element._initialLoadComplete = true; + + value.basePatchNum = '1'; + value.patchNum = '2'; + element._paramsChanged(value); + assert.isFalse(reloadStub.calledTwice); + assert.isTrue(reloadPatchDependentStub.calledOnce); + assert.isTrue(relatedClearSpy.calledOnce); + assert.isTrue(collapseStub.calledTwice); + }); + + test('reload entire page when patchRange doesnt change', () => { + const reloadStub = sandbox.stub(element, '_reload', + () => Promise.resolve()); + const collapseStub = sandbox.stub(element.$.fileList, 'collapseAllDiffs'); + const value = { + view: Gerrit.Nav.View.CHANGE, + }; + element._paramsChanged(value); + assert.isTrue(reloadStub.calledOnce); + element._initialLoadComplete = true; + element._paramsChanged(value); + assert.isTrue(reloadStub.calledTwice); + assert.isTrue(collapseStub.calledTwice); + }); + + test('related changes are updated and new patch selected after rebase', + done => { + element._changeNum = '42'; + sandbox.stub(element, 'computeLatestPatchNum', () => 1); + sandbox.stub(element, '_reload', + () => Promise.resolve()); + const e = {detail: {action: 'rebase'}}; + element._handleReloadChange(e).then(() => { + assert.isTrue(navigateToChangeStub.lastCall.calledWithExactly( + element._change)); + done(); + }); + }); + + test('related changes are not updated after other action', done => { + sandbox.stub(element, '_reload', () => Promise.resolve()); + sandbox.stub(element.$.relatedChanges, 'reload'); + const e = {detail: {action: 'abandon'}}; + element._handleReloadChange(e).then(() => { + assert.isFalse(navigateToChangeStub.called); + done(); + }); + }); + + test('_computeMergedCommitInfo', () => { + const dummyRevs = { + 1: {commit: {commit: 1}}, + 2: {commit: {}}, + }; + assert.deepEqual(element._computeMergedCommitInfo(0, dummyRevs), {}); + assert.deepEqual(element._computeMergedCommitInfo(1, dummyRevs), + dummyRevs[1].commit); + + // Regression test for issue 5337. + const commit = element._computeMergedCommitInfo(2, dummyRevs); + assert.notDeepEqual(commit, dummyRevs[2]); + assert.deepEqual(commit, {commit: 2}); + }); + + test('_computeCopyTextForTitle', () => { + const change = { + _number: 123, + subject: 'test subject', + revisions: { + rev1: {_number: 1}, + rev3: {_number: 3}, + }, + current_revision: 'rev3', + }; + sandbox.stub(Gerrit.Nav, 'getUrlForChange') + .returns('/change/123'); + assert.equal( + element._computeCopyTextForTitle(change), + '123: test subject | https://localhost:8081/change/123' + ); + }); + + test('get latest revision', () => { + let change = { + revisions: { + rev1: {_number: 1}, + rev3: {_number: 3}, + }, + current_revision: 'rev3', + }; + assert.equal(element._getLatestRevisionSHA(change), 'rev3'); + change = { + revisions: { + rev1: {_number: 1}, + }, + }; + assert.equal(element._getLatestRevisionSHA(change), 'rev1'); + }); + + test('show commit message edit button', () => { + const _change = { + status: element.ChangeStatus.MERGED, + }; + assert.isTrue(element._computeHideEditCommitMessage(false, false, {})); + assert.isTrue(element._computeHideEditCommitMessage(true, true, {})); + assert.isTrue(element._computeHideEditCommitMessage(false, true, {})); + assert.isFalse(element._computeHideEditCommitMessage(true, false, {})); + assert.isTrue(element._computeHideEditCommitMessage(true, false, + _change)); + assert.isTrue(element._computeHideEditCommitMessage(true, false, {}, + true)); + assert.isFalse(element._computeHideEditCommitMessage(true, false, {}, + false)); + }); + + test('_handleCommitMessageSave trims trailing whitespace', () => { + const putStub = sandbox.stub(element.$.restAPI, 'putChangeCommitMessage') + .returns(Promise.resolve({})); + + const mockEvent = content => { return {detail: {content}}; }; + + element._handleCommitMessageSave(mockEvent('test \n test ')); + assert.equal(putStub.lastCall.args[1], 'test\n test'); + + element._handleCommitMessageSave(mockEvent(' test\ntest')); + assert.equal(putStub.lastCall.args[1], ' test\ntest'); + + element._handleCommitMessageSave(mockEvent('\n\n\n\n\n\n\n\n')); + assert.equal(putStub.lastCall.args[1], '\n\n\n\n\n\n\n\n'); + }); + + test('_computeChangeIdCommitMessageError', () => { + let commitMessage = + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483'; + let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + null); + + change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); + + commitMessage = 'This is the greatest change.'; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'missing'); + }); + + test('multiple change Ids in commit message picks last', () => { + const commitMessage = [ + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', + ].join('\n'); + let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + null); + change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); + }); + + test('does not count change Id that starts mid line', () => { + const commitMessage = [ + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282484', + 'Change-Id: I4ce18b2395bca69d7a9aa48bf4554faa56282483', + ].join(' and '); + let change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282484'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + null); + change = {change_id: 'I4ce18b2395bca69d7a9aa48bf4554faa56282483'}; + assert.equal( + element._computeChangeIdCommitMessageError(commitMessage, change), + 'mismatch'); + }); + + test('_computeTitleAttributeWarning', () => { + let changeIdCommitMessageError = 'missing'; + assert.equal( + element._computeTitleAttributeWarning(changeIdCommitMessageError), + 'No Change-Id in commit message'); + + changeIdCommitMessageError = 'mismatch'; + assert.equal( + element._computeTitleAttributeWarning(changeIdCommitMessageError), + 'Change-Id mismatch'); + }); + + test('_computeChangeIdClass', () => { + let changeIdCommitMessageError = 'missing'; + assert.equal( + element._computeChangeIdClass(changeIdCommitMessageError), ''); + + changeIdCommitMessageError = 'mismatch'; + assert.equal( + element._computeChangeIdClass(changeIdCommitMessageError), 'warning'); + }); + + test('topic is coalesced to null', done => { + sandbox.stub(element, '_changeChanged'); + sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({ + id: '123456789', + labels: {}, + current_revision: 'foo', + revisions: {foo: {commit: {}}}, + })); + + element._getChangeDetail().then(() => { + assert.isNull(element._change.topic); + done(); + }); + }); + + test('commit sha is populated from getChangeDetail', done => { + sandbox.stub(element, '_changeChanged'); + sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({ + id: '123456789', + labels: {}, + current_revision: 'foo', + revisions: {foo: {commit: {}}}, + })); + + element._getChangeDetail().then(() => { + assert.equal('foo', element._commitInfo.commit); + done(); + }); + }); + + test('edit is added to change', () => { + sandbox.stub(element, '_changeChanged'); + sandbox.stub(element.$.restAPI, 'getChangeDetail', () => Promise.resolve({ + id: '123456789', + labels: {}, + current_revision: 'foo', + revisions: {foo: {commit: {}}}, + })); + sandbox.stub(element, '_getEdit', () => Promise.resolve({ + base_patch_set_number: 1, + commit: {commit: 'bar'}, + })); + element._patchRange = {}; + + return element._getChangeDetail().then(() => { + const revs = element._change.revisions; + assert.equal(Object.keys(revs).length, 2); + assert.deepEqual(revs['foo'], {commit: {commit: 'foo'}}); + assert.deepEqual(revs['bar'], { + _number: element.EDIT_NAME, + basePatchNum: 1, + commit: {commit: 'bar'}, + fetch: undefined, + }); + }); + }); + + test('_getBasePatchNum', () => { + const _change = { + _number: 42, + revisions: { + '98da160735fb81604b4c40e93c368f380539dd0e': { + _number: 1, + commit: { + parents: [], + }, + }, + }, + }; + const _patchRange = { + basePatchNum: 'PARENT', + }; + assert.equal(element._getBasePatchNum(_change, _patchRange), 'PARENT'); + + element._prefs = { + default_base_for_merges: 'FIRST_PARENT', + }; + + const _change2 = { + _number: 42, + revisions: { + '98da160735fb81604b4c40e93c368f380539dd0e': { + _number: 1, + commit: { + parents: [ + { + commit: '6e12bdf1176eb4ab24d8491ba3b6d0704409cde8', + subject: 'test', + }, + { + commit: '22f7db4754b5d9816fc581f3d9a6c0ef8429c841', + subject: 'test3', + }, + ], + }, + }, + }, + }; + assert.equal(element._getBasePatchNum(_change2, _patchRange), -1); + + _patchRange.patchNum = 1; + assert.equal(element._getBasePatchNum(_change2, _patchRange), 'PARENT'); + }); + + test('_openReplyDialog called with `ANY` when coming from tap event', + () => { + const openStub = sandbox.stub(element, '_openReplyDialog'); + element._serverConfig = {}; + MockInteractions.tap(element.$.replyBtn); + assert(openStub.lastCall.calledWithExactly( + element.$.replyDialog.FocusTarget.ANY), + '_openReplyDialog should have been passed ANY'); + assert.equal(openStub.callCount, 1); + }); + + test('_openReplyDialog called with `BODY` when coming from message reply' + + 'event', done => { + flush(() => { + const openStub = sandbox.stub(element, '_openReplyDialog'); + element.messagesList.fire('reply', + {message: {message: 'text'}}); + assert(openStub.lastCall.calledWithExactly( + element.$.replyDialog.FocusTarget.BODY), + '_openReplyDialog should have been passed BODY'); + assert.equal(openStub.callCount, 1); + done(); + }); + }); + + test('reply dialog focus can be controlled', () => { + const FocusTarget = element.$.replyDialog.FocusTarget; + const openStub = sandbox.stub(element, '_openReplyDialog'); + + const e = {detail: {}}; + element._handleShowReplyDialog(e); + assert(openStub.lastCall.calledWithExactly(FocusTarget.REVIEWERS), + '_openReplyDialog should have been passed REVIEWERS'); + assert.equal(openStub.callCount, 1); + + e.detail.value = {ccsOnly: true}; + element._handleShowReplyDialog(e); + assert(openStub.lastCall.calledWithExactly(FocusTarget.CCS), + '_openReplyDialog should have been passed CCS'); + assert.equal(openStub.callCount, 2); + }); + + test('getUrlParameter functionality', () => { + const locationStub = sandbox.stub(element, '_getLocationSearch'); + + locationStub.returns('?test'); + assert.equal(element._getUrlParameter('test'), 'test'); + locationStub.returns('?test2=12&test=3'); + assert.equal(element._getUrlParameter('test'), 'test'); + locationStub.returns(''); + assert.isNull(element._getUrlParameter('test')); + locationStub.returns('?'); + assert.isNull(element._getUrlParameter('test')); + locationStub.returns('?test2'); + assert.isNull(element._getUrlParameter('test')); + }); + + test('revert dialog opened with revert param', done => { + sandbox.stub(element.$.restAPI, 'getLoggedIn', () => Promise.resolve(true)); + sandbox.stub(Gerrit, 'awaitPluginsLoaded', () => Promise.resolve()); + + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 2, + }; + element._change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1, commit: {parents: []}}, + rev2: {_number: 2, commit: {parents: []}}, + }, + current_revision: 'rev1', + status: element.ChangeStatus.MERGED, + labels: {}, + actions: {}, + }; + + sandbox.stub(element, '_getUrlParameter', + param => { + assert.equal(param, 'revert'); + return param; + }); + + sandbox.stub(element.$.actions, 'showRevertDialog', + done); + + element._maybeShowRevertDialog(); + assert.isTrue(Gerrit.awaitPluginsLoaded.called); + }); + + suite('scroll related tests', () => { + test('document scrolling calls function to set scroll height', done => { + const originalHeight = document.body.scrollHeight; + const scrollStub = sandbox.stub(element, '_handleScroll', + () => { + assert.isTrue(scrollStub.called); + document.body.style.height = originalHeight + 'px'; + scrollStub.restore(); + done(); + }); + document.body.style.height = '10000px'; + element._handleScroll(); + }); + + test('scrollTop is set correctly', () => { + element.viewState = {scrollTop: TEST_SCROLL_TOP_PX}; + + sandbox.stub(element, '_reload', () => { + // When element is reloaded, ensure that the history + // state has the scrollTop set earlier. This will then + // be reset. + assert.isTrue(element.viewState.scrollTop == TEST_SCROLL_TOP_PX); + return Promise.resolve({}); + }); + + // simulate reloading component, which is done when route + // changes to match a regex of change view type. + element._paramsChanged({view: Gerrit.Nav.View.CHANGE}); + }); + + test('scrollTop is reset when new change is loaded', () => { + element._resetFileListViewState(); + assert.equal(element.viewState.scrollTop, 0); + }); + }); + + suite('reply dialog tests', () => { + setup(() => { + sandbox.stub(element.$.replyDialog, '_draftChanged'); + sandbox.stub(element.$.replyDialog, 'fetchChangeUpdates', + () => Promise.resolve({isLatest: true})); + element._change = {labels: {}}; + }); + + test('reply from comment adds quote text', () => { + const e = {detail: {message: {message: 'quote text'}}}; + element._handleMessageReply(e); + assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); + }); + + test('reply from comment replaces quote text', () => { + element.$.replyDialog.draft = '> old quote text\n\n some draft text'; + element.$.replyDialog.quote = '> old quote text\n\n'; + const e = {detail: {message: {message: 'quote text'}}}; + element._handleMessageReply(e); + assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); + }); + + test('reply from same comment preserves quote text', () => { + element.$.replyDialog.draft = '> quote text\n\n some draft text'; + element.$.replyDialog.quote = '> quote text\n\n'; + const e = {detail: {message: {message: 'quote text'}}}; + element._handleMessageReply(e); + assert.equal(element.$.replyDialog.draft, + '> quote text\n\n some draft text'); + assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); + }); + + test('reply from top of page contains previous draft', () => { + const div = document.createElement('div'); + element.$.replyDialog.draft = '> quote text\n\n some draft text'; + element.$.replyDialog.quote = '> quote text\n\n'; + const e = {target: div, preventDefault: sandbox.spy()}; + element._handleReplyTap(e); + assert.equal(element.$.replyDialog.draft, + '> quote text\n\n some draft text'); + assert.equal(element.$.replyDialog.quote, '> quote text\n\n'); + }); + }); + + test('reply button is disabled until server config is loaded', () => { + assert.isTrue(element._replyDisabled); + element._serverConfig = {}; + assert.isFalse(element._replyDisabled); + }); + + suite('commit message expand/collapse', () => { + setup(() => { + sandbox.stub(element, 'fetchChangeUpdates', + () => Promise.resolve({isLatest: false})); + }); + + test('commitCollapseToggle hidden for short commit message', () => { + element._latestCommitMessage = ''; + assert.isTrue(element.$.commitCollapseToggle.hasAttribute('hidden')); + }); + + test('commitCollapseToggle shown for long commit message', () => { + element._latestCommitMessage = _.times(31, String).join('\n'); + assert.isFalse(element.$.commitCollapseToggle.hasAttribute('hidden')); + }); + + test('commitCollapseToggle functions', () => { + element._latestCommitMessage = _.times(35, String).join('\n'); + assert.isTrue(element._commitCollapsed); + assert.isTrue(element._commitCollapsible); + assert.isTrue( + element.$.commitMessageEditor.hasAttribute('collapsed')); + MockInteractions.tap(element.$.commitCollapseToggleButton); + assert.isFalse(element._commitCollapsed); + assert.isTrue(element._commitCollapsible); + assert.isFalse( + element.$.commitMessageEditor.hasAttribute('collapsed')); + }); + }); + + suite('related changes expand/collapse', () => { + let updateHeightSpy; + setup(() => { + updateHeightSpy = sandbox.spy(element, '_updateRelatedChangeMaxHeight'); + }); + + test('relatedChangesToggle shown height greater than changeInfo height', + () => { + assert.isFalse(element.$.relatedChangesToggle.classList + .contains('showToggle')); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getScrollHeight', () => 60); + sandbox.stub(element, '_getLineHeight', () => 5); + sandbox.stub(window, 'matchMedia', () => { return {matches: true}; }); + element.$.relatedChanges.dispatchEvent( + new CustomEvent('new-section-loaded')); + assert.isTrue(element.$.relatedChangesToggle.classList + .contains('showToggle')); + assert.equal(updateHeightSpy.callCount, 1); + }); + + test('relatedChangesToggle hidden height less than changeInfo height', + () => { + assert.isFalse(element.$.relatedChangesToggle.classList + .contains('showToggle')); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getScrollHeight', () => 40); + sandbox.stub(element, '_getLineHeight', () => 5); + sandbox.stub(window, 'matchMedia', () => { return {matches: true}; }); + element.$.relatedChanges.dispatchEvent( + new CustomEvent('new-section-loaded')); + assert.isFalse(element.$.relatedChangesToggle.classList + .contains('showToggle')); + assert.equal(updateHeightSpy.callCount, 1); + }); + + test('relatedChangesToggle functions', () => { + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(window, 'matchMedia', () => { return {matches: false}; }); + element._relatedChangesLoading = false; + assert.isTrue(element._relatedChangesCollapsed); + assert.isTrue( + element.$.relatedChanges.classList.contains('collapsed')); + MockInteractions.tap(element.$.relatedChangesToggleButton); + assert.isFalse(element._relatedChangesCollapsed); + assert.isFalse( + element.$.relatedChanges.classList.contains('collapsed')); + }); + + test('_updateRelatedChangeMaxHeight without commit toggle', () => { + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => { return {matches: false}; }); + + // 50 (existing height) - 30 (extra height) = 20 (adjusted height). + // 20 (max existing height) % 12 (line height) = 6 (remainder). + // 20 (adjusted height) - 8 (remainder) = 12 (max height to set). + + element._updateRelatedChangeMaxHeight(); + assert.equal(getCustomCssValue('--relation-chain-max-height'), + '12px'); + assert.equal(getCustomCssValue('--related-change-btn-top-padding'), + ''); + }); + + test('_updateRelatedChangeMaxHeight with commit toggle', () => { + element._latestCommitMessage = _.times(31, String).join('\n'); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => { return {matches: false}; }); + + // 50 (existing height) % 12 (line height) = 2 (remainder). + // 50 (existing height) - 2 (remainder) = 48 (max height to set). + + element._updateRelatedChangeMaxHeight(); + assert.equal(getCustomCssValue('--relation-chain-max-height'), + '48px'); + assert.equal(getCustomCssValue('--related-change-btn-top-padding'), + '2px'); + }); + + test('_updateRelatedChangeMaxHeight in small screen mode', () => { + element._latestCommitMessage = _.times(31, String).join('\n'); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => { return {matches: true}; }); + + element._updateRelatedChangeMaxHeight(); + + // 400 (new height) % 12 (line height) = 4 (remainder). + // 400 (new height) - 4 (remainder) = 396. + + assert.equal(getCustomCssValue('--relation-chain-max-height'), + '396px'); + }); + + test('_updateRelatedChangeMaxHeight in medium screen mode', () => { + element._latestCommitMessage = _.times(31, String).join('\n'); + sandbox.stub(element, '_getOffsetHeight', () => 50); + sandbox.stub(element, '_getLineHeight', () => 12); + sandbox.stub(window, 'matchMedia', () => { + if (window.matchMedia.lastCall.args[0] === '(max-width: 75em)') { + return {matches: true}; + } else { + return {matches: false}; + } + }); + + // 100 (new height) % 12 (line height) = 4 (remainder). + // 100 (new height) - 4 (remainder) = 96. + element._updateRelatedChangeMaxHeight(); + assert.equal(getCustomCssValue('--relation-chain-max-height'), + '96px'); + }); + + suite('update checks', () => { + setup(() => { + sandbox.spy(element, '_startUpdateCheckTimer'); + sandbox.stub(element, 'async', f => { + // Only fire the async callback one time. + if (element.async.callCount > 1) { return; } + f.call(element); + }); + }); + + test('_startUpdateCheckTimer negative delay', () => { + sandbox.stub(element, 'fetchChangeUpdates'); + + element._serverConfig = {change: {update_delay: -1}}; + + assert.isTrue(element._startUpdateCheckTimer.called); + assert.isFalse(element.fetchChangeUpdates.called); + }); + + test('_startUpdateCheckTimer up-to-date', () => { + sandbox.stub(element, 'fetchChangeUpdates', + () => Promise.resolve({isLatest: true})); + + element._serverConfig = {change: {update_delay: 12345}}; + + assert.isTrue(element._startUpdateCheckTimer.called); + assert.isTrue(element.fetchChangeUpdates.called); + assert.equal(element.async.lastCall.args[1], 12345 * 1000); + }); + + test('_startUpdateCheckTimer out-of-date shows an alert', done => { + sandbox.stub(element, 'fetchChangeUpdates', + () => Promise.resolve({isLatest: false})); + element.addEventListener('show-alert', e => { + assert.equal(e.detail.message, + 'A newer patch set has been uploaded'); + done(); + }); + element._serverConfig = {change: {update_delay: 12345}}; + }); + + test('_startUpdateCheckTimer new status shows an alert', done => { + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({ + isLatest: true, + newStatus: element.ChangeStatus.MERGED, + })); + element.addEventListener('show-alert', e => { + assert.equal(e.detail.message, 'This change has been merged'); + done(); + }); + element._serverConfig = {change: {update_delay: 12345}}; + }); + + test('_startUpdateCheckTimer new messages shows an alert', done => { + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({ + isLatest: true, + newMessages: true, + })); + element.addEventListener('show-alert', e => { + assert.equal(e.detail.message, + 'There are new messages on this change'); + done(); + }); + element._serverConfig = {change: {update_delay: 12345}}; + }); + }); + + test('canStartReview computation', () => { + const change1 = {}; + const change2 = { + actions: { + ready: { + enabled: true, + }, + }, + }; + const change3 = { + actions: { + ready: { + label: 'Ready for Review', + }, + }, + }; + assert.isFalse(element._computeCanStartReview(change1)); + assert.isTrue(element._computeCanStartReview(change2)); + assert.isFalse(element._computeCanStartReview(change3)); + }); + }); + + test('header class computation', () => { + assert.equal(element._computeHeaderClass(), 'header'); + assert.equal(element._computeHeaderClass(true), 'header editMode'); + }); + + test('_maybeScrollToMessage', done => { + flush(() => { + const scrollStub = sandbox.stub(element.messagesList, + 'scrollToMessage'); + + element._maybeScrollToMessage(''); + assert.isFalse(scrollStub.called); + element._maybeScrollToMessage('message'); + assert.isFalse(scrollStub.called); + element._maybeScrollToMessage('#message-TEST'); + assert.isTrue(scrollStub.called); + assert.equal(scrollStub.lastCall.args[0], 'TEST'); + done(); + }); + }); + + test('topic update reloads related changes', () => { + sandbox.stub(element.$.relatedChanges, 'reload'); + element.dispatchEvent(new CustomEvent('topic-changed')); + assert.isTrue(element.$.relatedChanges.reload.calledOnce); + }); + + test('_computeEditMode', () => { + const callCompute = (range, params) => + element._computeEditMode({base: range}, {base: params}); + assert.isFalse(callCompute({}, {})); + assert.isTrue(callCompute({}, {edit: true})); + assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1}, {})); + assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1}, {})); + assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'}, {})); + }); + + test('_processEdit', () => { + element._patchRange = {}; + const change = { + current_revision: 'foo', + revisions: {foo: {commit: {}, actions: {cherrypick: {enabled: true}}}}, + }; + let mockChange; + + // With no edit, mockChange should be unmodified. + element._processEdit(mockChange = _.cloneDeep(change), null); + assert.deepEqual(mockChange, change); + + // When edit is not based on the latest PS, current_revision should be + // unmodified. + const edit = { + base_patch_set_number: 1, + commit: {commit: 'bar'}, + fetch: true, + }; + element._processEdit(mockChange = _.cloneDeep(change), edit); + assert.notDeepEqual(mockChange, change); + assert.equal(mockChange.revisions.bar._number, element.EDIT_NAME); + assert.equal(mockChange.current_revision, change.current_revision); + assert.deepEqual(mockChange.revisions.bar.commit, {commit: 'bar'}); + assert.notOk(mockChange.revisions.bar.actions); + + edit.base_revision = 'foo'; + element._processEdit(mockChange = _.cloneDeep(change), edit); + assert.notDeepEqual(mockChange, change); + assert.equal(mockChange.current_revision, 'bar'); + assert.deepEqual(mockChange.revisions.bar.actions, + mockChange.revisions.foo.actions); + + // If _patchRange.patchNum is defined, do not load edit. + element._patchRange.patchNum = 'baz'; + change.current_revision = 'baz'; + element._processEdit(mockChange = _.cloneDeep(change), edit); + assert.equal(element._patchRange.patchNum, 'baz'); + assert.notOk(mockChange.revisions.bar.actions); + }); + + test('file-action-tap handling', () => { + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + const fileList = element.$.fileList; + const Actions = GrEditConstants.Actions; + const controls = element.$.fileListHeader.$.editControls; + sandbox.stub(controls, 'openDeleteDialog'); + sandbox.stub(controls, 'openRenameDialog'); + sandbox.stub(controls, 'openRestoreDialog'); + sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'); + sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); + + // Delete + fileList.dispatchEvent(new CustomEvent('file-action-tap', { + detail: {action: Actions.DELETE.id, path: 'foo'}, + bubbles: true, + composed: true, + })); + flushAsynchronousOperations(); + + assert.isTrue(controls.openDeleteDialog.called); + assert.equal(controls.openDeleteDialog.lastCall.args[0], 'foo'); + + // Restore + fileList.dispatchEvent(new CustomEvent('file-action-tap', { + detail: {action: Actions.RESTORE.id, path: 'foo'}, + bubbles: true, + composed: true, + })); + flushAsynchronousOperations(); + + assert.isTrue(controls.openRestoreDialog.called); + assert.equal(controls.openRestoreDialog.lastCall.args[0], 'foo'); + + // Rename + fileList.dispatchEvent(new CustomEvent('file-action-tap', { + detail: {action: Actions.RENAME.id, path: 'foo'}, + bubbles: true, + composed: true, + })); + flushAsynchronousOperations(); + + assert.isTrue(controls.openRenameDialog.called); + assert.equal(controls.openRenameDialog.lastCall.args[0], 'foo'); + + // Open + fileList.dispatchEvent(new CustomEvent('file-action-tap', { + detail: {action: Actions.OPEN.id, path: 'foo'}, + bubbles: true, + composed: true, + })); + flushAsynchronousOperations(); + + assert.isTrue(Gerrit.Nav.getEditUrlForDiff.called); + assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[1], 'foo'); + assert.equal(Gerrit.Nav.getEditUrlForDiff.lastCall.args[2], '1'); + assert.isTrue(Gerrit.Nav.navigateToRelativeUrl.called); + }); + + test('_selectedRevision updates when patchNum is changed', () => { + const revision1 = {_number: 1, commit: {parents: []}}; + const revision2 = {_number: 2, commit: {parents: []}}; + sandbox.stub(element.$.restAPI, 'getChangeDetail').returns( + Promise.resolve({ + revisions: { + aaa: revision1, + bbb: revision2, + }, + labels: {}, + actions: {}, + current_revision: 'bbb', + change_id: 'loremipsumdolorsitamet', + })); + sandbox.stub(element, '_getEdit').returns(Promise.resolve()); + sandbox.stub(element, '_getPreferences').returns(Promise.resolve({})); + element._patchRange = {patchNum: '2'}; + return element._getChangeDetail().then(() => { + assert.strictEqual(element._selectedRevision, revision2); + + element.set('_patchRange.patchNum', '1'); + assert.strictEqual(element._selectedRevision, revision1); + }); + }); + + test('_selectedRevision is assigned when patchNum is edit', () => { + const revision1 = {_number: 1, commit: {parents: []}}; + const revision2 = {_number: 2, commit: {parents: []}}; + const revision3 = {_number: 'edit', commit: {parents: []}}; + sandbox.stub(element.$.restAPI, 'getChangeDetail').returns( + Promise.resolve({ + revisions: { + aaa: revision1, + bbb: revision2, + ccc: revision3, + }, + labels: {}, + actions: {}, + current_revision: 'ccc', + change_id: 'loremipsumdolorsitamet', + })); + sandbox.stub(element, '_getEdit').returns(Promise.resolve()); + sandbox.stub(element, '_getPreferences').returns(Promise.resolve({})); + element._patchRange = {patchNum: 'edit'}; + return element._getChangeDetail().then(() => { + assert.strictEqual(element._selectedRevision, revision3); + }); + }); + + test('_sendShowChangeEvent', () => { + element._change = {labels: {}}; + element._patchRange = {patchNum: 4}; + element._mergeable = true; + const showStub = sandbox.stub(element.$.jsAPI, 'handleEvent'); + element._sendShowChangeEvent(); + assert.isTrue(showStub.calledOnce); + assert.equal( + showStub.lastCall.args[0], element.$.jsAPI.EventType.SHOW_CHANGE); + assert.deepEqual(showStub.lastCall.args[1], { + change: {labels: {}}, + patchNum: 4, + info: {mergeable: true}, + }); + }); + + suite('_handleEditTap', () => { + let fireEdit; + + setup(() => { + fireEdit = () => { + element.$.actions.dispatchEvent(new CustomEvent('edit-tap')); + }; + navigateToChangeStub.restore(); + + element._change = {revisions: {rev1: {_number: 1}}}; + }); + + test('edit exists in revisions', done => { + sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { + assert.equal(args.length, 2); + assert.equal(args[1], element.EDIT_NAME); // patchNum + done(); + }); + + element.set('_change.revisions.rev2', {_number: element.EDIT_NAME}); + flushAsynchronousOperations(); + + fireEdit(); + }); + + test('no edit exists in revisions, non-latest patchset', done => { + sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { + assert.equal(args.length, 4); + assert.equal(args[1], 1); // patchNum + assert.equal(args[3], true); // opt_isEdit + done(); + }); + + element.set('_change.revisions.rev2', {_number: 2}); + element._patchRange = {patchNum: 1}; + flushAsynchronousOperations(); + + fireEdit(); + }); + + test('no edit exists in revisions, latest patchset', done => { + sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { + assert.equal(args.length, 4); + // No patch should be specified when patchNum == latest. + assert.isNotOk(args[1]); // patchNum + assert.equal(args[3], true); // opt_isEdit + done(); + }); + + element.set('_change.revisions.rev2', {_number: 2}); + element._patchRange = {patchNum: 2}; + flushAsynchronousOperations(); + + fireEdit(); + }); + }); + + test('_handleStopEditTap', done => { + sandbox.stub(element.$.metadata, '_computeLabelNames'); + navigateToChangeStub.restore(); + sandbox.stub(element, 'computeLatestPatchNum').returns(1); + sandbox.stub(Gerrit.Nav, 'navigateToChange', (...args) => { + assert.equal(args.length, 2); + assert.equal(args[1], 1); // patchNum + done(); + }); + + element._patchRange = {patchNum: 1}; + element.$.actions.dispatchEvent(new CustomEvent('stop-edit-tap', + {bubbles: false})); + }); + + suite('plugin endpoints', () => { + test('endpoint params', done => { + element._change = {labels: {}}; + element._selectedRevision = {}; + let hookEl; + let plugin; + Gerrit.install( + p => { + plugin = p; + plugin.hook('change-view-integration').getLastAttached() + .then( + el => hookEl = el); + }, + '0.1', + 'http://some/plugins/url.html'); + flush(() => { + assert.strictEqual(hookEl.plugin, plugin); + assert.strictEqual(hookEl.change, element._change); + assert.strictEqual(hookEl.revision, element._selectedRevision); + done(); + }); + }); + }); + + suite('_getMergeability', () => { + let getMergeableStub; + + setup(() => { + element._change = {labels: {}}; + getMergeableStub = sandbox.stub(element.$.restAPI, 'getMergeable') + .returns(Promise.resolve({mergeable: true})); + }); + + test('merged change', () => { + element._mergeable = null; + element._change.status = element.ChangeStatus.MERGED; + return element._getMergeability().then(() => { + assert.isFalse(element._mergeable); + assert.isFalse(getMergeableStub.called); + }); + }); + + test('abandoned change', () => { + element._mergeable = null; + element._change.status = element.ChangeStatus.ABANDONED; + return element._getMergeability().then(() => { + assert.isFalse(element._mergeable); + assert.isFalse(getMergeableStub.called); + }); + }); + + test('open change', () => { + element._mergeable = null; + return element._getMergeability().then(() => { + assert.isTrue(element._mergeable); + assert.isTrue(getMergeableStub.called); + }); + }); + }); + + test('_paramsChanged sets in projectLookup', () => { + sandbox.stub(element.$.relatedChanges, 'reload'); + sandbox.stub(element, '_reload').returns(Promise.resolve()); + const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + element._paramsChanged({ + view: Gerrit.Nav.View.CHANGE, + changeNum: 101, + project: 'test-project', + }); + assert.isTrue(setStub.calledOnce); + assert.isTrue(setStub.calledWith(101, 'test-project')); + }); + + test('_handleToggleStar called when star is tapped', () => { + element._change = { + owner: {_account_id: 1}, + starred: false, + }; + element._loggedIn = true; + const stub = sandbox.stub(element, '_handleToggleStar'); + flushAsynchronousOperations(); + + MockInteractions.tap(element.$.changeStar.shadowRoot + .querySelector('button')); + assert.isTrue(stub.called); + }); + + suite('gr-reporting tests', () => { + setup(() => { + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: 1, + }; + sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve()); + sandbox.stub(element, '_getProjectConfig').returns(Promise.resolve()); + sandbox.stub(element, '_reloadComments').returns(Promise.resolve()); + sandbox.stub(element, '_getMergeability').returns(Promise.resolve()); + sandbox.stub(element, '_getLatestCommitMessage') + .returns(Promise.resolve()); + }); + + test('don\'t report changedDisplayed on reply', done => { + const changeDisplayStub = + sandbox.stub(element.$.reporting, 'changeDisplayed'); + const changeFullyLoadedStub = + sandbox.stub(element.$.reporting, 'changeFullyLoaded'); + element._handleReplySent(); + flush(() => { + assert.isFalse(changeDisplayStub.called); + assert.isFalse(changeFullyLoadedStub.called); + done(); + }); + }); + + test('report changedDisplayed on _paramsChanged', done => { + const changeDisplayStub = + sandbox.stub(element.$.reporting, 'changeDisplayed'); + const changeFullyLoadedStub = + sandbox.stub(element.$.reporting, 'changeFullyLoaded'); + element._paramsChanged({ + view: Gerrit.Nav.View.CHANGE, + changeNum: 101, + project: 'test-project', + }); + flush(() => { + assert.isTrue(changeDisplayStub.called); + assert.isTrue(changeFullyLoadedStub.called); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js index 16c55cd..2649733 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,79 +14,100 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +/* + The custom CSS property `--gr-formatted-text-prose-max-width` controls the max + width of formatted text blocks that are not code. +*/ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.PathListMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrCommentList extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.PathListBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-comment-list'; } +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-formatted-text/gr-formatted-text.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-comment-list_html.js'; - static get properties() { - return { - changeNum: Number, - comments: Object, - patchNum: Number, - projectName: String, - /** @type {?} */ - projectConfig: Object, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrCommentList extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.PathListBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _computeFilesFromComments(comments) { - const arr = Object.keys(comments || {}); - return arr.sort(this.specialFilePathCompare); - } + static get is() { return 'gr-comment-list'; } - _isOnParent(comment) { - return comment.side === 'PARENT'; - } - - _computeDiffURL(filePath, changeNum, allComments) { - if ([filePath, changeNum, allComments].some(arg => arg === undefined)) { - return; - } - const fileComments = this._computeCommentsForFile(allComments, filePath); - // This can happen for files that don't exist anymore in the current ps. - if (fileComments.length === 0) return; - return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName, - filePath, fileComments[0].patch_set); - } - - _computeDiffLineURL(filePath, changeNum, patchNum, comment) { - const basePatchNum = comment.hasOwnProperty('parent') ? - -comment.parent : null; - return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName, - filePath, patchNum, basePatchNum, comment.line, - this._isOnParent(comment)); - } - - _computeCommentsForFile(comments, filePath) { - // Changes are not picked up by the dom-repeat due to the array instance - // identity not changing even when it has elements added/removed from it. - return (comments[filePath] || []).slice(); - } - - _computePatchDisplayName(comment) { - if (this._isOnParent(comment)) { - return 'Base, '; - } - if (comment.patch_set != this.patchNum) { - return `PS${comment.patch_set}, `; - } - return ''; - } + static get properties() { + return { + changeNum: Number, + comments: Object, + patchNum: Number, + projectName: String, + /** @type {?} */ + projectConfig: Object, + }; } - customElements.define(GrCommentList.is, GrCommentList); -})(); + _computeFilesFromComments(comments) { + const arr = Object.keys(comments || {}); + return arr.sort(this.specialFilePathCompare); + } + + _isOnParent(comment) { + return comment.side === 'PARENT'; + } + + _computeDiffURL(filePath, changeNum, allComments) { + if ([filePath, changeNum, allComments].some(arg => arg === undefined)) { + return; + } + const fileComments = this._computeCommentsForFile(allComments, filePath); + // This can happen for files that don't exist anymore in the current ps. + if (fileComments.length === 0) return; + return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName, + filePath, fileComments[0].patch_set); + } + + _computeDiffLineURL(filePath, changeNum, patchNum, comment) { + const basePatchNum = comment.hasOwnProperty('parent') ? + -comment.parent : null; + return Gerrit.Nav.getUrlForDiffById(changeNum, this.projectName, + filePath, patchNum, basePatchNum, comment.line, + this._isOnParent(comment)); + } + + _computeCommentsForFile(comments, filePath) { + // Changes are not picked up by the dom-repeat due to the array instance + // identity not changing even when it has elements added/removed from it. + return (comments[filePath] || []).slice(); + } + + _computePatchDisplayName(comment) { + if (this._isOnParent(comment)) { + return 'Base, '; + } + if (comment.patch_set != this.patchNum) { + return `PS${comment.patch_set}, `; + } + return ''; + } +} + +customElements.define(GrCommentList.is, GrCommentList);
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js index 52c0c89..d50ba6a 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_html.js
@@ -1,35 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<!-- - The custom CSS property `--gr-formatted-text-prose-max-width` controls the max - width of formatted text blocks that are not code. ---> - -<dom-module id="gr-comment-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -64,27 +51,19 @@ </style> <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file"> <div class="file"><a class="fileLink" href="[[_computeDiffURL(file, changeNum, comments)]]">[[computeDisplayPath(file)]]</a></div> - <template is="dom-repeat" - items="[[_computeCommentsForFile(comments, file)]]" as="comment"> + <template is="dom-repeat" items="[[_computeCommentsForFile(comments, file)]]" as="comment"> <div class="container"> - <a class="lineNum" - href$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"> - <span hidden$="[[!comment.line]]"> + <a class="lineNum" href\$="[[_computeDiffLineURL(file, changeNum, comment.patch_set, comment)]]"> + <span hidden\$="[[!comment.line]]"> <span>[[_computePatchDisplayName(comment)]]</span> Line <span>[[comment.line]]</span> </span> - <span hidden$="[[comment.line]]"> + <span hidden\$="[[comment.line]]"> File comment: </span> </a> - <gr-formatted-text - class="message" - no-trailing-margin - content="[[comment.message]]" - config="[[projectConfig.commentlinks]]"></gr-formatted-text> + <gr-formatted-text class="message" no-trailing-margin="" content="[[comment.message]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text> </div> </template> </template> - </template> - <script src="gr-comment-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html index a91ec0e..5e8c3ab 100644 --- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-comment-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-comment-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-comment-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,98 +40,101 @@ </template> </test-fixture> -<script> - suite('gr-comment-list tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-comment-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-comment-list tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x); - }); - - teardown(() => { sandbox.restore(); }); - - test('_computeFilesFromComments w/ special file path sorting', () => { - const comments = { - 'file_b.html': [], - 'file_c.css': [], - 'file_a.js': [], - 'test.cc': [], - 'test.h': [], - }; - const expected = [ - 'file_a.js', - 'file_b.html', - 'file_c.css', - 'test.h', - 'test.cc', - ]; - const actual = element._computeFilesFromComments(comments); - assert.deepEqual(actual, expected); - - assert.deepEqual(element._computeFilesFromComments(null), []); - }); - - test('_computePatchDisplayName', () => { - const comment = {line: 123, side: 'REVISION', patch_set: 10}; - - element.patchNum = 10; - assert.equal(element._computePatchDisplayName(comment), ''); - - element.patchNum = 9; - assert.equal(element._computePatchDisplayName(comment), 'PS10, '); - - comment.side = 'PARENT'; - assert.equal(element._computePatchDisplayName(comment), 'Base, '); - }); - - test('config commentlinks propagate to formatted text', () => { - element.comments = { - 'test.h': [{ - author: {name: 'foo'}, - patch_set: 4, - line: 10, - updated: '2017-10-30 20:48:40.000000000', - message: 'Ideadbeefdeadbeef', - unresolved: true, - }], - }; - element.projectConfig = { - commentlinks: {foo: {link: '#/q/$1', match: '(I[0-9a-f]{8,40})'}}, - }; - flushAsynchronousOperations(); - const formattedText = Polymer.dom(element.root).querySelector( - 'gr-formatted-text.message'); - assert.isOk(formattedText.config); - assert.deepEqual(formattedText.config, - element.projectConfig.commentlinks); - }); - - test('_computeDiffLineURL', () => { - const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); - element.projectName = 'proj'; - element.changeNum = 123; - - const comment = {line: 456}; - element._computeDiffLineURL('foo.cc', 123, 4, comment); - assert.isTrue(getUrlStub.calledOnce); - assert.deepEqual(getUrlStub.lastCall.args, - [123, 'proj', 'foo.cc', 4, null, 456, false]); - - comment.side = 'PARENT'; - element._computeDiffLineURL('foo.cc', 123, 4, comment); - assert.isTrue(getUrlStub.calledTwice); - assert.deepEqual(getUrlStub.lastCall.args, - [123, 'proj', 'foo.cc', 4, null, 456, true]); - - comment.parent = 12; - element._computeDiffLineURL('foo.cc', 123, 4, comment); - assert.isTrue(getUrlStub.calledThrice); - assert.deepEqual(getUrlStub.lastCall.args, - [123, 'proj', 'foo.cc', 4, -12, 456, true]); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x); }); + + teardown(() => { sandbox.restore(); }); + + test('_computeFilesFromComments w/ special file path sorting', () => { + const comments = { + 'file_b.html': [], + 'file_c.css': [], + 'file_a.js': [], + 'test.cc': [], + 'test.h': [], + }; + const expected = [ + 'file_a.js', + 'file_b.html', + 'file_c.css', + 'test.h', + 'test.cc', + ]; + const actual = element._computeFilesFromComments(comments); + assert.deepEqual(actual, expected); + + assert.deepEqual(element._computeFilesFromComments(null), []); + }); + + test('_computePatchDisplayName', () => { + const comment = {line: 123, side: 'REVISION', patch_set: 10}; + + element.patchNum = 10; + assert.equal(element._computePatchDisplayName(comment), ''); + + element.patchNum = 9; + assert.equal(element._computePatchDisplayName(comment), 'PS10, '); + + comment.side = 'PARENT'; + assert.equal(element._computePatchDisplayName(comment), 'Base, '); + }); + + test('config commentlinks propagate to formatted text', () => { + element.comments = { + 'test.h': [{ + author: {name: 'foo'}, + patch_set: 4, + line: 10, + updated: '2017-10-30 20:48:40.000000000', + message: 'Ideadbeefdeadbeef', + unresolved: true, + }], + }; + element.projectConfig = { + commentlinks: {foo: {link: '#/q/$1', match: '(I[0-9a-f]{8,40})'}}, + }; + flushAsynchronousOperations(); + const formattedText = dom(element.root).querySelector( + 'gr-formatted-text.message'); + assert.isOk(formattedText.config); + assert.deepEqual(formattedText.config, + element.projectConfig.commentlinks); + }); + + test('_computeDiffLineURL', () => { + const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); + element.projectName = 'proj'; + element.changeNum = 123; + + const comment = {line: 456}; + element._computeDiffLineURL('foo.cc', 123, 4, comment); + assert.isTrue(getUrlStub.calledOnce); + assert.deepEqual(getUrlStub.lastCall.args, + [123, 'proj', 'foo.cc', 4, null, 456, false]); + + comment.side = 'PARENT'; + element._computeDiffLineURL('foo.cc', 123, 4, comment); + assert.isTrue(getUrlStub.calledTwice); + assert.deepEqual(getUrlStub.lastCall.args, + [123, 'proj', 'foo.cc', 4, null, 456, true]); + + comment.parent = 12; + element._computeDiffLineURL('foo.cc', 123, 4, comment); + assert.isTrue(getUrlStub.calledThrice); + assert.deepEqual(getUrlStub.lastCall.args, + [123, 'proj', 'foo.cc', 4, -12, 456, true]); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js index a339865..79a3692 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info.js
@@ -14,68 +14,75 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrCommitInfo extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-commit-info'; } +import '../../../styles/shared-styles.js'; +import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-commit-info_html.js'; - static get properties() { - return { - change: Object, - /** @type {?} */ - commitInfo: Object, - serverConfig: Object, - _showWebLink: { - type: Boolean, - computed: '_computeShowWebLink(change, commitInfo, serverConfig)', - }, - _webLink: { - type: String, - computed: '_computeWebLink(change, commitInfo, serverConfig)', - }, - }; - } +/** @extends Polymer.Element */ +class GrCommitInfo extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _getWeblink(change, commitInfo, config) { - return Gerrit.Nav.getPatchSetWeblink( - change.project, - commitInfo.commit, - { - weblinks: commitInfo.web_links, - config, - }); - } + static get is() { return 'gr-commit-info'; } - _computeShowWebLink(change, commitInfo, serverConfig) { - // Polymer 2: check for undefined - if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) { - return undefined; - } - - const weblink = this._getWeblink(change, commitInfo, serverConfig); - return !!weblink && !!weblink.url; - } - - _computeWebLink(change, commitInfo, serverConfig) { - // Polymer 2: check for undefined - if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) { - return undefined; - } - - const {url} = this._getWeblink(change, commitInfo, serverConfig) || {}; - return url; - } - - _computeShortHash(commitInfo) { - const {name} = - this._getWeblink(this.change, commitInfo, this.serverConfig) || {}; - return name; - } + static get properties() { + return { + change: Object, + /** @type {?} */ + commitInfo: Object, + serverConfig: Object, + _showWebLink: { + type: Boolean, + computed: '_computeShowWebLink(change, commitInfo, serverConfig)', + }, + _webLink: { + type: String, + computed: '_computeWebLink(change, commitInfo, serverConfig)', + }, + }; } - customElements.define(GrCommitInfo.is, GrCommitInfo); -})(); + _getWeblink(change, commitInfo, config) { + return Gerrit.Nav.getPatchSetWeblink( + change.project, + commitInfo.commit, + { + weblinks: commitInfo.web_links, + config, + }); + } + + _computeShowWebLink(change, commitInfo, serverConfig) { + // Polymer 2: check for undefined + if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) { + return undefined; + } + + const weblink = this._getWeblink(change, commitInfo, serverConfig); + return !!weblink && !!weblink.url; + } + + _computeWebLink(change, commitInfo, serverConfig) { + // Polymer 2: check for undefined + if ([change, commitInfo, serverConfig].some(arg => arg === undefined)) { + return undefined; + } + + const {url} = this._getWeblink(change, commitInfo, serverConfig) || {}; + return url; + } + + _computeShortHash(commitInfo) { + const {name} = + this._getWeblink(this.change, commitInfo, this.serverConfig) || {}; + return name; + } +} + +customElements.define(GrCommitInfo.is, GrCommitInfo);
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js index 902bf41..ffd36f4 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_html.js
@@ -1,26 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> - -<dom-module id="gr-commit-info"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .container { align-items: center; @@ -29,19 +25,12 @@ </style> <div class="container"> <template is="dom-if" if="[[_showWebLink]]"> - <a target="_blank" rel="noopener" - href$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a> + <a target="_blank" rel="noopener" href\$="[[_webLink]]">[[_computeShortHash(commitInfo)]]</a> </template> <template is="dom-if" if="[[!_showWebLink]]"> [[_computeShortHash(commitInfo)]] </template> - <gr-copy-clipboard - has-tooltip - button-title="Copy full SHA to clipboard" - hide-input - text="[[commitInfo.commit]]"> + <gr-copy-clipboard has-tooltip="" button-title="Copy full SHA to clipboard" hide-input="" text="[[commitInfo.commit]]"> </gr-copy-clipboard> </div> - </template> - <script src="gr-commit-info.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html index b063561..f2fdbe0 100644 --- a/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html +++ b/polygerrit-ui/app/elements/change/gr-commit-info/gr-commit-info_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-commit-info</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../core/gr-router/gr-router.html"> -<link rel="import" href="gr-commit-info.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../core/gr-router/gr-router.js"></script> +<script type="module" src="./gr-commit-info.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../core/gr-router/gr-router.js'; +import './gr-commit-info.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,105 +42,108 @@ </template> </test-fixture> -<script> - suite('gr-commit-info tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../core/gr-router/gr-router.js'; +import './gr-commit-info.js'; +suite('gr-commit-info tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('weblinks use Gerrit.Nav interface', () => { - const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks') - .returns([{name: 'stubb', url: '#s'}]); - element.change = {}; - element.commitInfo = {}; - element.serverConfig = {}; - assert.isTrue(weblinksStub.called); - }); - - test('no web link when unavailable', () => { - element.commitInfo = {}; - element.serverConfig = {}; - element.change = {labels: [], project: ''}; - - assert.isNotOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - }); - - test('use web link when available', () => { - const router = document.createElement('gr-router'); - sandbox.stub(Gerrit.Nav, '_generateWeblinks', - router._generateWeblinks.bind(router)); - - element.change = {labels: [], project: ''}; - element.commitInfo = - {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]}; - element.serverConfig = {}; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - assert.equal(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig), 'link-url'); - }); - - test('does not relativize web links that begin with scheme', () => { - const router = document.createElement('gr-router'); - sandbox.stub(Gerrit.Nav, '_generateWeblinks', - router._generateWeblinks.bind(router)); - - element.change = {labels: [], project: ''}; - element.commitInfo = { - commit: 'commitsha', - web_links: [{name: 'gitweb', url: 'https://link-url'}], - }; - element.serverConfig = {}; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - assert.equal(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig), 'https://link-url'); - }); - - test('ignore web links that are neither gitweb nor gitiles', () => { - const router = document.createElement('gr-router'); - sandbox.stub(Gerrit.Nav, '_generateWeblinks', - router._generateWeblinks.bind(router)); - - element.change = {project: 'project-name'}; - element.commitInfo = { - commit: 'commit-sha', - web_links: [ - { - name: 'ignore', - url: 'ignore', - }, - { - name: 'gitiles', - url: 'https://link-url', - }, - ], - }; - element.serverConfig = {}; - - assert.isOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - assert.equal(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig), 'https://link-url'); - - // Remove gitiles link. - element.commitInfo.web_links.splice(1, 1); - assert.isNotOk(element._computeShowWebLink(element.change, - element.commitInfo, element.serverConfig)); - assert.isNotOk(element._computeWebLink(element.change, element.commitInfo, - element.serverConfig)); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('weblinks use Gerrit.Nav interface', () => { + const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks') + .returns([{name: 'stubb', url: '#s'}]); + element.change = {}; + element.commitInfo = {}; + element.serverConfig = {}; + assert.isTrue(weblinksStub.called); + }); + + test('no web link when unavailable', () => { + element.commitInfo = {}; + element.serverConfig = {}; + element.change = {labels: [], project: ''}; + + assert.isNotOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + }); + + test('use web link when available', () => { + const router = document.createElement('gr-router'); + sandbox.stub(Gerrit.Nav, '_generateWeblinks', + router._generateWeblinks.bind(router)); + + element.change = {labels: [], project: ''}; + element.commitInfo = + {commit: 'commitsha', web_links: [{name: 'gitweb', url: 'link-url'}]}; + element.serverConfig = {}; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), 'link-url'); + }); + + test('does not relativize web links that begin with scheme', () => { + const router = document.createElement('gr-router'); + sandbox.stub(Gerrit.Nav, '_generateWeblinks', + router._generateWeblinks.bind(router)); + + element.change = {labels: [], project: ''}; + element.commitInfo = { + commit: 'commitsha', + web_links: [{name: 'gitweb', url: 'https://link-url'}], + }; + element.serverConfig = {}; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), 'https://link-url'); + }); + + test('ignore web links that are neither gitweb nor gitiles', () => { + const router = document.createElement('gr-router'); + sandbox.stub(Gerrit.Nav, '_generateWeblinks', + router._generateWeblinks.bind(router)); + + element.change = {project: 'project-name'}; + element.commitInfo = { + commit: 'commit-sha', + web_links: [ + { + name: 'ignore', + url: 'ignore', + }, + { + name: 'gitiles', + url: 'https://link-url', + }, + ], + }; + element.serverConfig = {}; + + assert.isOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.equal(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig), 'https://link-url'); + + // Remove gitiles link. + element.commitInfo.web_links.splice(1, 1); + assert.isNotOk(element._computeShowWebLink(element.change, + element.commitInfo, element.serverConfig)); + assert.isNotOk(element._computeWebLink(element.change, element.commitInfo, + element.serverConfig)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js index 555c605..d950988 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.js
@@ -14,69 +14,79 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; + +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-abandon-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrConfirmAbandonDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-abandon-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmAbandonDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-abandon-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - message: String, - }; - } - - get keyBindings() { - return { - 'ctrl+enter meta+enter': '_handleEnterKey', - }; - } - - resetFocus() { - this.$.messageInput.textarea.focus(); - } - - _handleEnterKey(e) { - this._confirm(); - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this._confirm(); - } - - _confirm() { - this.fire('confirm', {reason: this.message}, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } + static get properties() { + return { + message: String, + }; } - customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog); -})(); + get keyBindings() { + return { + 'ctrl+enter meta+enter': '_handleEnterKey', + }; + } + + resetFocus() { + this.$.messageInput.textarea.focus(); + } + + _handleEnterKey(e) { + this._confirm(); + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this._confirm(); + } + + _confirm() { + this.fire('confirm', {reason: this.message}, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } +} + +customElements.define(GrConfirmAbandonDialog.is, GrConfirmAbandonDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js index 9e7857c4..e8b530b 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-confirm-abandon-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -48,21 +42,11 @@ width: 73ch; /* Add a char to account for the border. */ } </style> - <gr-dialog - confirm-label="Abandon" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Abandon" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Abandon Change</div> <div class="main" slot="main"> <label for="messageInput">Abandon Message</label> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - placeholder="<Insert reasoning here>" - bind-value="{{message}}"></iron-autogrow-textarea> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" placeholder="<Insert reasoning here>" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-dialog> - </template> - <script src="gr-confirm-abandon-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html index 3786174..522b290 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-abandon-dialog/gr-confirm-abandon-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-abandon-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-abandon-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-abandon-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-abandon-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,46 +40,48 @@ </template> </test-fixture> -<script> - suite('gr-confirm-abandon-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-abandon-dialog.js'; +suite('gr-confirm-abandon-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('_handleConfirmTap', () => { - const confirmHandler = sandbox.stub(); - element.addEventListener('confirm', confirmHandler); - sandbox.spy(element, '_handleConfirmTap'); - sandbox.spy(element, '_confirm'); - element.shadowRoot - .querySelector('gr-dialog').fire('confirm'); - assert.isTrue(confirmHandler.called); - assert.isTrue(confirmHandler.calledOnce); - assert.isTrue(element._handleConfirmTap.called); - assert.isTrue(element._confirm.called); - assert.isTrue(element._confirm.called); - assert.isTrue(element._confirm.calledOnce); - }); - - test('_handleCancelTap', () => { - const cancelHandler = sandbox.stub(); - element.addEventListener('cancel', cancelHandler); - sandbox.spy(element, '_handleCancelTap'); - element.shadowRoot - .querySelector('gr-dialog').fire('cancel'); - assert.isTrue(cancelHandler.called); - assert.isTrue(cancelHandler.calledOnce); - assert.isTrue(element._handleCancelTap.called); - assert.isTrue(element._handleCancelTap.calledOnce); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('_handleConfirmTap', () => { + const confirmHandler = sandbox.stub(); + element.addEventListener('confirm', confirmHandler); + sandbox.spy(element, '_handleConfirmTap'); + sandbox.spy(element, '_confirm'); + element.shadowRoot + .querySelector('gr-dialog').fire('confirm'); + assert.isTrue(confirmHandler.called); + assert.isTrue(confirmHandler.calledOnce); + assert.isTrue(element._handleConfirmTap.called); + assert.isTrue(element._confirm.called); + assert.isTrue(element._confirm.called); + assert.isTrue(element._confirm.calledOnce); + }); + + test('_handleCancelTap', () => { + const cancelHandler = sandbox.stub(); + element.addEventListener('cancel', cancelHandler); + sandbox.spy(element, '_handleCancelTap'); + element.shadowRoot + .querySelector('gr-dialog').fire('cancel'); + assert.isTrue(cancelHandler.called); + assert.isTrue(cancelHandler.calledOnce); + assert.isTrue(element._handleCancelTap.called); + assert.isTrue(element._handleCancelTap.calledOnce); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js index 35e9afb..9c5ddf4 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog.js
@@ -14,45 +14,54 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-cherrypick-conflict-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrConfirmCherrypickConflictDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the confirm button is pressed. + * + * @event confirm */ - class GrConfirmCherrypickConflictDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-cherrypick-conflict-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', null, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); } - customElements.define(GrConfirmCherrypickConflictDialog.is, - GrConfirmCherrypickConflictDialog); -})(); + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } +} + +customElements.define(GrConfirmCherrypickConflictDialog.is, + GrConfirmCherrypickConflictDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js index b9e9155..c03c246 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> - -<dom-module id="gr-confirm-cherrypick-conflict-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -36,10 +31,7 @@ width: 100%; } </style> - <gr-dialog - confirm-label="Continue" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Continue" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Cherry Pick Conflict!</div> <div class="main" slot="main"> <span>Cherry Pick failed! (merge conflicts)</span> @@ -47,6 +39,4 @@ <span>Please select "Continue" to continue with conflicts or select "cancel" to close the dialog.</span> </div> </gr-dialog> - </template> - <script src="gr-confirm-cherrypick-conflict-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html index 7c9896a..58b1182 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-conflict-dialog/gr-confirm-cherrypick-conflict-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-cherrypick-conflict-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-cherrypick-conflict-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-cherrypick-conflict-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-cherrypick-conflict-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,41 +40,43 @@ </template> </test-fixture> -<script> - suite('gr-confirm-cherrypick-conflict-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-cherrypick-conflict-dialog.js'; +suite('gr-confirm-cherrypick-conflict-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('_handleConfirmTap', () => { - const confirmHandler = sandbox.stub(); - element.addEventListener('confirm', confirmHandler); - sandbox.spy(element, '_handleConfirmTap'); - element.shadowRoot - .querySelector('gr-dialog').fire('confirm'); - assert.isTrue(confirmHandler.called); - assert.isTrue(confirmHandler.calledOnce); - assert.isTrue(element._handleConfirmTap.called); - assert.isTrue(element._handleConfirmTap.calledOnce); - }); - - test('_handleCancelTap', () => { - const cancelHandler = sandbox.stub(); - element.addEventListener('cancel', cancelHandler); - sandbox.spy(element, '_handleCancelTap'); - element.shadowRoot - .querySelector('gr-dialog').fire('cancel'); - assert.isTrue(cancelHandler.called); - assert.isTrue(cancelHandler.calledOnce); - assert.isTrue(element._handleCancelTap.called); - assert.isTrue(element._handleCancelTap.calledOnce); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { sandbox.restore(); }); + + test('_handleConfirmTap', () => { + const confirmHandler = sandbox.stub(); + element.addEventListener('confirm', confirmHandler); + sandbox.spy(element, '_handleConfirmTap'); + element.shadowRoot + .querySelector('gr-dialog').fire('confirm'); + assert.isTrue(confirmHandler.called); + assert.isTrue(confirmHandler.calledOnce); + assert.isTrue(element._handleConfirmTap.called); + assert.isTrue(element._handleConfirmTap.calledOnce); + }); + + test('_handleCancelTap', () => { + const cancelHandler = sandbox.stub(); + element.addEventListener('cancel', cancelHandler); + sandbox.spy(element, '_handleCancelTap'); + element.shadowRoot + .querySelector('gr-dialog').fire('cancel'); + assert.isTrue(cancelHandler.called); + assert.isTrue(cancelHandler.calledOnce); + assert.isTrue(element._handleCancelTap.called); + assert.isTrue(element._handleCancelTap.calledOnce); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js index 2b10a97..7405a30 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.js
@@ -14,115 +14,128 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const SUGGESTIONS_LIMIT = 15; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-cherrypick-dialog_html.js'; + +const SUGGESTIONS_LIMIT = 15; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrConfirmCherrypickDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-cherrypick-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmCherrypickDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-cherrypick-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - branch: String, - baseCommit: String, - changeStatus: String, - commitMessage: String, - commitNum: String, - message: String, - project: String, - _query: { - type: Function, - value() { - return this._getProjectBranchesSuggestions.bind(this); - }, + static get properties() { + return { + branch: String, + baseCommit: String, + changeStatus: String, + commitMessage: String, + commitNum: String, + message: String, + project: String, + _query: { + type: Function, + value() { + return this._getProjectBranchesSuggestions.bind(this); }, - }; - } - - static get observers() { - return [ - '_computeMessage(changeStatus, commitNum, commitMessage)', - ]; - } - - _computeMessage(changeStatus, commitNum, commitMessage) { - // Polymer 2: check for undefined - if ([ - changeStatus, - commitNum, - commitMessage, - ].some(arg => arg === undefined)) { - return; - } - - let newMessage = commitMessage; - - if (changeStatus === 'MERGED') { - newMessage += '(cherry picked from commit ' + commitNum + ')'; - } - this.message = newMessage; - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', null, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } - - resetFocus() { - this.$.branchInput.focus(); - } - - _getProjectBranchesSuggestions(input) { - if (input.startsWith('refs/heads/')) { - input = input.substring('refs/heads/'.length); - } - return this.$.restAPI.getRepoBranches( - input, this.project, SUGGESTIONS_LIMIT).then(response => { - const branches = []; - let branch; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - if (response[key].ref.startsWith('refs/heads/')) { - branch = response[key].ref.substring('refs/heads/'.length); - } else { - branch = response[key].ref; - } - branches.push({ - name: branch, - }); - } - return branches; - }); - } + }, + }; } - customElements.define(GrConfirmCherrypickDialog.is, - GrConfirmCherrypickDialog); -})(); + static get observers() { + return [ + '_computeMessage(changeStatus, commitNum, commitMessage)', + ]; + } + + _computeMessage(changeStatus, commitNum, commitMessage) { + // Polymer 2: check for undefined + if ([ + changeStatus, + commitNum, + commitMessage, + ].some(arg => arg === undefined)) { + return; + } + + let newMessage = commitMessage; + + if (changeStatus === 'MERGED') { + newMessage += '(cherry picked from commit ' + commitNum + ')'; + } + this.message = newMessage; + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } + + resetFocus() { + this.$.branchInput.focus(); + } + + _getProjectBranchesSuggestions(input) { + if (input.startsWith('refs/heads/')) { + input = input.substring('refs/heads/'.length); + } + return this.$.restAPI.getRepoBranches( + input, this.project, SUGGESTIONS_LIMIT).then(response => { + const branches = []; + let branch; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + if (response[key].ref.startsWith('refs/heads/')) { + branch = response[key].ref.substring('refs/heads/'.length); + } else { + branch = response[key].ref; + } + branches.push({ + name: branch, + }); + } + return branches; + }); + } +} + +customElements.define(GrConfirmCherrypickDialog.is, + GrConfirmCherrypickDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js index cab9fd6..ee6e55d 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-confirm-cherrypick-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -54,48 +45,25 @@ width: 73ch; /* Add a char to account for the border. */ } </style> - <gr-dialog - confirm-label="Cherry Pick" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Cherry Pick" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Cherry Pick Change to Another Branch</div> <div class="main" slot="main"> <label for="branchInput"> Cherry Pick to branch </label> - <gr-autocomplete - id="branchInput" - text="{{branch}}" - query="[[_query]]" - placeholder="Destination branch"> + <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch"> </gr-autocomplete> <label for="baseInput"> Provide base commit sha1 for cherry-pick </label> - <iron-input - maxlength="40" - placeholder="(optional)" - bind-value="{{baseCommit}}"> - <input - is="iron-input" - id="baseCommitInput" - maxlength="40" - placeholder="(optional)" - bind-value="{{baseCommit}}"> + <iron-input maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}"> + <input is="iron-input" id="baseCommitInput" maxlength="40" placeholder="(optional)" bind-value="{{baseCommit}}"> </iron-input> <label for="messageInput"> Cherry Pick Commit Message </label> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - rows="4" - max-rows="15" - bind-value="{{message}}"></iron-autogrow-textarea> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-confirm-cherrypick-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html index 12e6252..fa9531f 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-cherrypick-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-cherrypick-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-cherrypick-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-cherrypick-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,85 +40,87 @@ </template> </test-fixture> -<script> - suite('gr-confirm-cherrypick-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-cherrypick-dialog.js'; +suite('gr-confirm-cherrypick-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getRepoBranches(input) { - if (input.startsWith('test')) { - return Promise.resolve([ - { - ref: 'refs/heads/test-branch', - revision: '67ebf73496383c6777035e374d2d664009e2aa5c', - can_delete: true, - }, - ]); - } else { - return Promise.resolve({}); - } - }, - }); - element = fixture('basic'); - element.project = 'test-project'; + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getRepoBranches(input) { + if (input.startsWith('test')) { + return Promise.resolve([ + { + ref: 'refs/heads/test-branch', + revision: '67ebf73496383c6777035e374d2d664009e2aa5c', + can_delete: true, + }, + ]); + } else { + return Promise.resolve({}); + } + }, }); + element = fixture('basic'); + element.project = 'test-project'; + }); - teardown(() => { sandbox.restore(); }); + teardown(() => { sandbox.restore(); }); - test('with merged change', () => { - element.changeStatus = 'MERGED'; - element.commitMessage = 'message\n'; - element.commitNum = '123'; - element.branch = 'master'; - flushAsynchronousOperations(); - const expectedMessage = 'message\n(cherry picked from commit 123)'; - assert.equal(element.message, expectedMessage); - }); + test('with merged change', () => { + element.changeStatus = 'MERGED'; + element.commitMessage = 'message\n'; + element.commitNum = '123'; + element.branch = 'master'; + flushAsynchronousOperations(); + const expectedMessage = 'message\n(cherry picked from commit 123)'; + assert.equal(element.message, expectedMessage); + }); - test('with unmerged change', () => { - element.changeStatus = 'OPEN'; - element.commitMessage = 'message\n'; - element.commitNum = '123'; - element.branch = 'master'; - flushAsynchronousOperations(); - const expectedMessage = 'message\n'; - assert.equal(element.message, expectedMessage); - }); + test('with unmerged change', () => { + element.changeStatus = 'OPEN'; + element.commitMessage = 'message\n'; + element.commitNum = '123'; + element.branch = 'master'; + flushAsynchronousOperations(); + const expectedMessage = 'message\n'; + assert.equal(element.message, expectedMessage); + }); - test('with updated commit message', () => { - element.changeStatus = 'OPEN'; - element.commitMessage = 'message\n'; - element.commitNum = '123'; - element.branch = 'master'; - const myNewMessage = 'updated commit message'; - element.message = myNewMessage; - flushAsynchronousOperations(); - assert.equal(element.message, myNewMessage); - }); + test('with updated commit message', () => { + element.changeStatus = 'OPEN'; + element.commitMessage = 'message\n'; + element.commitNum = '123'; + element.branch = 'master'; + const myNewMessage = 'updated commit message'; + element.message = myNewMessage; + flushAsynchronousOperations(); + assert.equal(element.message, myNewMessage); + }); - test('_getProjectBranchesSuggestions empty', done => { - element._getProjectBranchesSuggestions('nonexistent').then(branches => { - assert.equal(branches.length, 0); - done(); - }); - }); - - test('resetFocus', () => { - const focusStub = sandbox.stub(element.$.branchInput, 'focus'); - element.resetFocus(); - assert.isTrue(focusStub.called); - }); - - test('_getProjectBranchesSuggestions non-empty', done => { - element._getProjectBranchesSuggestions('test-branch').then(branches => { - assert.equal(branches.length, 1); - assert.equal(branches[0].name, 'test-branch'); - done(); - }); + test('_getProjectBranchesSuggestions empty', done => { + element._getProjectBranchesSuggestions('nonexistent').then(branches => { + assert.equal(branches.length, 0); + done(); }); }); + + test('resetFocus', () => { + const focusStub = sandbox.stub(element.$.branchInput, 'focus'); + element.resetFocus(); + assert.isTrue(focusStub.called); + }); + + test('_getProjectBranchesSuggestions non-empty', done => { + element._getProjectBranchesSuggestions('test-branch').then(branches => { + assert.equal(branches.length, 1); + assert.equal(branches[0].name, 'test-branch'); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js index 8932af7..8316951 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -14,90 +14,102 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; - const SUGGESTIONS_LIMIT = 15; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-move-dialog_html.js'; + +const SUGGESTIONS_LIMIT = 15; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrConfirmMoveDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-move-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmMoveDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-move-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - branch: String, - message: String, - project: String, - _query: { - type: Function, - value() { - return this._getProjectBranchesSuggestions.bind(this); - }, + static get properties() { + return { + branch: String, + message: String, + project: String, + _query: { + type: Function, + value() { + return this._getProjectBranchesSuggestions.bind(this); }, - }; - } - - get keyBindings() { - return { - 'ctrl+enter meta+enter': '_handleConfirmTap', - }; - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', null, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } - - _getProjectBranchesSuggestions(input) { - if (input.startsWith('refs/heads/')) { - input = input.substring('refs/heads/'.length); - } - return this.$.restAPI.getRepoBranches( - input, this.project, SUGGESTIONS_LIMIT).then(response => { - const branches = []; - let branch; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - if (response[key].ref.startsWith('refs/heads/')) { - branch = response[key].ref.substring('refs/heads/'.length); - } else { - branch = response[key].ref; - } - branches.push({ - name: branch, - }); - } - return branches; - }); - } + }, + }; } - customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog); -})(); + get keyBindings() { + return { + 'ctrl+enter meta+enter': '_handleConfirmTap', + }; + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } + + _getProjectBranchesSuggestions(input) { + if (input.startsWith('refs/heads/')) { + input = input.substring('refs/heads/'.length); + } + return this.$.restAPI.getRepoBranches( + input, this.project, SUGGESTIONS_LIMIT).then(response => { + const branches = []; + let branch; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + if (response[key].ref.startsWith('refs/heads/')) { + branch = response[key].ref.substring('refs/heads/'.length); + } else { + branch = response[key].ref; + } + branches.push({ + name: branch, + }); + } + return branches; + }); + } +} + +customElements.define(GrConfirmMoveDialog.is, GrConfirmMoveDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js index f65ec03..b8f3336 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-confirm-move-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -54,10 +46,7 @@ color: var(--error-text-color); } </style> - <gr-dialog - confirm-label="Move Change" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Move Change" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Move Change to Another Branch</div> <div class="main" slot="main"> <p class="warning"> @@ -66,25 +55,13 @@ <label for="branchInput"> Move change to branch </label> - <gr-autocomplete - id="branchInput" - text="{{branch}}" - query="[[_query]]" - placeholder="Destination branch"> + <gr-autocomplete id="branchInput" text="{{branch}}" query="[[_query]]" placeholder="Destination branch"> </gr-autocomplete> <label for="messageInput"> Move Change Message </label> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - rows="4" - max-rows="15" - bind-value="{{message}}"></iron-autogrow-textarea> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" rows="4" max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-confirm-move-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html index 465ce73..27c9934 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-move-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-move-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-move-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-move-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,52 +40,54 @@ </template> </test-fixture> -<script> - suite('gr-confirm-move-dialog tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-move-dialog.js'; +suite('gr-confirm-move-dialog tests', () => { + let element; - setup(() => { - stub('gr-rest-api-interface', { - getRepoBranches(input) { - if (input.startsWith('test')) { - return Promise.resolve([ - { - ref: 'refs/heads/test-branch', - revision: '67ebf73496383c6777035e374d2d664009e2aa5c', - can_delete: true, - }, - ]); - } else { - return Promise.resolve({}); - } - }, - }); - element = fixture('basic'); - element.project = 'test-project'; + setup(() => { + stub('gr-rest-api-interface', { + getRepoBranches(input) { + if (input.startsWith('test')) { + return Promise.resolve([ + { + ref: 'refs/heads/test-branch', + revision: '67ebf73496383c6777035e374d2d664009e2aa5c', + can_delete: true, + }, + ]); + } else { + return Promise.resolve({}); + } + }, }); + element = fixture('basic'); + element.project = 'test-project'; + }); - test('with updated commit message', () => { - element.branch = 'master'; - const myNewMessage = 'updated commit message'; - element.message = myNewMessage; - flushAsynchronousOperations(); - assert.equal(element.message, myNewMessage); - }); + test('with updated commit message', () => { + element.branch = 'master'; + const myNewMessage = 'updated commit message'; + element.message = myNewMessage; + flushAsynchronousOperations(); + assert.equal(element.message, myNewMessage); + }); - test('_getProjectBranchesSuggestions empty', done => { - element._getProjectBranchesSuggestions('nonexistent').then(branches => { - assert.equal(branches.length, 0); - done(); - }); - }); - - test('_getProjectBranchesSuggestions non-empty', done => { - element._getProjectBranchesSuggestions('test-branch').then(branches => { - assert.equal(branches.length, 1); - assert.equal(branches[0].name, 'test-branch'); - done(); - }); + test('_getProjectBranchesSuggestions empty', done => { + element._getProjectBranchesSuggestions('nonexistent').then(branches => { + assert.equal(branches.length, 0); + done(); }); }); + + test('_getProjectBranchesSuggestions non-empty', done => { + element._getProjectBranchesSuggestions('test-branch').then(branches => { + assert.equal(branches.length, 1); + assert.equal(branches[0].name, 'test-branch'); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js index 607f587..e451034 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.js
@@ -14,157 +14,166 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrConfirmRebaseDialog extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-confirm-rebase-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-rebase-dialog_html.js'; - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ +/** @extends Polymer.Element */ +class GrConfirmRebaseDialog extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - static get properties() { - return { - branch: String, - changeNumber: Number, - hasParent: Boolean, - rebaseOnCurrent: Boolean, - _text: String, - _query: { - type: Function, - value() { - return this._getChangeSuggestions.bind(this); - }, + static get is() { return 'gr-confirm-rebase-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ + + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ + + static get properties() { + return { + branch: String, + changeNumber: Number, + hasParent: Boolean, + rebaseOnCurrent: Boolean, + _text: String, + _query: { + type: Function, + value() { + return this._getChangeSuggestions.bind(this); }, - _recentChanges: Array, - }; - } - - static get observers() { - return [ - '_updateSelectedOption(rebaseOnCurrent, hasParent)', - ]; - } - - // This is called by gr-change-actions every time the rebase dialog is - // re-opened. Unlike other autocompletes that make a request with each - // updated input, this one gets all recent changes once and then filters - // them by the input. The query is re-run each time the dialog is opened - // in case there are new/updated changes in the generic query since the - // last time it was run. - fetchRecentChanges() { - return this.$.restAPI.getChanges(null, `is:open -age:90d`) - .then(response => { - const changes = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - changes.push({ - name: `${response[key]._number}: ${response[key].subject}`, - value: response[key]._number, - }); - } - this._recentChanges = changes; - return this._recentChanges; - }); - } - - _getRecentChanges() { - if (this._recentChanges) { - return Promise.resolve(this._recentChanges); - } - return this.fetchRecentChanges(); - } - - _getChangeSuggestions(input) { - return this._getRecentChanges().then(changes => - this._filterChanges(input, changes)); - } - - _filterChanges(input, changes) { - return changes.filter(change => change.name.includes(input) && - change.value !== this.changeNumber); - } - - _displayParentOption(rebaseOnCurrent, hasParent) { - return hasParent && rebaseOnCurrent; - } - - _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) { - return hasParent && !rebaseOnCurrent; - } - - _displayTipOption(rebaseOnCurrent, hasParent) { - return !(!rebaseOnCurrent && !hasParent); - } - - /** - * There is a subtle but important difference between setting the base to an - * empty string and omitting it entirely from the payload. An empty string - * implies that the parent should be cleared and the change should be - * rebased on top of the target branch. Leaving out the base implies that it - * should be rebased on top of its current parent. - */ - _getSelectedBase() { - if (this.$.rebaseOnParentInput.checked) { return null; } - if (this.$.rebaseOnTipInput.checked) { return ''; } - // Change numbers will have their description appended by the - // autocomplete. - return this._text.split(':')[0]; - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent(new CustomEvent('confirm', - {detail: {base: this._getSelectedBase()}})); - this._text = ''; - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent(new CustomEvent('cancel')); - this._text = ''; - } - - _handleRebaseOnOther() { - this.$.parentInput.focus(); - } - - _handleEnterChangeNumberClick() { - this.$.rebaseOnOtherInput.checked = true; - } - - /** - * Sets the default radio button based on the state of the app and - * the corresponding value to be submitted. - */ - _updateSelectedOption(rebaseOnCurrent, hasParent) { - // Polymer 2: check for undefined - if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) { - return; - } - - if (this._displayParentOption(rebaseOnCurrent, hasParent)) { - this.$.rebaseOnParentInput.checked = true; - } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) { - this.$.rebaseOnTipInput.checked = true; - } else { - this.$.rebaseOnOtherInput.checked = true; - } - } + }, + _recentChanges: Array, + }; } - customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog); -})(); + static get observers() { + return [ + '_updateSelectedOption(rebaseOnCurrent, hasParent)', + ]; + } + + // This is called by gr-change-actions every time the rebase dialog is + // re-opened. Unlike other autocompletes that make a request with each + // updated input, this one gets all recent changes once and then filters + // them by the input. The query is re-run each time the dialog is opened + // in case there are new/updated changes in the generic query since the + // last time it was run. + fetchRecentChanges() { + return this.$.restAPI.getChanges(null, `is:open -age:90d`) + .then(response => { + const changes = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + changes.push({ + name: `${response[key]._number}: ${response[key].subject}`, + value: response[key]._number, + }); + } + this._recentChanges = changes; + return this._recentChanges; + }); + } + + _getRecentChanges() { + if (this._recentChanges) { + return Promise.resolve(this._recentChanges); + } + return this.fetchRecentChanges(); + } + + _getChangeSuggestions(input) { + return this._getRecentChanges().then(changes => + this._filterChanges(input, changes)); + } + + _filterChanges(input, changes) { + return changes.filter(change => change.name.includes(input) && + change.value !== this.changeNumber); + } + + _displayParentOption(rebaseOnCurrent, hasParent) { + return hasParent && rebaseOnCurrent; + } + + _displayParentUpToDateMsg(rebaseOnCurrent, hasParent) { + return hasParent && !rebaseOnCurrent; + } + + _displayTipOption(rebaseOnCurrent, hasParent) { + return !(!rebaseOnCurrent && !hasParent); + } + + /** + * There is a subtle but important difference between setting the base to an + * empty string and omitting it entirely from the payload. An empty string + * implies that the parent should be cleared and the change should be + * rebased on top of the target branch. Leaving out the base implies that it + * should be rebased on top of its current parent. + */ + _getSelectedBase() { + if (this.$.rebaseOnParentInput.checked) { return null; } + if (this.$.rebaseOnTipInput.checked) { return ''; } + // Change numbers will have their description appended by the + // autocomplete. + return this._text.split(':')[0]; + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('confirm', + {detail: {base: this._getSelectedBase()}})); + this._text = ''; + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('cancel')); + this._text = ''; + } + + _handleRebaseOnOther() { + this.$.parentInput.focus(); + } + + _handleEnterChangeNumberClick() { + this.$.rebaseOnOtherInput.checked = true; + } + + /** + * Sets the default radio button based on the state of the app and + * the corresponding value to be submitted. + */ + _updateSelectedOption(rebaseOnCurrent, hasParent) { + // Polymer 2: check for undefined + if ([rebaseOnCurrent, hasParent].some(arg => arg === undefined)) { + return; + } + + if (this._displayParentOption(rebaseOnCurrent, hasParent)) { + this.$.rebaseOnParentInput.checked = true; + } else if (this._displayTipOption(rebaseOnCurrent, hasParent)) { + this.$.rebaseOnTipInput.checked = true; + } else { + this.$.rebaseOnOtherInput.checked = true; + } + } +} + +customElements.define(GrConfirmRebaseDialog.is, GrConfirmRebaseDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js index cf2721a..20872bc 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-confirm-rebase-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -50,70 +44,43 @@ margin: var(--spacing-m) 0; } </style> - <gr-dialog - id="confirmDialog" - confirm-label="Rebase" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog id="confirmDialog" confirm-label="Rebase" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Confirm rebase</div> <div class="main" slot="main"> - <div id="rebaseOnParent" class="rebaseOption" - hidden$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]"> - <input id="rebaseOnParentInput" - name="rebaseOptions" - type="radio" - on-click="_handleRebaseOnParent"> + <div id="rebaseOnParent" class="rebaseOption" hidden\$="[[!_displayParentOption(rebaseOnCurrent, hasParent)]]"> + <input id="rebaseOnParentInput" name="rebaseOptions" type="radio" on-click="_handleRebaseOnParent"> <label id="rebaseOnParentLabel" for="rebaseOnParentInput"> Rebase on parent change </label> </div> - <div id="parentUpToDateMsg" class="message" - hidden$="[[!_displayParentUpToDateMsg(rebaseOnCurrent, hasParent)]]"> + <div id="parentUpToDateMsg" class="message" hidden\$="[[!_displayParentUpToDateMsg(rebaseOnCurrent, hasParent)]]"> This change is up to date with its parent. </div> - <div id="rebaseOnTip" class="rebaseOption" - hidden$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"> - <input id="rebaseOnTipInput" - name="rebaseOptions" - type="radio" - disabled$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]" - on-click="_handleRebaseOnTip"> + <div id="rebaseOnTip" class="rebaseOption" hidden\$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]"> + <input id="rebaseOnTipInput" name="rebaseOptions" type="radio" disabled\$="[[!_displayTipOption(rebaseOnCurrent, hasParent)]]" on-click="_handleRebaseOnTip"> <label id="rebaseOnTipLabel" for="rebaseOnTipInput"> Rebase on top of the [[branch]] - branch<span hidden$="[[!hasParent]]"> + branch<span hidden\$="[[!hasParent]]"> (breaks relation chain) </span> </label> </div> - <div id="tipUpToDateMsg" class="message" - hidden$="[[_displayTipOption(rebaseOnCurrent, hasParent)]]"> + <div id="tipUpToDateMsg" class="message" hidden\$="[[_displayTipOption(rebaseOnCurrent, hasParent)]]"> Change is up to date with the target branch already ([[branch]]) </div> <div id="rebaseOnOther" class="rebaseOption"> - <input id="rebaseOnOtherInput" - name="rebaseOptions" - type="radio" - on-click="_handleRebaseOnOther"> + <input id="rebaseOnOtherInput" name="rebaseOptions" type="radio" on-click="_handleRebaseOnOther"> <label id="rebaseOnOtherLabel" for="rebaseOnOtherInput"> - Rebase on a specific change, ref, or commit <span hidden$="[[!hasParent]]"> + Rebase on a specific change, ref, or commit <span hidden\$="[[!hasParent]]"> (breaks relation chain) </span> </label> </div> <div class="parentRevisionContainer"> - <gr-autocomplete - id="parentInput" - query="[[_query]]" - no-debounce - text="{{_text}}" - on-click="_handleEnterChangeNumberClick" - allow-non-suggested-values - placeholder="Change number, ref, or commit hash"> + <gr-autocomplete id="parentInput" query="[[_query]]" no-debounce="" text="{{_text}}" on-click="_handleEnterChangeNumberClick" allow-non-suggested-values="" placeholder="Change number, ref, or commit hash"> </gr-autocomplete> </div> </div> </gr-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-confirm-rebase-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html index fe42cac..d715fbe 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-rebase-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-rebase-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-rebase-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-rebase-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,167 +40,169 @@ </template> </test-fixture> -<script> - suite('gr-confirm-rebase-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-rebase-dialog.js'; +suite('gr-confirm-rebase-dialog tests', () => { + let element; + let sandbox; + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('controls with parent and rebase on current available', () => { + element.rebaseOnCurrent = true; + element.hasParent = true; + flushAsynchronousOperations(); + assert.isTrue(element.$.rebaseOnParentInput.checked); + assert.isFalse(element.$.rebaseOnParent.hasAttribute('hidden')); + assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden')); + assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden')); + assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); + }); + + test('controls with parent rebase on current not available', () => { + element.rebaseOnCurrent = false; + element.hasParent = true; + flushAsynchronousOperations(); + assert.isTrue(element.$.rebaseOnTipInput.checked); + assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden')); + assert.isFalse(element.$.parentUpToDateMsg.hasAttribute('hidden')); + assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden')); + assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); + }); + + test('controls without parent and rebase on current available', () => { + element.rebaseOnCurrent = true; + element.hasParent = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.rebaseOnTipInput.checked); + assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden')); + assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden')); + assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden')); + assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); + }); + + test('controls without parent rebase on current not available', () => { + element.rebaseOnCurrent = false; + element.hasParent = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.rebaseOnOtherInput.checked); + assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden')); + assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden')); + assert.isTrue(element.$.rebaseOnTip.hasAttribute('hidden')); + assert.isFalse(element.$.tipUpToDateMsg.hasAttribute('hidden')); + }); + + test('input cleared on cancel or submit', () => { + element._text = '123'; + element.$.confirmDialog.fire('confirm'); + assert.equal(element._text, ''); + + element._text = '123'; + element.$.confirmDialog.fire('cancel'); + assert.equal(element._text, ''); + }); + + test('_getSelectedBase', () => { + element._text = '5fab321c'; + element.$.rebaseOnParentInput.checked = true; + assert.equal(element._getSelectedBase(), null); + element.$.rebaseOnParentInput.checked = false; + element.$.rebaseOnTipInput.checked = true; + assert.equal(element._getSelectedBase(), ''); + element.$.rebaseOnTipInput.checked = false; + assert.equal(element._getSelectedBase(), element._text); + element._text = '101: Test'; + assert.equal(element._getSelectedBase(), '101'); + }); + + suite('parent suggestions', () => { + let recentChanges; setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); + recentChanges = [ + { + name: '123: my first awesome change', + value: 123, + }, + { + name: '124: my second awesome change', + value: 124, + }, + { + name: '245: my third awesome change', + value: 245, + }, + ]; + + sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve( + [ + { + _number: 123, + subject: 'my first awesome change', + }, + { + _number: 124, + subject: 'my second awesome change', + }, + { + _number: 245, + subject: 'my third awesome change', + }, + ] + )); }); - teardown(() => { - sandbox.restore(); + test('_getRecentChanges', () => { + sandbox.spy(element, '_getRecentChanges'); + return element._getRecentChanges() + .then(() => { + assert.deepEqual(element._recentChanges, recentChanges); + assert.equal(element.$.restAPI.getChanges.callCount, 1); + // When called a second time, should not re-request recent changes. + element._getRecentChanges(); + }) + .then(() => { + assert.equal(element._getRecentChanges.callCount, 2); + assert.equal(element.$.restAPI.getChanges.callCount, 1); + }); }); - test('controls with parent and rebase on current available', () => { - element.rebaseOnCurrent = true; - element.hasParent = true; - flushAsynchronousOperations(); - assert.isTrue(element.$.rebaseOnParentInput.checked); - assert.isFalse(element.$.rebaseOnParent.hasAttribute('hidden')); - assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden')); - assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden')); - assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); + test('_filterChanges', () => { + assert.equal(element._filterChanges('123', recentChanges).length, 1); + assert.equal(element._filterChanges('12', recentChanges).length, 2); + assert.equal(element._filterChanges('awesome', recentChanges).length, + 3); + assert.equal(element._filterChanges('third', recentChanges).length, + 1); + + element.changeNumber = 123; + assert.equal(element._filterChanges('123', recentChanges).length, 0); + assert.equal(element._filterChanges('124', recentChanges).length, 1); + assert.equal(element._filterChanges('awesome', recentChanges).length, + 2); }); - test('controls with parent rebase on current not available', () => { - element.rebaseOnCurrent = false; - element.hasParent = true; - flushAsynchronousOperations(); - assert.isTrue(element.$.rebaseOnTipInput.checked); - assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden')); - assert.isFalse(element.$.parentUpToDateMsg.hasAttribute('hidden')); - assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden')); - assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); - }); - - test('controls without parent and rebase on current available', () => { - element.rebaseOnCurrent = true; - element.hasParent = false; - flushAsynchronousOperations(); - assert.isTrue(element.$.rebaseOnTipInput.checked); - assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden')); - assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden')); - assert.isFalse(element.$.rebaseOnTip.hasAttribute('hidden')); - assert.isTrue(element.$.tipUpToDateMsg.hasAttribute('hidden')); - }); - - test('controls without parent rebase on current not available', () => { - element.rebaseOnCurrent = false; - element.hasParent = false; - flushAsynchronousOperations(); - assert.isTrue(element.$.rebaseOnOtherInput.checked); - assert.isTrue(element.$.rebaseOnParent.hasAttribute('hidden')); - assert.isTrue(element.$.parentUpToDateMsg.hasAttribute('hidden')); - assert.isTrue(element.$.rebaseOnTip.hasAttribute('hidden')); - assert.isFalse(element.$.tipUpToDateMsg.hasAttribute('hidden')); - }); - - test('input cleared on cancel or submit', () => { - element._text = '123'; - element.$.confirmDialog.fire('confirm'); - assert.equal(element._text, ''); - - element._text = '123'; - element.$.confirmDialog.fire('cancel'); - assert.equal(element._text, ''); - }); - - test('_getSelectedBase', () => { - element._text = '5fab321c'; - element.$.rebaseOnParentInput.checked = true; - assert.equal(element._getSelectedBase(), null); - element.$.rebaseOnParentInput.checked = false; - element.$.rebaseOnTipInput.checked = true; - assert.equal(element._getSelectedBase(), ''); - element.$.rebaseOnTipInput.checked = false; - assert.equal(element._getSelectedBase(), element._text); - element._text = '101: Test'; - assert.equal(element._getSelectedBase(), '101'); - }); - - suite('parent suggestions', () => { - let recentChanges; - setup(() => { - recentChanges = [ - { - name: '123: my first awesome change', - value: 123, - }, - { - name: '124: my second awesome change', - value: 124, - }, - { - name: '245: my third awesome change', - value: 245, - }, - ]; - - sandbox.stub(element.$.restAPI, 'getChanges').returns(Promise.resolve( - [ - { - _number: 123, - subject: 'my first awesome change', - }, - { - _number: 124, - subject: 'my second awesome change', - }, - { - _number: 245, - subject: 'my third awesome change', - }, - ] - )); - }); - - test('_getRecentChanges', () => { - sandbox.spy(element, '_getRecentChanges'); - return element._getRecentChanges() - .then(() => { - assert.deepEqual(element._recentChanges, recentChanges); - assert.equal(element.$.restAPI.getChanges.callCount, 1); - // When called a second time, should not re-request recent changes. - element._getRecentChanges(); - }) - .then(() => { - assert.equal(element._getRecentChanges.callCount, 2); - assert.equal(element.$.restAPI.getChanges.callCount, 1); - }); - }); - - test('_filterChanges', () => { - assert.equal(element._filterChanges('123', recentChanges).length, 1); - assert.equal(element._filterChanges('12', recentChanges).length, 2); - assert.equal(element._filterChanges('awesome', recentChanges).length, - 3); - assert.equal(element._filterChanges('third', recentChanges).length, - 1); - - element.changeNumber = 123; - assert.equal(element._filterChanges('123', recentChanges).length, 0); - assert.equal(element._filterChanges('124', recentChanges).length, 1); - assert.equal(element._filterChanges('awesome', recentChanges).length, - 2); - }); - - test('input text change triggers function', () => { - sandbox.spy(element, '_getRecentChanges'); - element.$.parentInput.noDebounce = true; - MockInteractions.pressAndReleaseKeyOn( - element.$.parentInput.$.input, - 13, - null, - 'enter'); - element._text = '1'; - assert.isTrue(element._getRecentChanges.calledOnce); - element._text = '12'; - assert.isTrue(element._getRecentChanges.calledTwice); - }); + test('input text change triggers function', () => { + sandbox.spy(element, '_getRecentChanges'); + element.$.parentInput.noDebounce = true; + MockInteractions.pressAndReleaseKeyOn( + element.$.parentInput.$.input, + 13, + null, + 'enter'); + element._text = '1'; + assert.isTrue(element._getRecentChanges.calledOnce); + element._text = '12'; + assert.isTrue(element._getRecentChanges.calledTwice); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js index 05660bf..9bc0f8d 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog.js
@@ -14,184 +14,196 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; - const ERR_COMMIT_NOT_FOUND = - 'Unable to find the commit hash of this change.'; - const CHANGE_SUBJECT_LIMIT = 50; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-revert-dialog_html.js'; - // TODO(dhruvsri): clean up repeated definitions after moving to js modules - const REVERT_TYPES = { - REVERT_SINGLE_CHANGE: 1, - REVERT_SUBMISSION: 2, - }; +const ERR_COMMIT_NOT_FOUND = + 'Unable to find the commit hash of this change.'; +const CHANGE_SUBJECT_LIMIT = 50; + +// TODO(dhruvsri): clean up repeated definitions after moving to js modules +const REVERT_TYPES = { + REVERT_SINGLE_CHANGE: 1, + REVERT_SUBMISSION: 2, +}; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrConfirmRevertDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-revert-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmRevertDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-revert-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - /* The revert message updated by the user - The default value is set by the dialog */ - _message: String, - _revertType: { - type: Number, - value: REVERT_TYPES.REVERT_SINGLE_CHANGE, - }, - _showRevertSubmission: { - type: Boolean, - value: false, - }, - _changesCount: Number, - _showErrorMessage: { - type: Boolean, - value: false, - }, - /* store the default revert messages per revert type so that we can - check if user has edited the revert message or not - Set when populate() is called */ - _originalRevertMessages: { - type: Array, - value() { return []; }, - }, - // Store the actual messages that the user has edited - _revertMessages: { - type: Array, - value() { return []; }, - }, - }; - } - - _computeIfSingleRevert(revertType) { - return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE; - } - - _computeIfRevertSubmission(revertType) { - return revertType === REVERT_TYPES.REVERT_SUBMISSION; - } - - _modifyRevertMsg(change, commitMessage, message) { - return this.$.jsAPI.modifyRevertMsg(change, - message, commitMessage); - } - - populate(change, commitMessage, changes) { - this._changesCount = changes.length; - // The option to revert a single change is always available - this._populateRevertSingleChangeMessage( - change, commitMessage, change.current_revision); - this._populateRevertSubmissionMessage(change, changes, commitMessage); - } - - _populateRevertSingleChangeMessage(change, commitMessage, commitHash) { - // Figure out what the revert title should be. - const originalTitle = (commitMessage || '').split('\n')[0]; - const revertTitle = `Revert "${originalTitle}"`; - if (!commitHash) { - this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); - return; - } - const revertCommitText = `This reverts commit ${commitHash}.`; - - this._message = `${revertTitle}\n\n${revertCommitText}\n\n` + - `Reason for revert: <INSERT REASONING HERE>\n`; - // This is to give plugins a chance to update message - this._message = this._modifyRevertMsg(change, commitMessage, - this._message); - this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; - this._showRevertSubmission = false; - this._revertMessages[this._revertType] = this._message; - this._originalRevertMessages[this._revertType] = this._message; - } - - _getTrimmedChangeSubject(subject) { - if (!subject) return ''; - if (subject.length < CHANGE_SUBJECT_LIMIT) return subject; - return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...'; - } - - _modifyRevertSubmissionMsg(change, msg, commitMessage) { - return this.$.jsAPI.modifyRevertSubmissionMsg(change, msg, - commitMessage); - } - - _populateRevertSubmissionMessage(change, changes, commitMessage) { - // Follow the same convention of the revert - const commitHash = change.current_revision; - if (!commitHash) { - this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); - return; - } - if (!changes || changes.length <= 1) return; - const submissionId = change.submission_id; - const revertTitle = 'Revert submission ' + submissionId; - this._message = revertTitle + '\n\n' + 'Reason for revert: <INSERT ' + - 'REASONING HERE>\n'; - this._message += 'Reverted Changes:\n'; - changes.forEach(change => { - this._message += change.change_id.substring(0, 10) + ':' - + this._getTrimmedChangeSubject(change.subject) + '\n'; - }); - this._message = this._modifyRevertSubmissionMsg(change, this._message, - commitMessage); - this._revertType = REVERT_TYPES.REVERT_SUBMISSION; - this._revertMessages[this._revertType] = this._message; - this._originalRevertMessages[this._revertType] = this._message; - this._showRevertSubmission = true; - } - - _handleRevertSingleChangeClicked() { - this._showErrorMessage = false; - this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message; - this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE]; - this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; - } - - _handleRevertSubmissionClicked() { - this._showErrorMessage = false; - this._revertType = REVERT_TYPES.REVERT_SUBMISSION; - this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message; - this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION]; - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - if (this._message === this._originalRevertMessages[this._revertType]) { - this._showErrorMessage = true; - return; - } - this.fire('confirm', {revertType: this._revertType, - message: this._message}, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', {revertType: this._revertType}, - {bubbles: false}); - } + static get properties() { + return { + /* The revert message updated by the user + The default value is set by the dialog */ + _message: String, + _revertType: { + type: Number, + value: REVERT_TYPES.REVERT_SINGLE_CHANGE, + }, + _showRevertSubmission: { + type: Boolean, + value: false, + }, + _changesCount: Number, + _showErrorMessage: { + type: Boolean, + value: false, + }, + /* store the default revert messages per revert type so that we can + check if user has edited the revert message or not + Set when populate() is called */ + _originalRevertMessages: { + type: Array, + value() { return []; }, + }, + // Store the actual messages that the user has edited + _revertMessages: { + type: Array, + value() { return []; }, + }, + }; } - customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog); -})(); + _computeIfSingleRevert(revertType) { + return revertType === REVERT_TYPES.REVERT_SINGLE_CHANGE; + } + + _computeIfRevertSubmission(revertType) { + return revertType === REVERT_TYPES.REVERT_SUBMISSION; + } + + _modifyRevertMsg(change, commitMessage, message) { + return this.$.jsAPI.modifyRevertMsg(change, + message, commitMessage); + } + + populate(change, commitMessage, changes) { + this._changesCount = changes.length; + // The option to revert a single change is always available + this._populateRevertSingleChangeMessage( + change, commitMessage, change.current_revision); + this._populateRevertSubmissionMessage(change, changes, commitMessage); + } + + _populateRevertSingleChangeMessage(change, commitMessage, commitHash) { + // Figure out what the revert title should be. + const originalTitle = (commitMessage || '').split('\n')[0]; + const revertTitle = `Revert "${originalTitle}"`; + if (!commitHash) { + this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); + return; + } + const revertCommitText = `This reverts commit ${commitHash}.`; + + this._message = `${revertTitle}\n\n${revertCommitText}\n\n` + + `Reason for revert: <INSERT REASONING HERE>\n`; + // This is to give plugins a chance to update message + this._message = this._modifyRevertMsg(change, commitMessage, + this._message); + this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; + this._showRevertSubmission = false; + this._revertMessages[this._revertType] = this._message; + this._originalRevertMessages[this._revertType] = this._message; + } + + _getTrimmedChangeSubject(subject) { + if (!subject) return ''; + if (subject.length < CHANGE_SUBJECT_LIMIT) return subject; + return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...'; + } + + _modifyRevertSubmissionMsg(change, msg, commitMessage) { + return this.$.jsAPI.modifyRevertSubmissionMsg(change, msg, + commitMessage); + } + + _populateRevertSubmissionMessage(change, changes, commitMessage) { + // Follow the same convention of the revert + const commitHash = change.current_revision; + if (!commitHash) { + this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); + return; + } + if (!changes || changes.length <= 1) return; + const submissionId = change.submission_id; + const revertTitle = 'Revert submission ' + submissionId; + this._message = revertTitle + '\n\n' + 'Reason for revert: <INSERT ' + + 'REASONING HERE>\n'; + this._message += 'Reverted Changes:\n'; + changes.forEach(change => { + this._message += change.change_id.substring(0, 10) + ':' + + this._getTrimmedChangeSubject(change.subject) + '\n'; + }); + this._message = this._modifyRevertSubmissionMsg(change, this._message, + commitMessage); + this._revertType = REVERT_TYPES.REVERT_SUBMISSION; + this._revertMessages[this._revertType] = this._message; + this._originalRevertMessages[this._revertType] = this._message; + this._showRevertSubmission = true; + } + + _handleRevertSingleChangeClicked() { + this._showErrorMessage = false; + this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION] = this._message; + this._message = this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE]; + this._revertType = REVERT_TYPES.REVERT_SINGLE_CHANGE; + } + + _handleRevertSubmissionClicked() { + this._showErrorMessage = false; + this._revertType = REVERT_TYPES.REVERT_SUBMISSION; + this._revertMessages[REVERT_TYPES.REVERT_SINGLE_CHANGE] = this._message; + this._message = this._revertMessages[REVERT_TYPES.REVERT_SUBMISSION]; + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + if (this._message === this._originalRevertMessages[this._revertType]) { + this._showErrorMessage = true; + return; + } + this.fire('confirm', {revertType: this._revertType, + message: this._message}, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', {revertType: this._revertType}, + {bubbles: false}); + } +} + +customElements.define(GrConfirmRevertDialog.is, GrConfirmRevertDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js index 144cf20..3f293cf 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-confirm-revert-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -56,54 +48,34 @@ margin-bottom: var(--spacing-m); } </style> - <gr-dialog - confirm-label="Revert" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Revert" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header"> Revert Merged Change </div> <div class="main" slot="main"> - <div class="error" hidden$="[[!_showErrorMessage]]"> + <div class="error" hidden\$="[[!_showErrorMessage]]"> <span> A reason is required </span> </div> <template is="dom-if" if="[[_showRevertSubmission]]"> <div class="revertSubmissionLayout"> - <input - name="revertOptions" - type="radio" - id="revertSingleChange" - on-change="_handleRevertSingleChangeClicked" - checked="[[_computeIfSingleRevert(_revertType)]]"> + <input name="revertOptions" type="radio" id="revertSingleChange" on-change="_handleRevertSingleChangeClicked" checked="[[_computeIfSingleRevert(_revertType)]]"> <label for="revertSingleChange" class="label revertSingleChange"> Revert single change </label> </div> <div class="revertSubmissionLayout"> - <input - name="revertOptions" - type="radio" - id="revertSubmission" - on-change="_handleRevertSubmissionClicked" - checked="[[_computeIfRevertSubmission(_revertType)]]"> + <input name="revertOptions" type="radio" id="revertSubmission" on-change="_handleRevertSubmissionClicked" checked="[[_computeIfRevertSubmission(_revertType)]]"> <label for="revertSubmission" class="label revertSubmission"> Revert entire submission ([[_changesCount]] Changes) </label> - </template> + </div></template> <gr-endpoint-decorator name="confirm-revert-change"> <label for="messageInput"> Revert Commit Message </label> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - max-rows="15" - bind-value="{{_message}}"></iron-autogrow-textarea> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" max-rows="15" bind-value="{{_message}}"></iron-autogrow-textarea> </gr-endpoint-decorator> </div> </gr-dialog> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> - </template> - <script src="gr-confirm-revert-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html index 29886dd..1f8837b 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-revert-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-revert-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-revert-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-revert-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,70 +40,72 @@ </template> </test-fixture> -<script> - suite('gr-confirm-revert-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-revert-dialog.js'; +suite('gr-confirm-revert-dialog tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox =sinon.sandbox.create(); - }); - - teardown(() => sandbox.restore()); - - test('no match', () => { - assert.isNotOk(element._message); - const alertStub = sandbox.stub(); - element.addEventListener('show-alert', alertStub); - element._populateRevertSingleChangeMessage({}, - 'not a commitHash in sight', undefined); - assert.isTrue(alertStub.calledOnce); - }); - - test('single line', () => { - assert.isNotOk(element._message); - element._populateRevertSingleChangeMessage({}, - 'one line commit\n\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert "one line commit"\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element._message, expected); - }); - - test('multi line', () => { - assert.isNotOk(element._message); - element._populateRevertSingleChangeMessage({}, - 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert "many lines"\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element._message, expected); - }); - - test('issue above change id', () => { - assert.isNotOk(element._message); - element._populateRevertSingleChangeMessage({}, - 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert "much lines"\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element._message, expected); - }); - - test('revert a revert', () => { - assert.isNotOk(element._message); - element._populateRevertSingleChangeMessage({}, - 'Revert "one line commit"\n\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert "Revert "one line commit""\n\n' + - 'This reverts commit abcd123.\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element._message, expected); - }); + setup(() => { + element = fixture('basic'); + sandbox =sinon.sandbox.create(); }); + + teardown(() => sandbox.restore()); + + test('no match', () => { + assert.isNotOk(element._message); + const alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); + element._populateRevertSingleChangeMessage({}, + 'not a commitHash in sight', undefined); + assert.isTrue(alertStub.calledOnce); + }); + + test('single line', () => { + assert.isNotOk(element._message); + element._populateRevertSingleChangeMessage({}, + 'one line commit\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert "one line commit"\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element._message, expected); + }); + + test('multi line', () => { + assert.isNotOk(element._message); + element._populateRevertSingleChangeMessage({}, + 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert "many lines"\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element._message, expected); + }); + + test('issue above change id', () => { + assert.isNotOk(element._message); + element._populateRevertSingleChangeMessage({}, + 'much lines\nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert "much lines"\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element._message, expected); + }); + + test('revert a revert', () => { + assert.isNotOk(element._message); + element._populateRevertSingleChangeMessage({}, + 'Revert "one line commit"\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert "Revert "one line commit""\n\n' + + 'This reverts commit abcd123.\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element._message, expected); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js index ae8dfa5..3cae44a 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog.js
@@ -14,87 +14,98 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; - const ERR_COMMIT_NOT_FOUND = - 'Unable to find the commit hash of this change.'; - const CHANGE_SUBJECT_LIMIT = 50; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-revert-submission-dialog_html.js'; + +const ERR_COMMIT_NOT_FOUND = + 'Unable to find the commit hash of this change.'; +const CHANGE_SUBJECT_LIMIT = 50; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrConfirmRevertSubmissionDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-revert-submission-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmRevertSubmissionDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-revert-submission-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - message: String, - commitMessage: String, - }; - } - - _getTrimmedChangeSubject(subject) { - if (!subject) return ''; - if (subject.length < CHANGE_SUBJECT_LIMIT) return subject; - return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...'; - } - - _modifyRevertSubmissionMsg(change) { - return this.$.jsAPI.modifyRevertSubmissionMsg(change, - this.message, this.commitMessage); - } - - _populateRevertSubmissionMessage(message, change, changes) { - // Follow the same convention of the revert - const commitHash = change.current_revision; - if (!commitHash) { - this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); - return; - } - const submissionId = change.submission_id; - const revertTitle = 'Revert submission ' + submissionId; - this.changes = changes; - this.message = revertTitle + '\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - this.message += 'Reverted Changes:\n'; - changes = changes || []; - changes.forEach(change => { - this.message += change.change_id.substring(0, 10) + ': ' + - this._getTrimmedChangeSubject(change.subject) + '\n'; - }); - this.message = this._modifyRevertSubmissionMsg(change); - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', null, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } + static get properties() { + return { + message: String, + commitMessage: String, + }; } - customElements.define(GrConfirmRevertSubmissionDialog.is, - GrConfirmRevertSubmissionDialog); -})(); + _getTrimmedChangeSubject(subject) { + if (!subject) return ''; + if (subject.length < CHANGE_SUBJECT_LIMIT) return subject; + return subject.substring(0, CHANGE_SUBJECT_LIMIT) + '...'; + } + + _modifyRevertSubmissionMsg(change) { + return this.$.jsAPI.modifyRevertSubmissionMsg(change, + this.message, this.commitMessage); + } + + _populateRevertSubmissionMessage(message, change, changes) { + // Follow the same convention of the revert + const commitHash = change.current_revision; + if (!commitHash) { + this.fire('show-alert', {message: ERR_COMMIT_NOT_FOUND}); + return; + } + const submissionId = change.submission_id; + const revertTitle = 'Revert submission ' + submissionId; + this.changes = changes; + this.message = revertTitle + '\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + this.message += 'Reverted Changes:\n'; + changes = changes || []; + changes.forEach(change => { + this.message += change.change_id.substring(0, 10) + ': ' + + this._getTrimmedChangeSubject(change.subject) + '\n'; + }); + this.message = this._modifyRevertSubmissionMsg(change); + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } +} + +customElements.define(GrConfirmRevertSubmissionDialog.is, + GrConfirmRevertSubmissionDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js index f2cfef8..a68920c 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2019 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-confirm-revert-submission-dialog"> - <template> +export const htmlTemplate = html` <!-- TODO(taoalpha): move all shared styles to a style module. --> <style include="shared-styles"> :host { @@ -45,24 +38,14 @@ width: 73ch; /* Add a char to account for the border. */ } </style> - <gr-dialog - confirm-label="Revert Submission" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Revert Submission" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Revert Submission</div> <div class="main" slot="main"> <label for="messageInput"> Revert Commit Message </label> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - max-rows="15" - bind-value="{{message}}"></iron-autogrow-textarea> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" max-rows="15" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-dialog> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> - </template> - <script src="gr-confirm-revert-submission-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html index 2513986..d84aa4d 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-revert-submission-dialog/gr-confirm-revert-submission-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-revert-submission-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-confirm-revert-submission-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-confirm-revert-submission-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-revert-submission-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,67 +41,69 @@ </template> </test-fixture> -<script> - suite('gr-confirm-revert-submission-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-revert-submission-dialog.js'; +suite('gr-confirm-revert-submission-dialog tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox =sinon.sandbox.create(); - }); - - teardown(() => sandbox.restore()); - - test('no match', () => { - assert.isNotOk(element.message); - const alertStub = sandbox.stub(); - element.addEventListener('show-alert', alertStub); - element._populateRevertSubmissionMessage( - 'not a commitHash in sight' - ); - assert.isTrue(alertStub.calledOnce); - }); - - test('single line', () => { - assert.isNotOk(element.message); - element._populateRevertSubmissionMessage( - 'one line commit\n\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert submission\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element.message, expected); - }); - - test('multi line', () => { - assert.isNotOk(element.message); - element._populateRevertSubmissionMessage( - 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert submission\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element.message, expected); - }); - - test('issue above change id', () => { - assert.isNotOk(element.message); - element._populateRevertSubmissionMessage( - 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert submission\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element.message, expected); - }); - - test('revert a revert', () => { - assert.isNotOk(element.message); - element._populateRevertSubmissionMessage( - 'Revert "one line commit"\n\nChange-Id: abcdefg\n', - 'abcd123'); - const expected = 'Revert submission\n\n' + - 'Reason for revert: <INSERT REASONING HERE>\n'; - assert.equal(element.message, expected); - }); + setup(() => { + element = fixture('basic'); + sandbox =sinon.sandbox.create(); }); + + teardown(() => sandbox.restore()); + + test('no match', () => { + assert.isNotOk(element.message); + const alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); + element._populateRevertSubmissionMessage( + 'not a commitHash in sight' + ); + assert.isTrue(alertStub.calledOnce); + }); + + test('single line', () => { + assert.isNotOk(element.message); + element._populateRevertSubmissionMessage( + 'one line commit\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + + test('multi line', () => { + assert.isNotOk(element.message); + element._populateRevertSubmissionMessage( + 'many lines\ncommit\n\nmessage\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + + test('issue above change id', () => { + assert.isNotOk(element.message); + element._populateRevertSubmissionMessage( + 'test \nvery\n\ncommit\n\nBug: Issue 42\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); + + test('revert a revert', () => { + assert.isNotOk(element.message); + element._populateRevertSubmissionMessage( + 'Revert "one line commit"\n\nChange-Id: abcdefg\n', + 'abcd123'); + const expected = 'Revert submission\n\n' + + 'Reason for revert: <INSERT REASONING HERE>\n'; + assert.equal(element.message, expected); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js index aa26681..037d53d 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.js
@@ -14,67 +14,80 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrConfirmSubmitDialog extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-confirm-submit-dialog'; } +import '@polymer/iron-icon/iron-icon.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-submit-dialog_html.js'; + +/** @extends Polymer.Element */ +class GrConfirmSubmitDialog extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-submit-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ + + /** + * Fired when the cancel button is pressed. + * + * @event cancel + */ + + static get properties() { + return { /** - * Fired when the confirm button is pressed. - * - * @event confirm + * @type {{ + * is_private: boolean, + * subject: string, + * }} */ + change: Object, - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { /** * @type {{ - * is_private: boolean, - * subject: string, + * label: string, * }} */ - change: Object, - - /** - * @type {{ - * label: string, - * }} - */ - action: Object, - }; - } - - resetFocus(e) { - this.$.dialog.resetFocus(); - } - - _computeUnresolvedCommentsWarning(change) { - const unresolvedCount = change.unresolved_comment_count; - const plural = unresolvedCount > 1 ? 's' : ''; - return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`; - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent(new CustomEvent('confirm', {bubbles: false})); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent(new CustomEvent('cancel', {bubbles: false})); - } + action: Object, + }; } - customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog); -})(); + resetFocus(e) { + this.$.dialog.resetFocus(); + } + + _computeUnresolvedCommentsWarning(change) { + const unresolvedCount = change.unresolved_comment_count; + const plural = unresolvedCount > 1 ? 's' : ''; + return `Heads Up! ${unresolvedCount} unresolved comment${plural}.`; + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('confirm', {bubbles: false})); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('cancel', {bubbles: false})); + } +} + +customElements.define(GrConfirmSubmitDialog.is, GrConfirmSubmitDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js index a845ed4..03e0f17 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> - -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> - -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-confirm-submit-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> #dialog { min-width: 40em; @@ -48,18 +36,13 @@ } } </style> - <gr-dialog - id="dialog" - confirm-label="Continue" - confirm-on-enter - on-cancel="_handleCancelTap" - on-confirm="_handleConfirmTap"> + <gr-dialog id="dialog" confirm-label="Continue" confirm-on-enter="" on-cancel="_handleCancelTap" on-confirm="_handleConfirmTap"> <div class="header" slot="header"> [[action.label]] </div> <div class="main" slot="main"> <gr-endpoint-decorator name="confirm-submit-change"> - <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p> + <p>Ready to submit “<strong>[[change.subject]]</strong>”?</p> <template is="dom-if" if="[[change.is_private]]"> <p> <iron-icon icon="gr-icons:error" class="warningBeforeSubmit"></iron-icon> @@ -79,6 +62,4 @@ </div> </gr-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-confirm-submit-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html index a5dffa8..5acbbde 100644 --- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog_test.html
@@ -19,17 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-confirm-submit-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="gr-confirm-submit-dialog.html"> +<script type="module" src="./gr-confirm-submit-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-submit-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,43 +42,45 @@ </template> </test-fixture> -<script> - suite('gr-file-list-header tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-confirm-submit-dialog.js'; +suite('gr-file-list-header tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('display', () => { - element.action = {label: 'my-label'}; - element.change = {subject: 'my-subject'}; - flushAsynchronousOperations(); - const header = element.shadowRoot - .querySelector('.header'); - assert.equal(header.textContent.trim(), 'my-label'); - - const message = element.shadowRoot - .querySelector('.main p'); - assert.notEqual(message.textContent.length, 0); - assert.notEqual(message.textContent.indexOf('my-subject'), -1); - }); - - test('_computeUnresolvedCommentsWarning', () => { - const change = {unresolved_comment_count: 1}; - assert.equal(element._computeUnresolvedCommentsWarning(change), - 'Heads Up! 1 unresolved comment.'); - - const change2 = {unresolved_comment_count: 2}; - assert.equal(element._computeUnresolvedCommentsWarning(change2), - 'Heads Up! 2 unresolved comments.'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('display', () => { + element.action = {label: 'my-label'}; + element.change = {subject: 'my-subject'}; + flushAsynchronousOperations(); + const header = element.shadowRoot + .querySelector('.header'); + assert.equal(header.textContent.trim(), 'my-label'); + + const message = element.shadowRoot + .querySelector('.main p'); + assert.notEqual(message.textContent.length, 0); + assert.notEqual(message.textContent.indexOf('my-subject'), -1); + }); + + test('_computeUnresolvedCommentsWarning', () => { + const change = {unresolved_comment_count: 1}; + assert.equal(element._computeUnresolvedCommentsWarning(change), + 'Heads Up! 1 unresolved comment.'); + + const change2 = {unresolved_comment_count: 2}; + assert.equal(element._computeUnresolvedCommentsWarning(change2), + 'Heads Up! 2 unresolved comments.'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js index 17c6f50..1b6e521 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog.js
@@ -14,214 +14,225 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-download-commands/gr-download-commands.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-download-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrDownloadDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-download-dialog'; } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when the user presses the close button. + * + * @event close */ - class GrDownloadDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-download-dialog'; } - /** - * Fired when the user presses the close button. - * - * @event close - */ - static get properties() { - return { - /** @type {{ revisions: Array }} */ - change: Object, - patchNum: String, - /** @type {?} */ - config: Object, + static get properties() { + return { + /** @type {{ revisions: Array }} */ + change: Object, + patchNum: String, + /** @type {?} */ + config: Object, - _schemes: { - type: Array, - value() { return []; }, - computed: '_computeSchemes(change, patchNum)', - observer: '_schemesChanged', - }, - _selectedScheme: String, - }; - } + _schemes: { + type: Array, + value() { return []; }, + computed: '_computeSchemes(change, patchNum)', + observer: '_schemesChanged', + }, + _selectedScheme: String, + }; + } - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'dialog'); - } + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } - focus() { - if (this._schemes.length) { - this.$.downloadCommands.focusOnCopy(); - } else { - this.$.download.focus(); - } - } - - getFocusStops() { - const links = this.shadowRoot - .querySelector('#archives').querySelectorAll('a'); - return { - start: this.$.closeButton, - end: links[links.length - 1], - }; - } - - _computeDownloadCommands(change, patchNum, _selectedScheme) { - let commandObj; - if (!change) return []; - for (const rev of Object.values(change.revisions || {})) { - if (this.patchNumEquals(rev._number, patchNum) && - rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) { - commandObj = rev.fetch[_selectedScheme].commands; - break; - } - } - const commands = []; - for (const title in commandObj) { - if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; } - commands.push({ - title, - command: commandObj[title], - }); - } - return commands; - } - - /** - * @param {!Object} change - * @param {number|string} patchNum - * - * @return {string} - */ - _computeZipDownloadLink(change, patchNum) { - return this._computeDownloadLink(change, patchNum, true); - } - - /** - * @param {!Object} change - * @param {number|string} patchNum - * - * @return {string} - */ - _computeZipDownloadFilename(change, patchNum) { - return this._computeDownloadFilename(change, patchNum, true); - } - - /** - * @param {!Object} change - * @param {number|string} patchNum - * @param {boolean=} opt_zip - * - * @return {string} Not sure why there was a mismatch - */ - _computeDownloadLink(change, patchNum, opt_zip) { - // Polymer 2: check for undefined - if ([change, patchNum].some(arg => arg === undefined)) { - return ''; - } - return this.changeBaseURL(change.project, change._number, patchNum) + - '/patch?' + (opt_zip ? 'zip' : 'download'); - } - - /** - * @param {!Object} change - * @param {number|string} patchNum - * @param {boolean=} opt_zip - * - * @return {string} - */ - _computeDownloadFilename(change, patchNum, opt_zip) { - // Polymer 2: check for undefined - if ([change, patchNum].some(arg => arg === undefined)) { - return ''; - } - - let shortRev = ''; - for (const rev in change.revisions) { - if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) { - shortRev = rev.substr(0, 7); - break; - } - } - return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64'); - } - - _computeHidePatchFile(change, patchNum) { - // Polymer 2: check for undefined - if ([change, patchNum].some(arg => arg === undefined)) { - return false; - } - for (const rev of Object.values(change.revisions || {})) { - if (this.patchNumEquals(rev._number, patchNum)) { - const parentLength = rev.commit && rev.commit.parents ? - rev.commit.parents.length : 0; - return parentLength == 0; - } - } - return false; - } - - _computeArchiveDownloadLink(change, patchNum, format) { - // Polymer 2: check for undefined - if ([change, patchNum, format].some(arg => arg === undefined)) { - return ''; - } - return this.changeBaseURL(change.project, change._number, patchNum) + - '/archive?format=' + format; - } - - _computeSchemes(change, patchNum) { - // Polymer 2: check for undefined - if ([change, patchNum].some(arg => arg === undefined)) { - return []; - } - - for (const rev of Object.values(change.revisions || {})) { - if (this.patchNumEquals(rev._number, patchNum)) { - const fetch = rev.fetch; - if (fetch) { - return Object.keys(fetch).sort(); - } - break; - } - } - return []; - } - - _computePatchSetQuantity(revisions) { - if (!revisions) { return 0; } - return Object.keys(revisions).length; - } - - _handleCloseTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('close', null, {bubbles: false}); - } - - _schemesChanged(schemes) { - if (schemes.length === 0) { return; } - if (!schemes.includes(this._selectedScheme)) { - this._selectedScheme = schemes.sort()[0]; - } - } - - _computeShowDownloadCommands(schemes) { - return schemes.length ? '' : 'hidden'; + focus() { + if (this._schemes.length) { + this.$.downloadCommands.focusOnCopy(); + } else { + this.$.download.focus(); } } - customElements.define(GrDownloadDialog.is, GrDownloadDialog); -})(); + getFocusStops() { + const links = this.shadowRoot + .querySelector('#archives').querySelectorAll('a'); + return { + start: this.$.closeButton, + end: links[links.length - 1], + }; + } + + _computeDownloadCommands(change, patchNum, _selectedScheme) { + let commandObj; + if (!change) return []; + for (const rev of Object.values(change.revisions || {})) { + if (this.patchNumEquals(rev._number, patchNum) && + rev && rev.fetch && rev.fetch.hasOwnProperty(_selectedScheme)) { + commandObj = rev.fetch[_selectedScheme].commands; + break; + } + } + const commands = []; + for (const title in commandObj) { + if (!commandObj || !commandObj.hasOwnProperty(title)) { continue; } + commands.push({ + title, + command: commandObj[title], + }); + } + return commands; + } + + /** + * @param {!Object} change + * @param {number|string} patchNum + * + * @return {string} + */ + _computeZipDownloadLink(change, patchNum) { + return this._computeDownloadLink(change, patchNum, true); + } + + /** + * @param {!Object} change + * @param {number|string} patchNum + * + * @return {string} + */ + _computeZipDownloadFilename(change, patchNum) { + return this._computeDownloadFilename(change, patchNum, true); + } + + /** + * @param {!Object} change + * @param {number|string} patchNum + * @param {boolean=} opt_zip + * + * @return {string} Not sure why there was a mismatch + */ + _computeDownloadLink(change, patchNum, opt_zip) { + // Polymer 2: check for undefined + if ([change, patchNum].some(arg => arg === undefined)) { + return ''; + } + return this.changeBaseURL(change.project, change._number, patchNum) + + '/patch?' + (opt_zip ? 'zip' : 'download'); + } + + /** + * @param {!Object} change + * @param {number|string} patchNum + * @param {boolean=} opt_zip + * + * @return {string} + */ + _computeDownloadFilename(change, patchNum, opt_zip) { + // Polymer 2: check for undefined + if ([change, patchNum].some(arg => arg === undefined)) { + return ''; + } + + let shortRev = ''; + for (const rev in change.revisions) { + if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) { + shortRev = rev.substr(0, 7); + break; + } + } + return shortRev + '.diff.' + (opt_zip ? 'zip' : 'base64'); + } + + _computeHidePatchFile(change, patchNum) { + // Polymer 2: check for undefined + if ([change, patchNum].some(arg => arg === undefined)) { + return false; + } + for (const rev of Object.values(change.revisions || {})) { + if (this.patchNumEquals(rev._number, patchNum)) { + const parentLength = rev.commit && rev.commit.parents ? + rev.commit.parents.length : 0; + return parentLength == 0; + } + } + return false; + } + + _computeArchiveDownloadLink(change, patchNum, format) { + // Polymer 2: check for undefined + if ([change, patchNum, format].some(arg => arg === undefined)) { + return ''; + } + return this.changeBaseURL(change.project, change._number, patchNum) + + '/archive?format=' + format; + } + + _computeSchemes(change, patchNum) { + // Polymer 2: check for undefined + if ([change, patchNum].some(arg => arg === undefined)) { + return []; + } + + for (const rev of Object.values(change.revisions || {})) { + if (this.patchNumEquals(rev._number, patchNum)) { + const fetch = rev.fetch; + if (fetch) { + return Object.keys(fetch).sort(); + } + break; + } + } + return []; + } + + _computePatchSetQuantity(revisions) { + if (!revisions) { return 0; } + return Object.keys(revisions).length; + } + + _handleCloseTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('close', null, {bubbles: false}); + } + + _schemesChanged(schemes) { + if (schemes.length === 0) { return; } + if (!schemes.includes(this._selectedScheme)) { + this._selectedScheme = schemes.sort()[0]; + } + } + + _computeShowDownloadCommands(schemes) { + return schemes.length ? '' : 'hidden'; + } +} + +customElements.define(GrDownloadDialog.is, GrDownloadDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js index 4ddc876..324f9f4 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-download-commands/gr-download-commands.html"> - -<dom-module id="gr-download-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -77,37 +69,26 @@ Patch set [[patchNum]] of [[_computePatchSetQuantity(change.revisions)]] </h3> </section> - <section class$="[[_computeShowDownloadCommands(_schemes)]]"> - <gr-download-commands - id="downloadCommands" - commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" - schemes="[[_schemes]]" - selected-scheme="{{_selectedScheme}}"></gr-download-commands> + <section class\$="[[_computeShowDownloadCommands(_schemes)]]"> + <gr-download-commands id="downloadCommands" commands="[[_computeDownloadCommands(change, patchNum, _selectedScheme)]]" schemes="[[_schemes]]" selected-scheme="{{_selectedScheme}}"></gr-download-commands> </section> <section class="flexContainer"> - <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]" hidden> + <div class="patchFiles" hidden="[[_computeHidePatchFile(change, patchNum)]]"> <label>Patch file</label> <div> - <a - id="download" - href$="[[_computeDownloadLink(change, patchNum)]]" - download> + <a id="download" href\$="[[_computeDownloadLink(change, patchNum)]]" download=""> [[_computeDownloadFilename(change, patchNum)]] </a> - <a - href$="[[_computeZipDownloadLink(change, patchNum)]]" - download> + <a href\$="[[_computeZipDownloadLink(change, patchNum)]]" download=""> [[_computeZipDownloadFilename(change, patchNum)]] </a> </div> </div> - <div class="archivesContainer" hidden$="[[!config.archives.length]]" hidden> + <div class="archivesContainer" hidden\$="[[!config.archives.length]]" hidden=""> <label>Archive</label> <div id="archives" class="archives"> <template is="dom-repeat" items="[[config.archives]]" as="format"> - <a - href$="[[_computeArchiveDownloadLink(change, patchNum, format)]]" - download> + <a href\$="[[_computeArchiveDownloadLink(change, patchNum, format)]]" download=""> [[format]] </a> </template> @@ -116,11 +97,7 @@ </section> <section class="footer"> <span class="closeButtonContainer"> - <gr-button id="closeButton" - link - on-click="_handleCloseTap">Close</gr-button> + <gr-button id="closeButton" link="" on-click="_handleCloseTap">Close</gr-button> </span> </section> - </template> - <script src="gr-download-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html index a3755ba..8a55c21 100644 --- a/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-download-dialog/gr-download-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-download-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-download-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-download-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-download-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -41,184 +46,187 @@ </template> </test-fixture> -<script> - function getChangeObject() { - return { - current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72', - revisions: { - '34685798fe548b6d17d1e8e5edc43a26d055cc72': { - _number: 1, - commit: { - parents: [], +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-download-dialog.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +function getChangeObject() { + return { + current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72', + revisions: { + '34685798fe548b6d17d1e8e5edc43a26d055cc72': { + _number: 1, + commit: { + parents: [], + }, + fetch: { + repo: { + commands: { + repo: 'repo download test-project 5/1', + }, }, - fetch: { - repo: { - commands: { - repo: 'repo download test-project 5/1', - }, + ssh: { + commands: { + 'Checkout': + 'git fetch ' + + 'ssh://andybons@localhost:29418/test-project ' + + 'refs/changes/05/5/1 && git checkout FETCH_HEAD', + 'Cherry Pick': + 'git fetch ' + + 'ssh://andybons@localhost:29418/test-project ' + + 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD', + 'Format Patch': + 'git fetch ' + + 'ssh://andybons@localhost:29418/test-project ' + + 'refs/changes/05/5/1 ' + + '&& git format-patch -1 --stdout FETCH_HEAD', + 'Pull': + 'git pull ' + + 'ssh://andybons@localhost:29418/test-project ' + + 'refs/changes/05/5/1', }, - ssh: { - commands: { - 'Checkout': - 'git fetch ' + - 'ssh://andybons@localhost:29418/test-project ' + - 'refs/changes/05/5/1 && git checkout FETCH_HEAD', - 'Cherry Pick': - 'git fetch ' + - 'ssh://andybons@localhost:29418/test-project ' + - 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD', - 'Format Patch': - 'git fetch ' + - 'ssh://andybons@localhost:29418/test-project ' + - 'refs/changes/05/5/1 ' + - '&& git format-patch -1 --stdout FETCH_HEAD', - 'Pull': - 'git pull ' + - 'ssh://andybons@localhost:29418/test-project ' + - 'refs/changes/05/5/1', - }, - }, - http: { - commands: { - 'Checkout': - 'git fetch ' + - 'http://andybons@localhost:8080/a/test-project ' + - 'refs/changes/05/5/1 && git checkout FETCH_HEAD', - 'Cherry Pick': - 'git fetch ' + - 'http://andybons@localhost:8080/a/test-project ' + - 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD', - 'Format Patch': - 'git fetch ' + - 'http://andybons@localhost:8080/a/test-project ' + - 'refs/changes/05/5/1 && ' + - 'git format-patch -1 --stdout FETCH_HEAD', - 'Pull': - 'git pull ' + - 'http://andybons@localhost:8080/a/test-project ' + - 'refs/changes/05/5/1', - }, + }, + http: { + commands: { + 'Checkout': + 'git fetch ' + + 'http://andybons@localhost:8080/a/test-project ' + + 'refs/changes/05/5/1 && git checkout FETCH_HEAD', + 'Cherry Pick': + 'git fetch ' + + 'http://andybons@localhost:8080/a/test-project ' + + 'refs/changes/05/5/1 && git cherry-pick FETCH_HEAD', + 'Format Patch': + 'git fetch ' + + 'http://andybons@localhost:8080/a/test-project ' + + 'refs/changes/05/5/1 && ' + + 'git format-patch -1 --stdout FETCH_HEAD', + 'Pull': + 'git pull ' + + 'http://andybons@localhost:8080/a/test-project ' + + 'refs/changes/05/5/1', }, }, }, }, - }; - } + }, + }; +} - function getChangeObjectNoFetch() { - return { - current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72', - revisions: { - '34685798fe548b6d17d1e8e5edc43a26d055cc72': { - _number: 1, - commit: { - parents: [], - }, - fetch: {}, +function getChangeObjectNoFetch() { + return { + current_revision: '34685798fe548b6d17d1e8e5edc43a26d055cc72', + revisions: { + '34685798fe548b6d17d1e8e5edc43a26d055cc72': { + _number: 1, + commit: { + parents: [], }, + fetch: {}, }, + }, + }; +} + +suite('gr-download-dialog', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + + element = fixture('basic'); + element.patchNum = '1'; + element.config = { + schemes: { + 'anonymous http': {}, + 'http': {}, + 'repo': {}, + 'ssh': {}, + }, + archives: ['tgz', 'tar', 'tbz2', 'txz'], }; - } - suite('gr-download-dialog', async () => { - await readyToTest(); - let element; - let sandbox; + flushAsynchronousOperations(); + }); + teardown(() => { + sandbox.restore(); + }); + + test('anchors use download attribute', () => { + const anchors = Array.from( + dom(element.root).querySelectorAll('a')); + assert.isTrue(!anchors.some(a => !a.hasAttribute('download'))); + }); + + suite('gr-download-dialog tests with no fetch options', () => { setup(() => { - sandbox = sinon.sandbox.create(); - - element = fixture('basic'); - element.patchNum = '1'; - element.config = { - schemes: { - 'anonymous http': {}, - 'http': {}, - 'repo': {}, - 'ssh': {}, - }, - archives: ['tgz', 'tar', 'tbz2', 'txz'], - }; - + element.change = getChangeObjectNoFetch(); flushAsynchronousOperations(); }); - teardown(() => { - sandbox.restore(); - }); - - test('anchors use download attribute', () => { - const anchors = Array.from( - Polymer.dom(element.root).querySelectorAll('a')); - assert.isTrue(!anchors.some(a => !a.hasAttribute('download'))); - }); - - suite('gr-download-dialog tests with no fetch options', () => { - setup(() => { - element.change = getChangeObjectNoFetch(); - flushAsynchronousOperations(); - }); - - test('focuses on first download link if no copy links', () => { - const focusStub = sandbox.stub(element.$.download, 'focus'); - element.focus(); - assert.isTrue(focusStub.called); - focusStub.restore(); - }); - }); - - suite('gr-download-dialog with fetch options', () => { - setup(() => { - element.change = getChangeObject(); - flushAsynchronousOperations(); - }); - - test('focuses on first copy link', () => { - const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy'); - element.focus(); - flushAsynchronousOperations(); - assert.isTrue(focusStub.called); - focusStub.restore(); - }); - - test('computed fields', () => { - assert.equal(element._computeArchiveDownloadLink( - {project: 'test/project', _number: 123}, 2, 'tgz'), - '/changes/test%2Fproject~123/revisions/2/archive?format=tgz'); - }); - - test('close event', done => { - element.addEventListener('close', () => { - done(); - }); - MockInteractions.tap(element.shadowRoot - .querySelector('.closeButtonContainer gr-button')); - }); - }); - - test('_computeShowDownloadCommands', () => { - assert.equal(element._computeShowDownloadCommands([]), 'hidden'); - assert.equal(element._computeShowDownloadCommands(['test']), ''); - }); - - test('_computeHidePatchFile', () => { - const patchNum = '1'; - - const change1 = { - revisions: { - r1: {_number: 1, commit: {parents: []}}, - }, - }; - assert.isTrue(element._computeHidePatchFile(change1, patchNum)); - - const change2 = { - revisions: { - r1: {_number: 1, commit: {parents: [ - {commit: 'p1'}, - ]}}, - }, - }; - assert.isFalse(element._computeHidePatchFile(change2, patchNum)); + test('focuses on first download link if no copy links', () => { + const focusStub = sandbox.stub(element.$.download, 'focus'); + element.focus(); + assert.isTrue(focusStub.called); + focusStub.restore(); }); }); + + suite('gr-download-dialog with fetch options', () => { + setup(() => { + element.change = getChangeObject(); + flushAsynchronousOperations(); + }); + + test('focuses on first copy link', () => { + const focusStub = sinon.stub(element.$.downloadCommands, 'focusOnCopy'); + element.focus(); + flushAsynchronousOperations(); + assert.isTrue(focusStub.called); + focusStub.restore(); + }); + + test('computed fields', () => { + assert.equal(element._computeArchiveDownloadLink( + {project: 'test/project', _number: 123}, 2, 'tgz'), + '/changes/test%2Fproject~123/revisions/2/archive?format=tgz'); + }); + + test('close event', done => { + element.addEventListener('close', () => { + done(); + }); + MockInteractions.tap(element.shadowRoot + .querySelector('.closeButtonContainer gr-button')); + }); + }); + + test('_computeShowDownloadCommands', () => { + assert.equal(element._computeShowDownloadCommands([]), 'hidden'); + assert.equal(element._computeShowDownloadCommands(['test']), ''); + }); + + test('_computeHidePatchFile', () => { + const patchNum = '1'; + + const change1 = { + revisions: { + r1: {_number: 1, commit: {parents: []}}, + }, + }; + assert.isTrue(element._computeHidePatchFile(change1, patchNum)); + + const change2 = { + revisions: { + r1: {_number: 1, commit: {parents: [ + {commit: 'p1'}, + ]}}, + }, + }; + assert.isFalse(element._computeHidePatchFile(change2, patchNum)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-constants.js b/polygerrit-ui/app/elements/change/gr-file-list-constants.js index 8bdcf7a..0f93b52 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-constants.js +++ b/polygerrit-ui/app/elements/change/gr-file-list-constants.js
@@ -1,31 +1,29 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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. + */ +(function(window) { + 'use strict'; -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 + const GrFileListConstants = window.GrFileListConstants || {}; -http://www.apache.org/licenses/LICENSE-2.0 + GrFileListConstants.FilesExpandedState = { + ALL: 'all', + NONE: 'none', + SOME: 'some', + }; -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. ---> -<script> - (function(window) { - 'use strict'; - - const GrFileListConstants = window.GrFileListConstants || {}; - - GrFileListConstants.FilesExpandedState = { - ALL: 'all', - NONE: 'none', - SOME: 'some', - }; - - window.GrFileListConstants = GrFileListConstants; - })(window); -</script> + window.GrFileListConstants = GrFileListConstants; +})(window);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js index 34e1cfa..eef436b 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.js
@@ -14,264 +14,286 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Maximum length for patch set descriptions. - const PATCH_DESC_MAX_LENGTH = 500; - const MERGED_STATUS = 'MERGED'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../diff/gr-diff-mode-selector/gr-diff-mode-selector.js'; +import '../../diff/gr-patch-range-select/gr-patch-range-select.js'; +import '../../edit/gr-edit-controls/gr-edit-controls.js'; +import '../../shared/gr-editable-label/gr-editable-label.js'; +import '../../shared/gr-linked-chip/gr-linked-chip.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../gr-file-list-constants.js'; +import '../gr-commit-info/gr-commit-info.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-file-list-header_html.js'; + +// Maximum length for patch set descriptions. +const PATCH_DESC_MAX_LENGTH = 500; +const MERGED_STATUS = 'MERGED'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrFileListHeader extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-file-list-header'; } + /** + * @event expand-diffs + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * @event collapse-diffs */ - class GrFileListHeader extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-file-list-header'; } - /** - * @event expand-diffs - */ - /** - * @event collapse-diffs - */ + /** + * @event open-diff-prefs + */ - /** - * @event open-diff-prefs - */ + /** + * @event open-included-in-dialog + */ - /** - * @event open-included-in-dialog - */ + /** + * @event open-download-dialog + */ - /** - * @event open-download-dialog - */ + /** + * @event open-upload-help-dialog + */ - /** - * @event open-upload-help-dialog - */ + static get properties() { + return { + account: Object, + allPatchSets: Array, + /** @type {?} */ + change: Object, + changeNum: String, + changeUrl: String, + changeComments: Object, + commitInfo: Object, + editMode: Boolean, + loggedIn: Boolean, + serverConfig: Object, + shownFileCount: Number, + diffPrefs: Object, + diffPrefsDisabled: Boolean, + diffViewMode: { + type: String, + notify: true, + }, + patchNum: String, + basePatchNum: String, + filesExpanded: String, + // Caps the number of files that can be shown and have the 'show diffs' / + // 'hide diffs' buttons still be functional. + _maxFilesForBulkActions: { + type: Number, + readOnly: true, + value: 225, + }, + _patchsetDescription: { + type: String, + value: '', + }, + _descriptionReadOnly: { + type: Boolean, + computed: '_computeDescriptionReadOnly(loggedIn, change, account)', + }, + revisionInfo: Object, + }; + } - static get properties() { - return { - account: Object, - allPatchSets: Array, - /** @type {?} */ - change: Object, - changeNum: String, - changeUrl: String, - changeComments: Object, - commitInfo: Object, - editMode: Boolean, - loggedIn: Boolean, - serverConfig: Object, - shownFileCount: Number, - diffPrefs: Object, - diffPrefsDisabled: Boolean, - diffViewMode: { - type: String, - notify: true, - }, - patchNum: String, - basePatchNum: String, - filesExpanded: String, - // Caps the number of files that can be shown and have the 'show diffs' / - // 'hide diffs' buttons still be functional. - _maxFilesForBulkActions: { - type: Number, - readOnly: true, - value: 225, - }, - _patchsetDescription: { - type: String, - value: '', - }, - _descriptionReadOnly: { - type: Boolean, - computed: '_computeDescriptionReadOnly(loggedIn, change, account)', - }, - revisionInfo: Object, - }; + static get observers() { + return [ + '_computePatchSetDescription(change, patchNum)', + ]; + } + + setDiffViewMode(mode) { + this.$.modeSelect.setMode(mode); + } + + _expandAllDiffs() { + this._expanded = true; + this.fire('expand-diffs'); + } + + _collapseAllDiffs() { + this._expanded = false; + this.fire('collapse-diffs'); + } + + _computeExpandedClass(filesExpanded) { + const classes = []; + if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) { + classes.push('expanded'); + } + if (filesExpanded === GrFileListConstants.FilesExpandedState.SOME || + filesExpanded === GrFileListConstants.FilesExpandedState.ALL) { + classes.push('openFile'); + } + return classes.join(' '); + } + + _computeDescriptionPlaceholder(readOnly) { + return (readOnly ? 'No' : 'Add') + ' patchset description'; + } + + _computeDescriptionReadOnly(loggedIn, change, account) { + // Polymer 2: check for undefined + if ([loggedIn, change, account].some(arg => arg === undefined)) { + return undefined; } - static get observers() { - return [ - '_computePatchSetDescription(change, patchNum)', - ]; + return !(loggedIn && (account._account_id === change.owner._account_id)); + } + + _computePatchSetDescription(change, patchNum) { + // Polymer 2: check for undefined + if ([change, patchNum].some(arg => arg === undefined)) { + return; } - setDiffViewMode(mode) { - this.$.modeSelect.setMode(mode); - } + const rev = this.getRevisionByPatchNum(change.revisions, patchNum); + this._patchsetDescription = (rev && rev.description) ? + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + } - _expandAllDiffs() { - this._expanded = true; - this.fire('expand-diffs'); - } + _handleDescriptionRemoved(e) { + return this._updateDescription('', e); + } - _collapseAllDiffs() { - this._expanded = false; - this.fire('collapse-diffs'); - } - - _computeExpandedClass(filesExpanded) { - const classes = []; - if (filesExpanded === GrFileListConstants.FilesExpandedState.ALL) { - classes.push('expanded'); + /** + * @param {!Object} revisions The revisions object keyed by revision hashes + * @param {?Object} patchSet A revision already fetched from {revisions} + * @return {string|undefined} the SHA hash corresponding to the revision. + */ + _getPatchsetHash(revisions, patchSet) { + for (const rev in revisions) { + if (revisions.hasOwnProperty(rev) && + revisions[rev] === patchSet) { + return rev; } - if (filesExpanded === GrFileListConstants.FilesExpandedState.SOME || - filesExpanded === GrFileListConstants.FilesExpandedState.ALL) { - classes.push('openFile'); - } - return classes.join(' '); - } - - _computeDescriptionPlaceholder(readOnly) { - return (readOnly ? 'No' : 'Add') + ' patchset description'; - } - - _computeDescriptionReadOnly(loggedIn, change, account) { - // Polymer 2: check for undefined - if ([loggedIn, change, account].some(arg => arg === undefined)) { - return undefined; - } - - return !(loggedIn && (account._account_id === change.owner._account_id)); - } - - _computePatchSetDescription(change, patchNum) { - // Polymer 2: check for undefined - if ([change, patchNum].some(arg => arg === undefined)) { - return; - } - - const rev = this.getRevisionByPatchNum(change.revisions, patchNum); - this._patchsetDescription = (rev && rev.description) ? - rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; - } - - _handleDescriptionRemoved(e) { - return this._updateDescription('', e); - } - - /** - * @param {!Object} revisions The revisions object keyed by revision hashes - * @param {?Object} patchSet A revision already fetched from {revisions} - * @return {string|undefined} the SHA hash corresponding to the revision. - */ - _getPatchsetHash(revisions, patchSet) { - for (const rev in revisions) { - if (revisions.hasOwnProperty(rev) && - revisions[rev] === patchSet) { - return rev; - } - } - } - - _handleDescriptionChanged(e) { - const desc = e.detail.trim(); - this._updateDescription(desc, e); - } - - /** - * Update the patchset description with the rest API. - * - * @param {string} desc - * @param {?(Event|Node)} e - * @return {!Promise} - */ - _updateDescription(desc, e) { - const target = Polymer.dom(e).rootTarget; - if (target) { target.disabled = true; } - const rev = this.getRevisionByPatchNum(this.change.revisions, - this.patchNum); - const sha = this._getPatchsetHash(this.change.revisions, rev); - return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc) - .then(res => { - if (res.ok) { - if (target) { target.disabled = false; } - this.set(['change', 'revisions', sha, 'description'], desc); - this._patchsetDescription = desc; - } - }) - .catch(err => { - if (target) { target.disabled = false; } - return; - }); - } - - _computePrefsButtonHidden(prefs, diffPrefsDisabled) { - return diffPrefsDisabled || !prefs; - } - - _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) { - return shownFileCount <= maxFilesForBulkActions; - } - - _handlePatchChange(e) { - const {basePatchNum, patchNum} = e.detail; - if (this.patchNumEquals(basePatchNum, this.basePatchNum) && - this.patchNumEquals(patchNum, this.patchNum)) { return; } - Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum); - } - - _handlePrefsTap(e) { - e.preventDefault(); - this.fire('open-diff-prefs'); - } - - _handleIncludedInTap(e) { - e.preventDefault(); - this.fire('open-included-in-dialog'); - } - - _handleDownloadTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent( - new CustomEvent('open-download-dialog', {bubbles: false})); - } - - _computeEditModeClass(editMode) { - return editMode ? 'editMode' : ''; - } - - _computePatchInfoClass(patchNum, allPatchSets) { - const latestNum = this.computeLatestPatchNum(allPatchSets); - if (this.patchNumEquals(patchNum, latestNum)) { - return ''; - } - return 'patchInfoOldPatchSet'; - } - - _hideIncludedIn(change) { - return change && change.status === MERGED_STATUS ? '' : 'hide'; - } - - _handleUploadTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.dispatchEvent( - new CustomEvent('open-upload-help-dialog', {bubbles: false})); - } - - _computeUploadHelpContainerClass(change, account) { - const changeIsMerged = change && change.status === MERGED_STATUS; - const ownerId = change && change.owner && change.owner._account_id ? - change.owner._account_id : null; - const userId = account && account._account_id; - const userIsOwner = ownerId && userId && ownerId === userId; - const hideContainer = !userIsOwner || changeIsMerged; - return 'uploadContainer desktop' + (hideContainer ? ' hide' : ''); } } - customElements.define(GrFileListHeader.is, GrFileListHeader); -})(); + _handleDescriptionChanged(e) { + const desc = e.detail.trim(); + this._updateDescription(desc, e); + } + + /** + * Update the patchset description with the rest API. + * + * @param {string} desc + * @param {?(Event|Node)} e + * @return {!Promise} + */ + _updateDescription(desc, e) { + const target = dom(e).rootTarget; + if (target) { target.disabled = true; } + const rev = this.getRevisionByPatchNum(this.change.revisions, + this.patchNum); + const sha = this._getPatchsetHash(this.change.revisions, rev); + return this.$.restAPI.setDescription(this.changeNum, this.patchNum, desc) + .then(res => { + if (res.ok) { + if (target) { target.disabled = false; } + this.set(['change', 'revisions', sha, 'description'], desc); + this._patchsetDescription = desc; + } + }) + .catch(err => { + if (target) { target.disabled = false; } + return; + }); + } + + _computePrefsButtonHidden(prefs, diffPrefsDisabled) { + return diffPrefsDisabled || !prefs; + } + + _fileListActionsVisible(shownFileCount, maxFilesForBulkActions) { + return shownFileCount <= maxFilesForBulkActions; + } + + _handlePatchChange(e) { + const {basePatchNum, patchNum} = e.detail; + if (this.patchNumEquals(basePatchNum, this.basePatchNum) && + this.patchNumEquals(patchNum, this.patchNum)) { return; } + Gerrit.Nav.navigateToChange(this.change, patchNum, basePatchNum); + } + + _handlePrefsTap(e) { + e.preventDefault(); + this.fire('open-diff-prefs'); + } + + _handleIncludedInTap(e) { + e.preventDefault(); + this.fire('open-included-in-dialog'); + } + + _handleDownloadTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent( + new CustomEvent('open-download-dialog', {bubbles: false})); + } + + _computeEditModeClass(editMode) { + return editMode ? 'editMode' : ''; + } + + _computePatchInfoClass(patchNum, allPatchSets) { + const latestNum = this.computeLatestPatchNum(allPatchSets); + if (this.patchNumEquals(patchNum, latestNum)) { + return ''; + } + return 'patchInfoOldPatchSet'; + } + + _hideIncludedIn(change) { + return change && change.status === MERGED_STATUS ? '' : 'hide'; + } + + _handleUploadTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.dispatchEvent( + new CustomEvent('open-upload-help-dialog', {bubbles: false})); + } + + _computeUploadHelpContainerClass(change, account) { + const changeIsMerged = change && change.status === MERGED_STATUS; + const ownerId = change && change.owner && change.owner._account_id ? + change.owner._account_id : null; + const userId = account && account._account_id; + const userIsOwner = ownerId && userId && ownerId === userId; + const hideContainer = !userIsOwner || changeIsMerged; + return 'uploadContainer desktop' + (hideContainer ? ' hide' : ''); + } +} + +customElements.define(GrFileListHeader.is, GrFileListHeader);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js index 79f6c50..28cd645 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_html.js
@@ -1,39 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../diff/gr-diff-mode-selector/gr-diff-mode-selector.html"> -<link rel="import" href="../../diff/gr-patch-range-select/gr-patch-range-select.html"> -<link rel="import" href="../../edit/gr-edit-controls/gr-edit-controls.html"> -<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html"> -<link rel="import" href="../../shared/gr-linked-chip/gr-linked-chip.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../gr-file-list-constants.html"> -<link rel="import" href="../gr-commit-info/gr-commit-info.html"> - -<dom-module id="gr-file-list-header"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .prefsButton { float: right; @@ -152,96 +135,49 @@ } } </style> - <div class$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]"> + <div class\$="patchInfo-header [[_computeEditModeClass(editMode)]] [[_computePatchInfoClass(patchNum, allPatchSets)]]"> <div class="patchInfo-left"> <div class="patchInfoContent"> - <gr-patch-range-select - id="rangeSelect" - change-comments="[[changeComments]]" - change-num="[[changeNum]]" - patch-num="[[patchNum]]" - base-patch-num="[[basePatchNum]]" - available-patches="[[allPatchSets]]" - revisions="[[change.revisions]]" - revision-info="[[revisionInfo]]" - on-patch-range-change="_handlePatchChange"> + <gr-patch-range-select id="rangeSelect" change-comments="[[changeComments]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" base-patch-num="[[basePatchNum]]" available-patches="[[allPatchSets]]" revisions="[[change.revisions]]" revision-info="[[revisionInfo]]" on-patch-range-change="_handlePatchChange"> </gr-patch-range-select> <span class="separator"></span> - <gr-commit-info - change="[[change]]" - server-config="[[serverConfig]]" - commit-info="[[commitInfo]]"></gr-commit-info> + <gr-commit-info change="[[change]]" server-config="[[serverConfig]]" commit-info="[[commitInfo]]"></gr-commit-info> <span class="container latestPatchContainer"> <span class="separator"></span> - <a href$="[[changeUrl]]">Go to latest patch set</a> + <a href\$="[[changeUrl]]">Go to latest patch set</a> </span> <span class="container descriptionContainer hideOnEdit"> <span class="separator"></span> - <template - is="dom-if" - if="[[_patchsetDescription]]"> - <gr-linked-chip - id="descriptionChip" - text="[[_patchsetDescription]]" - removable="[[!_descriptionReadOnly]]" - on-remove="_handleDescriptionRemoved"></gr-linked-chip> + <template is="dom-if" if="[[_patchsetDescription]]"> + <gr-linked-chip id="descriptionChip" text="[[_patchsetDescription]]" removable="[[!_descriptionReadOnly]]" on-remove="_handleDescriptionRemoved"></gr-linked-chip> </template> - <template - is="dom-if" - if="[[!_patchsetDescription]]"> - <gr-editable-label - id="descriptionLabel" - uppercase - class="descriptionLabel" - label-text="Add patchset description" - value="[[_patchsetDescription]]" - placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]" - read-only="[[_descriptionReadOnly]]" - on-changed="_handleDescriptionChanged"></gr-editable-label> + <template is="dom-if" if="[[!_patchsetDescription]]"> + <gr-editable-label id="descriptionLabel" uppercase="" class="descriptionLabel" label-text="Add patchset description" value="[[_patchsetDescription]]" placeholder="[[_computeDescriptionPlaceholder(_descriptionReadOnly)]]" read-only="[[_descriptionReadOnly]]" on-changed="_handleDescriptionChanged"></gr-editable-label> </template> </span> </div> </div> - <div class$="rightControls [[_computeExpandedClass(filesExpanded)]]"> + <div class\$="rightControls [[_computeExpandedClass(filesExpanded)]]"> <span class="showOnEdit flexContainer"> - <gr-edit-controls - id="editControls" - patch-num="[[patchNum]]" - change="[[change]]"></gr-edit-controls> + <gr-edit-controls id="editControls" patch-num="[[patchNum]]" change="[[change]]"></gr-edit-controls> <span class="separator"></span> </span> - <span class$="[[_computeUploadHelpContainerClass(change, account)]]"> - <gr-button link - class="upload" - on-click="_handleUploadTap">Update Change</gr-button> + <span class\$="[[_computeUploadHelpContainerClass(change, account)]]"> + <gr-button link="" class="upload" on-click="_handleUploadTap">Update Change</gr-button> </span> <span class="downloadContainer desktop"> - <gr-button link - class="download" - title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG, - ShortcutSection.ACTIONS)]]" - on-click="_handleDownloadTap">Download</gr-button> + <gr-button link="" class="download" title="[[createTitle(Shortcut.OPEN_DOWNLOAD_DIALOG, + ShortcutSection.ACTIONS)]]" on-click="_handleDownloadTap">Download</gr-button> </span> - <span class$="includedInContainer [[_hideIncludedIn(change)]] desktop"> - <gr-button link - class="includedIn" - on-click="_handleIncludedInTap">Included In</gr-button> + <span class\$="includedInContainer [[_hideIncludedIn(change)]] desktop"> + <gr-button link="" class="includedIn" on-click="_handleIncludedInTap">Included In</gr-button> </span> - <template is="dom-if" - if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"> - <gr-button - id="expandBtn" - link - title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT, - ShortcutSection.DIFFS)]]" - on-click="_expandAllDiffs">Expand All</gr-button> - <gr-button - id="collapseBtn" - link - on-click="_collapseAllDiffs">Collapse All</gr-button> + <template is="dom-if" if="[[_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"> + <gr-button id="expandBtn" link="" title="[[createTitle(Shortcut.EXPAND_ALL_DIFF_CONTEXT, + ShortcutSection.DIFFS)]]" on-click="_expandAllDiffs">Expand All</gr-button> + <gr-button id="collapseBtn" link="" on-click="_collapseAllDiffs">Collapse All</gr-button> </template> - <template is="dom-if" - if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"> + <template is="dom-if" if="[[!_fileListActionsVisible(shownFileCount, _maxFilesForBulkActions)]]"> <div class="warning"> Bulk actions disabled because there are too many files. </div> @@ -249,25 +185,12 @@ <div class="fileViewActions"> <span class="separator"></span> <span class="fileViewActionsLabel">Diff view:</span> - <gr-diff-mode-selector - id="modeSelect" - mode="{{diffViewMode}}" - save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector> - <span id="diffPrefsContainer" - class="hideOnEdit" - hidden$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]" - hidden> - <gr-button - link - has-tooltip - title="Diff preferences" - class="prefsButton desktop" - on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button> + <gr-diff-mode-selector id="modeSelect" mode="{{diffViewMode}}" save-on-change="[[!diffPrefsDisabled]]"></gr-diff-mode-selector> + <span id="diffPrefsContainer" class="hideOnEdit" hidden\$="[[_computePrefsButtonHidden(diffPrefs, diffPrefsDisabled)]]" hidden=""> + <gr-button link="" has-tooltip="" title="Diff preferences" class="prefsButton desktop" on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button> </span> </div> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-file-list-header.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html index f8a4e87..32ecb14 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html +++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.html
@@ -19,17 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-file-list-header</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="gr-file-list-header.html"> +<script type="module" src="./gr-file-list-header.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-file-list-header.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -43,281 +48,284 @@ </template> </test-fixture> -<script> - suite('gr-file-list-header tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-file-list-header.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-file-list-header tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({test: 'config'}); }, - getAccount() { return Promise.resolve(null); }, - _fetchSharedCacheURL() { return Promise.resolve({}); }, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({test: 'config'}); }, + getAccount() { return Promise.resolve(null); }, + _fetchSharedCacheURL() { return Promise.resolve({}); }, }); + element = fixture('basic'); + }); - teardown(done => { - flush(() => { - sandbox.restore(); - done(); - }); - }); - - test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => { - element.diffPrefsDisabled = true; - flushAsynchronousOperations(); - assert.isTrue(element.$.diffPrefsContainer.hidden); - - element.diffPrefsDisabled = false; - flushAsynchronousOperations(); - assert.isTrue(element.$.diffPrefsContainer.hidden); - - element.diffPrefsDisabled = true; - element.diffPrefs = {font_size: '12'}; - flushAsynchronousOperations(); - assert.isTrue(element.$.diffPrefsContainer.hidden); - - element.diffPrefsDisabled = false; - flushAsynchronousOperations(); - assert.isFalse(element.$.diffPrefsContainer.hidden); - }); - - test('_computeDescriptionReadOnly', () => { - assert.equal(element._computeDescriptionReadOnly(false, - {owner: {_account_id: 1}}, {_account_id: 1}), true); - assert.equal(element._computeDescriptionReadOnly(true, - {owner: {_account_id: 0}}, {_account_id: 1}), true); - assert.equal(element._computeDescriptionReadOnly(true, - {owner: {_account_id: 1}}, {_account_id: 1}), false); - }); - - test('_computeDescriptionPlaceholder', () => { - assert.equal(element._computeDescriptionPlaceholder(true), - 'No patchset description'); - assert.equal(element._computeDescriptionPlaceholder(false), - 'Add patchset description'); - }); - - test('description editing', () => { - const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription') - .returns(Promise.resolve({ok: true})); - - element.changeNum = '42'; - element.basePatchNum = 'PARENT'; - element.patchNum = 1; - - element.change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - revisions: { - rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}}, - }, - current_revision: 'rev1', - status: 'NEW', - labels: {}, - actions: {}, - owner: {_account_id: 1}, - }; - element.account = {_account_id: 1}; - element.loggedIn = true; - - flushAsynchronousOperations(); - - // The element has a description, so the account chip should be visible - // and the description label should not exist. - const chip = Polymer.dom(element.root).querySelector('#descriptionChip'); - let label = Polymer.dom(element.root).querySelector('#descriptionLabel'); - - assert.equal(chip.text, 'test'); - assert.isNotOk(label); - - // Simulate tapping the remove button, but call function directly so that - // can determine what happens after the promise is resolved. - return element._handleDescriptionRemoved() - .then(() => { - // The API stub should be called with an empty string for the new - // description. - assert.equal(putDescStub.lastCall.args[2], ''); - assert.equal(element.change.revisions.rev1.description, ''); - - flushAsynchronousOperations(); - // The editable label should now be visible and the chip hidden. - label = Polymer.dom(element.root).querySelector('#descriptionLabel'); - assert.isOk(label); - assert.equal(getComputedStyle(chip).display, 'none'); - assert.notEqual(getComputedStyle(label).display, 'none'); - assert.isFalse(label.readOnly); - // Edit the label to have a new value of test2, and save. - label.editing = true; - label._inputText = 'test2'; - label._save(); - flushAsynchronousOperations(); - // The API stub should be called with an `test2` for the new - // description. - assert.equal(putDescStub.callCount, 2); - assert.equal(putDescStub.lastCall.args[2], 'test2'); - }) - .then(() => { - flushAsynchronousOperations(); - // The chip should be visible again, and the label hidden. - assert.equal(element.change.revisions.rev1.description, 'test2'); - assert.equal(getComputedStyle(label).display, 'none'); - assert.notEqual(getComputedStyle(chip).display, 'none'); - }); - }); - - test('expandAllDiffs called when expand button clicked', () => { - element.shownFileCount = 1; - flushAsynchronousOperations(); - sandbox.stub(element, '_expandAllDiffs'); - MockInteractions.tap(Polymer.dom(element.root).querySelector( - '#expandBtn')); - assert.isTrue(element._expandAllDiffs.called); - }); - - test('collapseAllDiffs called when expand button clicked', () => { - element.shownFileCount = 1; - flushAsynchronousOperations(); - sandbox.stub(element, '_collapseAllDiffs'); - MockInteractions.tap(Polymer.dom(element.root).querySelector( - '#collapseBtn')); - assert.isTrue(element._collapseAllDiffs.called); - }); - - test('show/hide diffs disabled for large amounts of files', done => { - const computeSpy = sandbox.spy(element, '_fileListActionsVisible'); - element._files = []; - element.changeNum = '42'; - element.basePatchNum = 'PARENT'; - element.patchNum = '2'; - element.shownFileCount = 1; - flush(() => { - assert.isTrue(computeSpy.lastCall.returnValue); - _.times(element._maxFilesForBulkActions + 1, () => { - element.shownFileCount = element.shownFileCount + 1; - }); - assert.isFalse(computeSpy.lastCall.returnValue); - done(); - }); - }); - - test('fileViewActions are properly hidden', () => { - const actions = element.shadowRoot - .querySelector('.fileViewActions'); - assert.equal(getComputedStyle(actions).display, 'none'); - element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME; - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(actions).display, 'none'); - element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL; - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(actions).display, 'none'); - element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE; - flushAsynchronousOperations(); - assert.equal(getComputedStyle(actions).display, 'none'); - }); - - test('expand/collapse buttons are toggled correctly', () => { - element.shownFileCount = 10; - flushAsynchronousOperations(); - const expandBtn = element.shadowRoot.querySelector('#expandBtn'); - const collapseBtn = element.shadowRoot.querySelector('#collapseBtn'); - assert.notEqual(getComputedStyle(expandBtn).display, 'none'); - assert.equal(getComputedStyle(collapseBtn).display, 'none'); - element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME; - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(expandBtn).display, 'none'); - assert.equal(getComputedStyle(collapseBtn).display, 'none'); - element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL; - flushAsynchronousOperations(); - assert.equal(getComputedStyle(expandBtn).display, 'none'); - assert.notEqual(getComputedStyle(collapseBtn).display, 'none'); - element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE; - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(expandBtn).display, 'none'); - assert.equal(getComputedStyle(collapseBtn).display, 'none'); - }); - - test('navigateToChange called when range select changes', () => { - const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - element.change = { - change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', - revisions: { - rev2: {_number: 2}, - rev1: {_number: 1}, - rev13: {_number: 13}, - rev3: {_number: 3}, - }, - status: 'NEW', - labels: {}, - }; - element.basePatchNum = 1; - element.patchNum = 2; - - element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}}); - assert.equal(navigateToChangeStub.callCount, 1); - assert.isTrue(navigateToChangeStub.lastCall - .calledWithExactly(element.change, 3, 1)); - }); - - test('class is applied to file list on old patch set', () => { - const allPatchSets = [{num: 4}, {num: 2}, {num: 1}]; - assert.equal(element._computePatchInfoClass('1', allPatchSets), - 'patchInfoOldPatchSet'); - assert.equal(element._computePatchInfoClass('2', allPatchSets), - 'patchInfoOldPatchSet'); - assert.equal(element._computePatchInfoClass('4', allPatchSets), ''); - }); - - suite('editMode behavior', () => { - setup(() => { - element.diffPrefsDisabled = false; - element.diffPrefs = {}; - }); - - const isVisible = el => { - assert.ok(el); - return getComputedStyle(el).getPropertyValue('display') !== 'none'; - }; - - test('patch specific elements', () => { - element.editMode = true; - sandbox.stub(element, 'computeLatestPatchNum').returns('2'); - flushAsynchronousOperations(); - - assert.isFalse(isVisible(element.$.diffPrefsContainer)); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.descriptionContainer'))); - - element.editMode = false; - flushAsynchronousOperations(); - - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.descriptionContainer'))); - assert.isTrue(isVisible(element.$.diffPrefsContainer)); - }); - - test('edit-controls visibility', () => { - element.editMode = true; - flushAsynchronousOperations(); - assert.isTrue(isVisible(element.$.editControls.parentElement)); - - element.editMode = false; - flushAsynchronousOperations(); - assert.isFalse(isVisible(element.$.editControls.parentElement)); - }); - - test('_computeUploadHelpContainerClass', () => { - // Only show the upload helper button when an unmerged change is viewed - // by its owner. - const accountA = {_account_id: 1}; - const accountB = {_account_id: 2}; - assert.notInclude(element._computeUploadHelpContainerClass( - {owner: accountA}, accountA), 'hide'); - assert.include(element._computeUploadHelpContainerClass( - {owner: accountA}, accountB), 'hide'); - }); + teardown(done => { + flush(() => { + sandbox.restore(); + done(); }); }); + + test('Diff preferences hidden when no prefs or diffPrefsDisabled', () => { + element.diffPrefsDisabled = true; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element.diffPrefsDisabled = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element.diffPrefsDisabled = true; + element.diffPrefs = {font_size: '12'}; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element.diffPrefsDisabled = false; + flushAsynchronousOperations(); + assert.isFalse(element.$.diffPrefsContainer.hidden); + }); + + test('_computeDescriptionReadOnly', () => { + assert.equal(element._computeDescriptionReadOnly(false, + {owner: {_account_id: 1}}, {_account_id: 1}), true); + assert.equal(element._computeDescriptionReadOnly(true, + {owner: {_account_id: 0}}, {_account_id: 1}), true); + assert.equal(element._computeDescriptionReadOnly(true, + {owner: {_account_id: 1}}, {_account_id: 1}), false); + }); + + test('_computeDescriptionPlaceholder', () => { + assert.equal(element._computeDescriptionPlaceholder(true), + 'No patchset description'); + assert.equal(element._computeDescriptionPlaceholder(false), + 'Add patchset description'); + }); + + test('description editing', () => { + const putDescStub = sandbox.stub(element.$.restAPI, 'setDescription') + .returns(Promise.resolve({ok: true})); + + element.changeNum = '42'; + element.basePatchNum = 'PARENT'; + element.patchNum = 1; + + element.change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev1: {_number: 1, description: 'test', commit: {commit: 'rev1'}}, + }, + current_revision: 'rev1', + status: 'NEW', + labels: {}, + actions: {}, + owner: {_account_id: 1}, + }; + element.account = {_account_id: 1}; + element.loggedIn = true; + + flushAsynchronousOperations(); + + // The element has a description, so the account chip should be visible + // and the description label should not exist. + const chip = dom(element.root).querySelector('#descriptionChip'); + let label = dom(element.root).querySelector('#descriptionLabel'); + + assert.equal(chip.text, 'test'); + assert.isNotOk(label); + + // Simulate tapping the remove button, but call function directly so that + // can determine what happens after the promise is resolved. + return element._handleDescriptionRemoved() + .then(() => { + // The API stub should be called with an empty string for the new + // description. + assert.equal(putDescStub.lastCall.args[2], ''); + assert.equal(element.change.revisions.rev1.description, ''); + + flushAsynchronousOperations(); + // The editable label should now be visible and the chip hidden. + label = dom(element.root).querySelector('#descriptionLabel'); + assert.isOk(label); + assert.equal(getComputedStyle(chip).display, 'none'); + assert.notEqual(getComputedStyle(label).display, 'none'); + assert.isFalse(label.readOnly); + // Edit the label to have a new value of test2, and save. + label.editing = true; + label._inputText = 'test2'; + label._save(); + flushAsynchronousOperations(); + // The API stub should be called with an `test2` for the new + // description. + assert.equal(putDescStub.callCount, 2); + assert.equal(putDescStub.lastCall.args[2], 'test2'); + }) + .then(() => { + flushAsynchronousOperations(); + // The chip should be visible again, and the label hidden. + assert.equal(element.change.revisions.rev1.description, 'test2'); + assert.equal(getComputedStyle(label).display, 'none'); + assert.notEqual(getComputedStyle(chip).display, 'none'); + }); + }); + + test('expandAllDiffs called when expand button clicked', () => { + element.shownFileCount = 1; + flushAsynchronousOperations(); + sandbox.stub(element, '_expandAllDiffs'); + MockInteractions.tap(dom(element.root).querySelector( + '#expandBtn')); + assert.isTrue(element._expandAllDiffs.called); + }); + + test('collapseAllDiffs called when expand button clicked', () => { + element.shownFileCount = 1; + flushAsynchronousOperations(); + sandbox.stub(element, '_collapseAllDiffs'); + MockInteractions.tap(dom(element.root).querySelector( + '#collapseBtn')); + assert.isTrue(element._collapseAllDiffs.called); + }); + + test('show/hide diffs disabled for large amounts of files', done => { + const computeSpy = sandbox.spy(element, '_fileListActionsVisible'); + element._files = []; + element.changeNum = '42'; + element.basePatchNum = 'PARENT'; + element.patchNum = '2'; + element.shownFileCount = 1; + flush(() => { + assert.isTrue(computeSpy.lastCall.returnValue); + _.times(element._maxFilesForBulkActions + 1, () => { + element.shownFileCount = element.shownFileCount + 1; + }); + assert.isFalse(computeSpy.lastCall.returnValue); + done(); + }); + }); + + test('fileViewActions are properly hidden', () => { + const actions = element.shadowRoot + .querySelector('.fileViewActions'); + assert.equal(getComputedStyle(actions).display, 'none'); + element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME; + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(actions).display, 'none'); + element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL; + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(actions).display, 'none'); + element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE; + flushAsynchronousOperations(); + assert.equal(getComputedStyle(actions).display, 'none'); + }); + + test('expand/collapse buttons are toggled correctly', () => { + element.shownFileCount = 10; + flushAsynchronousOperations(); + const expandBtn = element.shadowRoot.querySelector('#expandBtn'); + const collapseBtn = element.shadowRoot.querySelector('#collapseBtn'); + assert.notEqual(getComputedStyle(expandBtn).display, 'none'); + assert.equal(getComputedStyle(collapseBtn).display, 'none'); + element.filesExpanded = GrFileListConstants.FilesExpandedState.SOME; + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(expandBtn).display, 'none'); + assert.equal(getComputedStyle(collapseBtn).display, 'none'); + element.filesExpanded = GrFileListConstants.FilesExpandedState.ALL; + flushAsynchronousOperations(); + assert.equal(getComputedStyle(expandBtn).display, 'none'); + assert.notEqual(getComputedStyle(collapseBtn).display, 'none'); + element.filesExpanded = GrFileListConstants.FilesExpandedState.NONE; + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(expandBtn).display, 'none'); + assert.equal(getComputedStyle(collapseBtn).display, 'none'); + }); + + test('navigateToChange called when range select changes', () => { + const navigateToChangeStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + element.change = { + change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca', + revisions: { + rev2: {_number: 2}, + rev1: {_number: 1}, + rev13: {_number: 13}, + rev3: {_number: 3}, + }, + status: 'NEW', + labels: {}, + }; + element.basePatchNum = 1; + element.patchNum = 2; + + element._handlePatchChange({detail: {basePatchNum: 1, patchNum: 3}}); + assert.equal(navigateToChangeStub.callCount, 1); + assert.isTrue(navigateToChangeStub.lastCall + .calledWithExactly(element.change, 3, 1)); + }); + + test('class is applied to file list on old patch set', () => { + const allPatchSets = [{num: 4}, {num: 2}, {num: 1}]; + assert.equal(element._computePatchInfoClass('1', allPatchSets), + 'patchInfoOldPatchSet'); + assert.equal(element._computePatchInfoClass('2', allPatchSets), + 'patchInfoOldPatchSet'); + assert.equal(element._computePatchInfoClass('4', allPatchSets), ''); + }); + + suite('editMode behavior', () => { + setup(() => { + element.diffPrefsDisabled = false; + element.diffPrefs = {}; + }); + + const isVisible = el => { + assert.ok(el); + return getComputedStyle(el).getPropertyValue('display') !== 'none'; + }; + + test('patch specific elements', () => { + element.editMode = true; + sandbox.stub(element, 'computeLatestPatchNum').returns('2'); + flushAsynchronousOperations(); + + assert.isFalse(isVisible(element.$.diffPrefsContainer)); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.descriptionContainer'))); + + element.editMode = false; + flushAsynchronousOperations(); + + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.descriptionContainer'))); + assert.isTrue(isVisible(element.$.diffPrefsContainer)); + }); + + test('edit-controls visibility', () => { + element.editMode = true; + flushAsynchronousOperations(); + assert.isTrue(isVisible(element.$.editControls.parentElement)); + + element.editMode = false; + flushAsynchronousOperations(); + assert.isFalse(isVisible(element.$.editControls.parentElement)); + }); + + test('_computeUploadHelpContainerClass', () => { + // Only show the upload helper button when an unmerged change is viewed + // by its owner. + const accountA = {_account_id: 1}; + const accountB = {_account_id: 2}; + assert.notInclude(element._computeUploadHelpContainerClass( + {owner: accountA}, accountA), 'hide'); + assert.include(element._computeUploadHelpContainerClass( + {owner: accountA}, accountB), 'hide'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js index d2f11c2..858fc05 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,1359 +14,1388 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Maximum length for patch set descriptions. - const PATCH_DESC_MAX_LENGTH = 500; - const WARN_SHOW_ALL_THRESHOLD = 1000; - const LOADING_DEBOUNCE_INTERVAL = 100; +import '../../../behaviors/async-foreach-behavior/async-foreach-behavior.js'; +import '../../../behaviors/dom-util-behavior/dom-util-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../diff/gr-diff-cursor/gr-diff-cursor.js'; +import '../../diff/gr-diff-host/gr-diff-host.js'; +import '../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js'; +import '../../edit/gr-edit-file-controls/gr-edit-file-controls.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-cursor-manager/gr-cursor-manager.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-linked-text/gr-linked-text.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js'; +import '../../shared/gr-tooltip-content/gr-tooltip-content.js'; +import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js'; +import '../gr-file-list-constants.js'; +import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-file-list_html.js'; - const SIZE_BAR_MAX_WIDTH = 61; - const SIZE_BAR_GAP_WIDTH = 1; - const SIZE_BAR_MIN_WIDTH = 1.5; +// Maximum length for patch set descriptions. +const PATCH_DESC_MAX_LENGTH = 500; +const WARN_SHOW_ALL_THRESHOLD = 1000; +const LOADING_DEBOUNCE_INTERVAL = 100; - const RENDER_TIMING_LABEL = 'FileListRenderTime'; - const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile'; - const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs'; - const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff'; +const SIZE_BAR_MAX_WIDTH = 61; +const SIZE_BAR_GAP_WIDTH = 1; +const SIZE_BAR_MIN_WIDTH = 1.5; - const FileStatus = { - A: 'Added', - C: 'Copied', - D: 'Deleted', - M: 'Modified', - R: 'Renamed', - W: 'Rewritten', - U: 'Unchanged', - }; +const RENDER_TIMING_LABEL = 'FileListRenderTime'; +const RENDER_AVG_TIMING_LABEL = 'FileListRenderTimePerFile'; +const EXPAND_ALL_TIMING_LABEL = 'ExpandAllDiffs'; +const EXPAND_ALL_AVG_TIMING_LABEL = 'ExpandAllPerDiff'; + +const FileStatus = { + A: 'Added', + C: 'Copied', + D: 'Deleted', + M: 'Modified', + R: 'Renamed', + W: 'Rewritten', + U: 'Unchanged', +}; + +/** + * @appliesMixin Gerrit.AsyncForeachMixin + * @appliesMixin Gerrit.DomUtilMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.PathListMixin + * @extends Polymer.Element + */ +class GrFileList extends mixinBehaviors( [ + Gerrit.AsyncForeachBehavior, + Gerrit.DomUtilBehavior, + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.PathListBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-file-list'; } + /** + * Fired when a draft refresh should get triggered + * + * @event reload-drafts + */ + + static get properties() { + return { + /** @type {?} */ + patchRange: Object, + patchNum: String, + changeNum: String, + /** @type {?} */ + changeComments: Object, + drafts: Object, + revisions: Array, + projectConfig: Object, + selectedIndex: { + type: Number, + notify: true, + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + /** @type {?} */ + change: Object, + diffViewMode: { + type: String, + notify: true, + observer: '_updateDiffPreferences', + }, + editMode: { + type: Boolean, + observer: '_editModeChanged', + }, + filesExpanded: { + type: String, + value: GrFileListConstants.FilesExpandedState.NONE, + notify: true, + }, + _filesByPath: Object, + _files: { + type: Array, + observer: '_filesChanged', + value() { return []; }, + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _reviewed: { + type: Array, + value() { return []; }, + }, + diffPrefs: { + type: Object, + notify: true, + observer: '_updateDiffPreferences', + }, + /** @type {?} */ + _userPrefs: Object, + _showInlineDiffs: Boolean, + numFilesShown: { + type: Number, + notify: true, + }, + /** @type {?} */ + _patchChange: { + type: Object, + computed: '_calculatePatchChange(_files)', + }, + fileListIncrement: Number, + _hideChangeTotals: { + type: Boolean, + computed: '_shouldHideChangeTotals(_patchChange)', + }, + _hideBinaryChangeTotals: { + type: Boolean, + computed: '_shouldHideBinaryChangeTotals(_patchChange)', + }, + + _shownFiles: { + type: Array, + computed: '_computeFilesShown(numFilesShown, _files)', + }, + + /** + * The amount of files added to the shown files list the last time it was + * updated. This is used for reporting the average render time. + */ + _reportinShownFilesIncrement: Number, + + _expandedFilePaths: { + type: Array, + value() { return []; }, + }, + _displayLine: Boolean, + _loading: { + type: Boolean, + observer: '_loadingChanged', + }, + /** @type {Gerrit.LayoutStats|undefined} */ + _sizeBarLayout: { + type: Object, + computed: '_computeSizeBarLayout(_shownFiles.*)', + }, + + _showSizeBars: { + type: Boolean, + value: true, + computed: '_computeShowSizeBars(_userPrefs)', + }, + + /** @type {Function} */ + _cancelForEachDiff: Function, + + _showDynamicColumns: { + type: Boolean, + computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' + + '_dynamicContentEndpoints, _dynamicSummaryEndpoints)', + }, + /** @type {Array<string>} */ + _dynamicHeaderEndpoints: { + type: Array, + }, + /** @type {Array<string>} */ + _dynamicContentEndpoints: { + type: Array, + }, + /** @type {Array<string>} */ + _dynamicSummaryEndpoints: { + type: Array, + }, + }; + } + + static get observers() { + return [ + '_expandedPathsChanged(_expandedFilePaths.splices)', + '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' + + '_loading)', + ]; + } + + get keyBindings() { + return { + esc: '_handleEscKey', + }; + } + + keyboardShortcuts() { + return { + [this.Shortcut.LEFT_PANE]: '_handleLeftPane', + [this.Shortcut.RIGHT_PANE]: '_handleRightPane', + [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff', + [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs', + [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext', + [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev', + [this.Shortcut.NEXT_LINE]: '_handleCursorNext', + [this.Shortcut.PREV_LINE]: '_handleCursorPrev', + [this.Shortcut.NEW_COMMENT]: '_handleNewComment', + [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile', + [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile', + [this.Shortcut.OPEN_FILE]: '_handleOpenFile', + [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk', + [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk', + [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed', + [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane', + + // Final two are actually handled by gr-comment-thread. + [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null, + [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null, + }; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('keydown', + e => this._scopedKeydownHandler(e)); + } + + /** @override */ + attached() { + super.attached(); + Gerrit.awaitPluginsLoaded().then(() => { + this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints( + 'change-view-file-list-header'); + this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints( + 'change-view-file-list-content'); + this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints( + 'change-view-file-list-summary'); + + if (this._dynamicHeaderEndpoints.length !== + this._dynamicContentEndpoints.length) { + console.warn( + 'Different number of dynamic file-list header and content.'); + } + if (this._dynamicHeaderEndpoints.length !== + this._dynamicSummaryEndpoints.length) { + console.warn( + 'Different number of dynamic file-list headers and summary.'); + } + }); + } + + /** @override */ + detached() { + super.detached(); + this._cancelDiffs(); + } /** - * @appliesMixin Gerrit.AsyncForeachMixin - * @appliesMixin Gerrit.DomUtilMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.PathListMixin - * @extends Polymer.Element + * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard + * events must be scoped to a component level (e.g. `enter`) in order to not + * override native browser functionality. + * + * Context: Issue 7277 */ - class GrFileList extends Polymer.mixinBehaviors( [ - Gerrit.AsyncForeachBehavior, - Gerrit.DomUtilBehavior, - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.PathListBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-file-list'; } - /** - * Fired when a draft refresh should get triggered - * - * @event reload-drafts - */ - - static get properties() { - return { - /** @type {?} */ - patchRange: Object, - patchNum: String, - changeNum: String, - /** @type {?} */ - changeComments: Object, - drafts: Object, - revisions: Array, - projectConfig: Object, - selectedIndex: { - type: Number, - notify: true, - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - /** @type {?} */ - change: Object, - diffViewMode: { - type: String, - notify: true, - observer: '_updateDiffPreferences', - }, - editMode: { - type: Boolean, - observer: '_editModeChanged', - }, - filesExpanded: { - type: String, - value: GrFileListConstants.FilesExpandedState.NONE, - notify: true, - }, - _filesByPath: Object, - _files: { - type: Array, - observer: '_filesChanged', - value() { return []; }, - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _reviewed: { - type: Array, - value() { return []; }, - }, - diffPrefs: { - type: Object, - notify: true, - observer: '_updateDiffPreferences', - }, - /** @type {?} */ - _userPrefs: Object, - _showInlineDiffs: Boolean, - numFilesShown: { - type: Number, - notify: true, - }, - /** @type {?} */ - _patchChange: { - type: Object, - computed: '_calculatePatchChange(_files)', - }, - fileListIncrement: Number, - _hideChangeTotals: { - type: Boolean, - computed: '_shouldHideChangeTotals(_patchChange)', - }, - _hideBinaryChangeTotals: { - type: Boolean, - computed: '_shouldHideBinaryChangeTotals(_patchChange)', - }, - - _shownFiles: { - type: Array, - computed: '_computeFilesShown(numFilesShown, _files)', - }, - - /** - * The amount of files added to the shown files list the last time it was - * updated. This is used for reporting the average render time. - */ - _reportinShownFilesIncrement: Number, - - _expandedFilePaths: { - type: Array, - value() { return []; }, - }, - _displayLine: Boolean, - _loading: { - type: Boolean, - observer: '_loadingChanged', - }, - /** @type {Gerrit.LayoutStats|undefined} */ - _sizeBarLayout: { - type: Object, - computed: '_computeSizeBarLayout(_shownFiles.*)', - }, - - _showSizeBars: { - type: Boolean, - value: true, - computed: '_computeShowSizeBars(_userPrefs)', - }, - - /** @type {Function} */ - _cancelForEachDiff: Function, - - _showDynamicColumns: { - type: Boolean, - computed: '_computeShowDynamicColumns(_dynamicHeaderEndpoints, ' + - '_dynamicContentEndpoints, _dynamicSummaryEndpoints)', - }, - /** @type {Array<string>} */ - _dynamicHeaderEndpoints: { - type: Array, - }, - /** @type {Array<string>} */ - _dynamicContentEndpoints: { - type: Array, - }, - /** @type {Array<string>} */ - _dynamicSummaryEndpoints: { - type: Array, - }, - }; - } - - static get observers() { - return [ - '_expandedPathsChanged(_expandedFilePaths.splices)', - '_computeFiles(_filesByPath, changeComments, patchRange, _reviewed, ' + - '_loading)', - ]; - } - - get keyBindings() { - return { - esc: '_handleEscKey', - }; - } - - keyboardShortcuts() { - return { - [this.Shortcut.LEFT_PANE]: '_handleLeftPane', - [this.Shortcut.RIGHT_PANE]: '_handleRightPane', - [this.Shortcut.TOGGLE_INLINE_DIFF]: '_handleToggleInlineDiff', - [this.Shortcut.TOGGLE_ALL_INLINE_DIFFS]: '_handleToggleAllInlineDiffs', - [this.Shortcut.CURSOR_NEXT_FILE]: '_handleCursorNext', - [this.Shortcut.CURSOR_PREV_FILE]: '_handleCursorPrev', - [this.Shortcut.NEXT_LINE]: '_handleCursorNext', - [this.Shortcut.PREV_LINE]: '_handleCursorPrev', - [this.Shortcut.NEW_COMMENT]: '_handleNewComment', - [this.Shortcut.OPEN_LAST_FILE]: '_handleOpenLastFile', - [this.Shortcut.OPEN_FIRST_FILE]: '_handleOpenFirstFile', - [this.Shortcut.OPEN_FILE]: '_handleOpenFile', - [this.Shortcut.NEXT_CHUNK]: '_handleNextChunk', - [this.Shortcut.PREV_CHUNK]: '_handlePrevChunk', - [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed', - [this.Shortcut.TOGGLE_LEFT_PANE]: '_handleToggleLeftPane', - - // Final two are actually handled by gr-comment-thread. - [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null, - [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null, - }; - } - - /** @override */ - created() { - super.created(); - this.addEventListener('keydown', - e => this._scopedKeydownHandler(e)); - } - - /** @override */ - attached() { - super.attached(); - Gerrit.awaitPluginsLoaded().then(() => { - this._dynamicHeaderEndpoints = Gerrit._endpoints.getDynamicEndpoints( - 'change-view-file-list-header'); - this._dynamicContentEndpoints = Gerrit._endpoints.getDynamicEndpoints( - 'change-view-file-list-content'); - this._dynamicSummaryEndpoints = Gerrit._endpoints.getDynamicEndpoints( - 'change-view-file-list-summary'); - - if (this._dynamicHeaderEndpoints.length !== - this._dynamicContentEndpoints.length) { - console.warn( - 'Different number of dynamic file-list header and content.'); - } - if (this._dynamicHeaderEndpoints.length !== - this._dynamicSummaryEndpoints.length) { - console.warn( - 'Different number of dynamic file-list headers and summary.'); - } - }); - } - - /** @override */ - detached() { - super.detached(); - this._cancelDiffs(); - } - - /** - * Iron-a11y-keys-behavior catches keyboard events globally. Some keyboard - * events must be scoped to a component level (e.g. `enter`) in order to not - * override native browser functionality. - * - * Context: Issue 7277 - */ - _scopedKeydownHandler(e) { - if (e.keyCode === 13) { - // Enter. - this._handleOpenFile(e); - } - } - - reload() { - if (!this.changeNum || !this.patchRange.patchNum) { - return Promise.resolve(); - } - - this._loading = true; - - this.collapseAllDiffs(); - const promises = []; - - promises.push(this._getFiles().then(filesByPath => { - this._filesByPath = filesByPath; - })); - promises.push(this._getLoggedIn() - .then(loggedIn => this._loggedIn = loggedIn) - .then(loggedIn => { - if (!loggedIn) { return; } - - return this._getReviewedFiles().then(reviewed => { - this._reviewed = reviewed; - }); - })); - - promises.push(this._getDiffPreferences().then(prefs => { - this.diffPrefs = prefs; - })); - - promises.push(this._getPreferences().then(prefs => { - this._userPrefs = prefs; - })); - - return Promise.all(promises).then(() => { - this._loading = false; - this._detectChromiteButler(); - this.$.reporting.fileListDisplayed(); - }); - } - - _detectChromiteButler() { - const hasButler = !!document.getElementById('butler-suggested-owners'); - if (hasButler) { - this.$.reporting.reportExtension('butler'); - } - } - - get diffs() { - const diffs = Polymer.dom(this.root).querySelectorAll('gr-diff-host'); - // It is possible that a bogus diff element is hanging around invisibly - // from earlier with a different patch set choice and associated with a - // different entry in the files array. So filter on visible items only. - return Array.from(diffs).filter( - el => !!el && !!el.style && el.style.display !== 'none'); - } - - openDiffPrefs() { - this.$.diffPreferencesDialog.open(); - } - - _calculatePatchChange(files) { - const magicFilesExcluded = files.filter(files => - !this.isMagicPath(files.__path) - ); - - return magicFilesExcluded.reduce((acc, obj) => { - const inserted = obj.lines_inserted ? obj.lines_inserted : 0; - const deleted = obj.lines_deleted ? obj.lines_deleted : 0; - const total_size = (obj.size && obj.binary) ? obj.size : 0; - const size_delta_inserted = - obj.binary && obj.size_delta > 0 ? obj.size_delta : 0; - const size_delta_deleted = - obj.binary && obj.size_delta < 0 ? obj.size_delta : 0; - - return { - inserted: acc.inserted + inserted, - deleted: acc.deleted + deleted, - size_delta_inserted: acc.size_delta_inserted + size_delta_inserted, - size_delta_deleted: acc.size_delta_deleted + size_delta_deleted, - total_size: acc.total_size + total_size, - }; - }, {inserted: 0, deleted: 0, size_delta_inserted: 0, - size_delta_deleted: 0, total_size: 0}); - } - - _getDiffPreferences() { - return this.$.restAPI.getDiffPreferences(); - } - - _getPreferences() { - return this.$.restAPI.getPreferences(); - } - - _togglePathExpanded(path) { - // Is the path in the list of expanded diffs? IF so remove it, otherwise - // add it to the list. - const pathIndex = this._expandedFilePaths.indexOf(path); - if (pathIndex === -1) { - this.push('_expandedFilePaths', path); - } else { - this.splice('_expandedFilePaths', pathIndex, 1); - } - } - - _togglePathExpandedByIndex(index) { - this._togglePathExpanded(this._files[index].__path); - } - - _updateDiffPreferences() { - if (!this.diffs.length) { return; } - // Re-render all expanded diffs sequentially. - this.$.reporting.time(EXPAND_ALL_TIMING_LABEL); - this._renderInOrder(this._expandedFilePaths, this.diffs, - this._expandedFilePaths.length); - } - - _forEachDiff(fn) { - const diffs = this.diffs; - for (let i = 0; i < diffs.length; i++) { - fn(diffs[i]); - } - } - - expandAllDiffs() { - this._showInlineDiffs = true; - - // Find the list of paths that are in the file list, but not in the - // expanded list. - const newPaths = []; - let path; - for (let i = 0; i < this._shownFiles.length; i++) { - path = this._shownFiles[i].__path; - if (!this._expandedFilePaths.includes(path)) { - newPaths.push(path); - } - } - - this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths)); - } - - collapseAllDiffs() { - this._showInlineDiffs = false; - this._expandedFilePaths = []; - this.filesExpanded = this._computeExpandedFiles( - this._expandedFilePaths.length, this._files.length); - this.$.diffCursor.handleDiffUpdate(); - } - - /** - * Computes a string with the number of comments and unresolved comments. - * - * @param {!Object} changeComments - * @param {!Object} patchRange - * @param {string} path - * @return {string} - */ - _computeCommentsString(changeComments, patchRange, path) { - const unresolvedCount = - changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) + - changeComments.computeUnresolvedNum(patchRange.patchNum, path); - const commentCount = - changeComments.computeCommentCount(patchRange.basePatchNum, path) + - changeComments.computeCommentCount(patchRange.patchNum, path); - const commentString = GrCountStringFormatter.computePluralString( - commentCount, 'comment'); - const unresolvedString = GrCountStringFormatter.computeString( - unresolvedCount, 'unresolved'); - - return commentString + - // Add a space if both comments and unresolved - (commentString && unresolvedString ? ' ' : '') + - // Add parentheses around unresolved if it exists. - (unresolvedString ? `(${unresolvedString})` : ''); - } - - /** - * Computes a string with the number of drafts. - * - * @param {!Object} changeComments - * @param {!Object} patchRange - * @param {string} path - * @return {string} - */ - _computeDraftsString(changeComments, patchRange, path) { - const draftCount = - changeComments.computeDraftCount(patchRange.basePatchNum, path) + - changeComments.computeDraftCount(patchRange.patchNum, path); - return GrCountStringFormatter.computePluralString(draftCount, 'draft'); - } - - /** - * Computes a shortened string with the number of drafts. - * - * @param {!Object} changeComments - * @param {!Object} patchRange - * @param {string} path - * @return {string} - */ - _computeDraftsStringMobile(changeComments, patchRange, path) { - const draftCount = - changeComments.computeDraftCount(patchRange.basePatchNum, path) + - changeComments.computeDraftCount(patchRange.patchNum, path); - return GrCountStringFormatter.computeShortString(draftCount, 'd'); - } - - /** - * Computes a shortened string with the number of comments. - * - * @param {!Object} changeComments - * @param {!Object} patchRange - * @param {string} path - * @return {string} - */ - _computeCommentsStringMobile(changeComments, patchRange, path) { - const commentCount = - changeComments.computeCommentCount(patchRange.basePatchNum, path) + - changeComments.computeCommentCount(patchRange.patchNum, path); - return GrCountStringFormatter.computeShortString(commentCount, 'c'); - } - - /** - * @param {string} path - * @param {boolean=} opt_reviewed - */ - _reviewFile(path, opt_reviewed) { - if (this.editMode) { return; } - const index = this._files.findIndex(file => file.__path === path); - const reviewed = opt_reviewed || !this._files[index].isReviewed; - - this.set(['_files', index, 'isReviewed'], reviewed); - if (index < this._shownFiles.length) { - this.notifyPath(`_shownFiles.${index}.isReviewed`); - } - - this._saveReviewedState(path, reviewed); - } - - _saveReviewedState(path, reviewed) { - return this.$.restAPI.saveFileReviewed(this.changeNum, - this.patchRange.patchNum, path, reviewed); - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _getReviewedFiles() { - if (this.editMode) { return Promise.resolve([]); } - return this.$.restAPI.getReviewedFiles(this.changeNum, - this.patchRange.patchNum); - } - - _getFiles() { - return this.$.restAPI.getChangeOrEditFiles( - this.changeNum, this.patchRange); - } - - /** - * The closure compiler doesn't realize this.specialFilePathCompare is - * valid. - * - * @suppress {checkTypes} - */ - _normalizeChangeFilesResponse(response) { - if (!response) { return []; } - const paths = Object.keys(response).sort(this.specialFilePathCompare); - const files = []; - for (let i = 0; i < paths.length; i++) { - const info = response[paths[i]]; - info.__path = paths[i]; - info.lines_inserted = info.lines_inserted || 0; - info.lines_deleted = info.lines_deleted || 0; - files.push(info); - } - return files; - } - - /** - * Handle all events from the file list dom-repeat so event handleers don't - * have to get registered for potentially very long lists. - */ - _handleFileListClick(e) { - // Traverse upwards to find the row element if the target is not the row. - let row = e.target; - while (!row.classList.contains('row') && row.parentElement) { - row = row.parentElement; - } - - const path = row.dataset.path; - // Handle checkbox mark as reviewed. - if (e.target.classList.contains('markReviewed')) { - e.preventDefault(); - return this._reviewFile(path); - } - - // If a path cannot be interpreted from the click target (meaning it's not - // somewhere in the row, e.g. diff content) or if the user clicked the - // link, defer to the native behavior. - if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; } - - // Disregard the event if the click target is in the edit controls. - if (this.descendedFromClass(e.target, 'editFileControls')) { return; } - - e.preventDefault(); - this._togglePathExpanded(path); - } - - _handleLeftPane(e) { - if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) { - return; - } - - e.preventDefault(); - this.$.diffCursor.moveLeft(); - } - - _handleRightPane(e) { - if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) { - return; - } - - e.preventDefault(); - this.$.diffCursor.moveRight(); - } - - _handleToggleInlineDiff(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e) || - this.$.fileCursor.index === -1) { return; } - - e.preventDefault(); - this._togglePathExpandedByIndex(this.$.fileCursor.index); - } - - _handleToggleAllInlineDiffs(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - this._toggleInlineDiffs(); - } - - _handleCursorNext(e) { - if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { - return; - } - - if (this._showInlineDiffs) { - e.preventDefault(); - this.$.diffCursor.moveDown(); - this._displayLine = true; - } else { - // Down key - if (this.getKeyboardEvent(e).keyCode === 40) { return; } - e.preventDefault(); - this.$.fileCursor.next(); - this.selectedIndex = this.$.fileCursor.index; - } - } - - _handleCursorPrev(e) { - if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { - return; - } - - if (this._showInlineDiffs) { - e.preventDefault(); - this.$.diffCursor.moveUp(); - this._displayLine = true; - } else { - // Up key - if (this.getKeyboardEvent(e).keyCode === 38) { return; } - e.preventDefault(); - this.$.fileCursor.previous(); - this.selectedIndex = this.$.fileCursor.index; - } - } - - _handleNewComment(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - e.preventDefault(); - this.$.diffCursor.createCommentInPlace(); - } - - _handleOpenLastFile(e) { - // Check for meta key to avoid overriding native chrome shortcut. - if (this.shouldSuppressKeyboardShortcut(e) || - this.getKeyboardEvent(e).metaKey) { return; } - - e.preventDefault(); - this._openSelectedFile(this._files.length - 1); - } - - _handleOpenFirstFile(e) { - // Check for meta key to avoid overriding native chrome shortcut. - if (this.shouldSuppressKeyboardShortcut(e) || - this.getKeyboardEvent(e).metaKey) { return; } - - e.preventDefault(); - this._openSelectedFile(0); - } - - _handleOpenFile(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - e.preventDefault(); - - if (this._showInlineDiffs) { - this._openCursorFile(); - return; - } - - this._openSelectedFile(); - } - - _handleNextChunk(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) || - this._noDiffsExpanded()) { - return; - } - - e.preventDefault(); - if (this.isModifierPressed(e, 'shiftKey')) { - this.$.diffCursor.moveToNextCommentThread(); - } else { - this.$.diffCursor.moveToNextChunk(); - } - } - - _handlePrevChunk(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) || - this._noDiffsExpanded()) { - return; - } - - e.preventDefault(); - if (this.isModifierPressed(e, 'shiftKey')) { - this.$.diffCursor.moveToPreviousCommentThread(); - } else { - this.$.diffCursor.moveToPreviousChunk(); - } - } - - _handleToggleFileReviewed(e) { - if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { - return; - } - - e.preventDefault(); - if (!this._files[this.$.fileCursor.index]) { return; } - this._reviewFile(this._files[this.$.fileCursor.index].__path); - } - - _handleToggleLeftPane(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - this._forEachDiff(diff => { - diff.toggleLeftDiff(); - }); - } - - _toggleInlineDiffs() { - if (this._showInlineDiffs) { - this.collapseAllDiffs(); - } else { - this.expandAllDiffs(); - } - } - - _openCursorFile() { - const diff = this.$.diffCursor.getTargetDiffElement(); - Gerrit.Nav.navigateToDiff(this.change, diff.path, - diff.patchRange.patchNum, this.patchRange.basePatchNum); - } - - /** - * @param {number=} opt_index - */ - _openSelectedFile(opt_index) { - if (opt_index != null) { - this.$.fileCursor.setCursorAtIndex(opt_index); - } - if (!this._files[this.$.fileCursor.index]) { return; } - Gerrit.Nav.navigateToDiff(this.change, - this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum, - this.patchRange.basePatchNum); - } - - _addDraftAtTarget() { - const diff = this.$.diffCursor.getTargetDiffElement(); - const target = this.$.diffCursor.getTargetLineElement(); - if (diff && target) { - diff.addDraftAtLine(target); - } - } - - _shouldHideChangeTotals(_patchChange) { - return _patchChange.inserted === 0 && _patchChange.deleted === 0; - } - - _shouldHideBinaryChangeTotals(_patchChange) { - return _patchChange.size_delta_inserted === 0 && - _patchChange.size_delta_deleted === 0; - } - - _computeFileStatus(status) { - return status || 'M'; - } - - _computeDiffURL(change, patchRange, path, editMode) { - // Polymer 2: check for undefined - if ([change, patchRange, path, editMode] - .some(arg => arg === undefined)) { - return; - } - // TODO(kaspern): Fix editing for commit messages and merge lists. - if (editMode && path !== this.COMMIT_MESSAGE_PATH && - path !== this.MERGE_LIST_PATH) { - return Gerrit.Nav.getEditUrlForDiff(change, path, patchRange.patchNum, - patchRange.basePatchNum); - } - return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum, - patchRange.basePatchNum); - } - - _formatBytes(bytes) { - if (bytes == 0) return '+/-0 B'; - const bits = 1024; - const decimals = 1; - const sizes = - ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; - const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits)); - const prepend = bytes > 0 ? '+' : ''; - return prepend + parseFloat((bytes / Math.pow(bits, exponent)) - .toFixed(decimals)) + ' ' + sizes[exponent]; - } - - _formatPercentage(size, delta) { - const oldSize = size - delta; - - if (oldSize === 0) { return ''; } - - const percentage = Math.round(Math.abs(delta * 100 / oldSize)); - return '(' + (delta > 0 ? '+' : '-') + percentage + '%)'; - } - - _computeBinaryClass(delta) { - if (delta === 0) { return; } - return delta >= 0 ? 'added' : 'removed'; - } - - /** - * @param {string} baseClass - * @param {string} path - */ - _computeClass(baseClass, path) { - const classes = []; - if (baseClass) { - classes.push(baseClass); - } - if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) { - classes.push('invisible'); - } - return classes.join(' '); - } - - _computePathClass(path, expandedFilesRecord) { - return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : ''; - } - - _computeShowHideIcon(path, expandedFilesRecord) { - return this._isFileExpanded(path, expandedFilesRecord) ? - 'gr-icons:expand-less' : 'gr-icons:expand-more'; - } - - _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) { - // Polymer 2: check for undefined - if ([ - filesByPath, - changeComments, - patchRange, - reviewed, - loading, - ].some(arg => arg === undefined)) { - return; - } - - // Await all promises resolving from reload. @See Issue 9057 - if (loading || !changeComments) { return; } - - const commentedPaths = changeComments.getPaths(patchRange); - const files = Object.assign({}, filesByPath); - Object.keys(commentedPaths).forEach(commentedPath => { - if (files.hasOwnProperty(commentedPath)) { return; } - files[commentedPath] = {status: 'U'}; - }); - const reviewedSet = new Set(reviewed || []); - for (const filePath in files) { - if (!files.hasOwnProperty(filePath)) { continue; } - files[filePath].isReviewed = reviewedSet.has(filePath); - } - - this._files = this._normalizeChangeFilesResponse(files); - } - - _computeFilesShown(numFilesShown, files) { - // Polymer 2: check for undefined - if ([numFilesShown, files].some(arg => arg === undefined)) { - return undefined; - } - - const previousNumFilesShown = this._shownFiles ? - this._shownFiles.length : 0; - - const filesShown = files.slice(0, numFilesShown); - this.fire('files-shown-changed', {length: filesShown.length}); - - // Start the timer for the rendering work hwere because this is where the - // _shownFiles property is being set, and _shownFiles is used in the - // dom-repeat binding. - this.$.reporting.time(RENDER_TIMING_LABEL); - - // How many more files are being shown (if it's an increase). - this._reportinShownFilesIncrement = - Math.max(0, filesShown.length - previousNumFilesShown); - - return filesShown; - } - - _updateDiffCursor() { - // Overwrite the cursor's list of diffs: - this.$.diffCursor.splice( - ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs)); - } - - _filesChanged() { - if (this._files && this._files.length > 0) { - Polymer.dom.flush(); - const files = Array.from( - Polymer.dom(this.root).querySelectorAll('.file-row')); - this.$.fileCursor.stops = files; - this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true); - } - } - - _incrementNumFilesShown() { - this.numFilesShown += this.fileListIncrement; - } - - _computeFileListControlClass(numFilesShown, files) { - return numFilesShown >= files.length ? 'invisible' : ''; - } - - _computeIncrementText(numFilesShown, files) { - if (!files) { return ''; } - const text = - Math.min(this.fileListIncrement, files.length - numFilesShown); - return 'Show ' + text + ' more'; - } - - _computeShowAllText(files) { - if (!files) { return ''; } - return 'Show all ' + files.length + ' files'; - } - - _computeWarnShowAll(files) { - return files.length > WARN_SHOW_ALL_THRESHOLD; - } - - _computeShowAllWarning(files) { - if (!this._computeWarnShowAll(files)) { return ''; } - return 'Warning: showing all ' + files.length + - ' files may take several seconds.'; - } - - _showAllFiles() { - this.numFilesShown = this._files.length; - } - - _computePatchSetDescription(revisions, patchNum) { - // Polymer 2: check for undefined - if ([revisions, patchNum].some(arg => arg === undefined)) { - return ''; - } - - const rev = this.getRevisionByPatchNum(revisions, patchNum); - return (rev && rev.description) ? - rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; - } - - /** - * Get a descriptive label for use in the status indicator's tooltip and - * ARIA label. - * - * @param {string} status - * @return {string} - */ - _computeFileStatusLabel(status) { - const statusCode = this._computeFileStatus(status); - return FileStatus.hasOwnProperty(statusCode) ? - FileStatus[statusCode] : 'Status Unknown'; - } - - _isFileExpanded(path, expandedFilesRecord) { - return expandedFilesRecord.base.includes(path); - } - - _onLineSelected(e, detail) { - this.$.diffCursor.moveToLineNumber(detail.number, detail.side, - detail.path); - } - - _computeExpandedFiles(expandedCount, totalCount) { - if (expandedCount === 0) { - return GrFileListConstants.FilesExpandedState.NONE; - } else if (expandedCount === totalCount) { - return GrFileListConstants.FilesExpandedState.ALL; - } - return GrFileListConstants.FilesExpandedState.SOME; - } - - /** - * Handle splices to the list of expanded file paths. If there are any new - * entries in the expanded list, then render each diff corresponding in - * order by waiting for the previous diff to finish before starting the next - * one. - * - * @param {!Array} record The splice record in the expanded paths list. - */ - _expandedPathsChanged(record) { - // Clear content for any diffs that are not open so if they get re-opened - // the stale content does not flash before it is cleared and reloaded. - const collapsedDiffs = this.diffs.filter(diff => - this._expandedFilePaths.indexOf(diff.path) === -1); - this._clearCollapsedDiffs(collapsedDiffs); - - if (!record) { return; } // Happens after "Collapse all" clicked. - - this.filesExpanded = this._computeExpandedFiles( - this._expandedFilePaths.length, this._files.length); - - // Find the paths introduced by the new index splices: - const newPaths = record.indexSplices - .map(splice => splice.object.slice( - splice.index, splice.index + splice.addedCount)) - .reduce((acc, paths) => acc.concat(paths), []); - - // Required so that the newly created diff view is included in this.diffs. - Polymer.dom.flush(); - - this.$.reporting.time(EXPAND_ALL_TIMING_LABEL); - - if (newPaths.length) { - this._renderInOrder(newPaths, this.diffs, newPaths.length); - } - - this._updateDiffCursor(); - this.$.diffCursor.handleDiffUpdate(); - } - - _clearCollapsedDiffs(collapsedDiffs) { - for (const diff of collapsedDiffs) { - diff.cancel(); - diff.clearDiffContent(); - } - } - - /** - * Given an array of paths and a NodeList of diff elements, render the diff - * for each path in order, awaiting the previous render to complete before - * continung. - * - * @param {!Array<string>} paths - * @param {!NodeList<!Object>} diffElements (GrDiffHostElement) - * @param {number} initialCount The total number of paths in the pass. This - * is used to generate log messages. - * @return {!Promise} - */ - _renderInOrder(paths, diffElements, initialCount) { - let iter = 0; - - return (new Promise(resolve => { - this.fire('reload-drafts', {resolve}); - })).then(() => this.asyncForeach(paths, (path, cancel) => { - this._cancelForEachDiff = cancel; - - iter++; - console.log('Expanding diff', iter, 'of', initialCount, ':', - path); - const diffElem = this._findDiffByPath(path, diffElements); - if (!diffElem) { - console.warn(`Did not find <gr-diff-host> element for ${path}`); - return Promise.resolve(); - } - diffElem.comments = this.changeComments.getCommentsBySideForPath( - path, this.patchRange, this.projectConfig); - const promises = [diffElem.reload()]; - if (this._loggedIn && !this.diffPrefs.manual_review) { - promises.push(this._reviewFile(path, true)); - } - return Promise.all(promises); - }).then(() => { - this._cancelForEachDiff = null; - this._nextRenderParams = null; - console.log('Finished expanding', initialCount, 'diff(s)'); - this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL, - EXPAND_ALL_AVG_TIMING_LABEL, initialCount); - this.$.diffCursor.handleDiffUpdate(); - })); - } - - /** Cancel the rendering work of every diff in the list */ - _cancelDiffs() { - if (this._cancelForEachDiff) { this._cancelForEachDiff(); } - this._forEachDiff(d => d.cancel()); - } - - /** - * In the given NodeList of diff elements, find the diff for the given path. - * - * @param {string} path - * @param {!NodeList<!Object>} diffElements (GrDiffElement) - * @return {!Object|undefined} (GrDiffElement) - */ - _findDiffByPath(path, diffElements) { - for (let i = 0; i < diffElements.length; i++) { - if (diffElements[i].path === path) { - return diffElements[i]; - } - } - } - - /** - * Reset the comments of a modified thread - * - * @param {string} rootId - * @param {string} path - */ - reloadCommentsForThreadWithRootId(rootId, path) { - // Don't bother continuing if we already know that the path that contains - // the updated comment thread is not expanded. - if (!this._expandedFilePaths.includes(path)) { return; } - const diff = this.diffs.find(d => d.path === path); - - const threadEl = diff.getThreadEls().find(t => t.rootId === rootId); - if (!threadEl) { return; } - - const newComments = this.changeComments.getCommentsForThread(rootId); - - // If newComments is null, it means that a single draft was - // removed from a thread in the thread view, and the thread should - // no longer exist. Remove the existing thread element in the diff - // view. - if (!newComments) { - threadEl.fireRemoveSelf(); - return; - } - - // Comments are not returned with the commentSide attribute from - // the api, but it's necessary to be stored on the diff's - // comments due to use in the _handleCommentUpdate function. - // The comment thread already has a side associated with it, so - // set the comment's side to match. - threadEl.comments = newComments.map(c => Object.assign( - c, {__commentSide: threadEl.commentSide} - )); - Polymer.dom.flush(); - return; - } - - _handleEscKey(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - e.preventDefault(); - this._displayLine = false; - } - - /** - * Update the loading class for the file list rows. The update is inside a - * debouncer so that the file list doesn't flash gray when the API requests - * are reasonably fast. - * - * @param {boolean} loading - */ - _loadingChanged(loading) { - this.debounce('loading-change', () => { - // Only show set the loading if there have been files loaded to show. In - // this way, the gray loading style is not shown on initial loads. - this.classList.toggle('loading', loading && !!this._files.length); - }, LOADING_DEBOUNCE_INTERVAL); - } - - _editModeChanged(editMode) { - this.classList.toggle('editMode', editMode); - } - - _computeReviewedClass(isReviewed) { - return isReviewed ? 'isReviewed' : ''; - } - - _computeReviewedText(isReviewed) { - return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED'; - } - - /** - * Given a file path, return whether that path should have visible size bars - * and be included in the size bars calculation. - * - * @param {string} path - * @return {boolean} - */ - _showBarsForPath(path) { - return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH; - } - - /** - * Compute size bar layout values from the file list. - * - * @return {Gerrit.LayoutStats|undefined} - * - */ - _computeSizeBarLayout(shownFilesRecord) { - if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; } - const stats = { - maxInserted: 0, - maxDeleted: 0, - maxAdditionWidth: 0, - maxDeletionWidth: 0, - deletionOffset: 0, - }; - shownFilesRecord.base - .filter(f => this._showBarsForPath(f.__path)) - .forEach(f => { - if (f.lines_inserted) { - stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted); - } - if (f.lines_deleted) { - stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted); - } - }); - const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted); - if (!isNaN(ratio)) { - stats.maxAdditionWidth = - (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio; - stats.maxDeletionWidth = - SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth; - stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH; - } - return stats; - } - - /** - * Get the width of the addition bar for a file. - * - * @param {Object} file - * @param {Gerrit.LayoutStats} stats - * @return {number} - */ - _computeBarAdditionWidth(file, stats) { - if (stats.maxInserted === 0 || - !file.lines_inserted || - !this._showBarsForPath(file.__path)) { - return 0; - } - const width = - stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted; - return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width); - } - - /** - * Get the x-offset of the addition bar for a file. - * - * @param {Object} file - * @param {Gerrit.LayoutStats} stats - * @return {number} - */ - _computeBarAdditionX(file, stats) { - return stats.maxAdditionWidth - - this._computeBarAdditionWidth(file, stats); - } - - /** - * Get the width of the deletion bar for a file. - * - * @param {Object} file - * @param {Gerrit.LayoutStats} stats - * @return {number} - */ - _computeBarDeletionWidth(file, stats) { - if (stats.maxDeleted === 0 || - !file.lines_deleted || - !this._showBarsForPath(file.__path)) { - return 0; - } - const width = - stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted; - return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width); - } - - /** - * Get the x-offset of the deletion bar for a file. - * - * @param {Gerrit.LayoutStats} stats - * - * @return {number} - */ - _computeBarDeletionX(stats) { - return stats.deletionOffset; - } - - _computeShowSizeBars(userPrefs) { - return !!userPrefs.size_bar_in_change_table; - } - - _computeSizeBarsClass(showSizeBars, path) { - let hideClass = ''; - if (!showSizeBars) { - hideClass = 'hide'; - } else if (!this._showBarsForPath(path)) { - hideClass = 'invisible'; - } - return `sizeBars desktop ${hideClass}`; - } - - /** - * Shows registered dynamic columns iff the 'header', 'content' and - * 'summary' endpoints are regiestered the exact same number of times. - * Ideally, there should be a better way to enforce the expectation of the - * dependencies between dynamic endpoints. - */ - _computeShowDynamicColumns( - headerEndpoints, contentEndpoints, summaryEndpoints) { - return headerEndpoints && contentEndpoints && summaryEndpoints && - headerEndpoints.length === contentEndpoints.length && - headerEndpoints.length === summaryEndpoints.length; - } - - /** - * Returns true if none of the inline diffs have been expanded. - * - * @return {boolean} - */ - _noDiffsExpanded() { - return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE; - } - - /** - * Method to call via binding when each file list row is rendered. This - * allows approximate detection of when the dom-repeat has completed - * rendering. - * - * @param {number} index The index of the row being rendered. - * @return {string} an empty string. - */ - _reportRenderedRow(index) { - if (index === this._shownFiles.length - 1) { - this.async(() => { - this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL, - RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement); - }, 1); - } - return ''; - } - - _reviewedTitle(reviewed) { - if (reviewed) { - return 'Mark as not reviewed (shortcut: r)'; - } - - return 'Mark as reviewed (shortcut: r)'; - } - - _handleReloadingDiffPreference() { - this._getDiffPreferences().then(prefs => { - this.diffPrefs = prefs; - }); + _scopedKeydownHandler(e) { + if (e.keyCode === 13) { + // Enter. + this._handleOpenFile(e); } } - customElements.define(GrFileList.is, GrFileList); -})(); + reload() { + if (!this.changeNum || !this.patchRange.patchNum) { + return Promise.resolve(); + } + + this._loading = true; + + this.collapseAllDiffs(); + const promises = []; + + promises.push(this._getFiles().then(filesByPath => { + this._filesByPath = filesByPath; + })); + promises.push(this._getLoggedIn() + .then(loggedIn => this._loggedIn = loggedIn) + .then(loggedIn => { + if (!loggedIn) { return; } + + return this._getReviewedFiles().then(reviewed => { + this._reviewed = reviewed; + }); + })); + + promises.push(this._getDiffPreferences().then(prefs => { + this.diffPrefs = prefs; + })); + + promises.push(this._getPreferences().then(prefs => { + this._userPrefs = prefs; + })); + + return Promise.all(promises).then(() => { + this._loading = false; + this._detectChromiteButler(); + this.$.reporting.fileListDisplayed(); + }); + } + + _detectChromiteButler() { + const hasButler = !!document.getElementById('butler-suggested-owners'); + if (hasButler) { + this.$.reporting.reportExtension('butler'); + } + } + + get diffs() { + const diffs = dom(this.root).querySelectorAll('gr-diff-host'); + // It is possible that a bogus diff element is hanging around invisibly + // from earlier with a different patch set choice and associated with a + // different entry in the files array. So filter on visible items only. + return Array.from(diffs).filter( + el => !!el && !!el.style && el.style.display !== 'none'); + } + + openDiffPrefs() { + this.$.diffPreferencesDialog.open(); + } + + _calculatePatchChange(files) { + const magicFilesExcluded = files.filter(files => + !this.isMagicPath(files.__path) + ); + + return magicFilesExcluded.reduce((acc, obj) => { + const inserted = obj.lines_inserted ? obj.lines_inserted : 0; + const deleted = obj.lines_deleted ? obj.lines_deleted : 0; + const total_size = (obj.size && obj.binary) ? obj.size : 0; + const size_delta_inserted = + obj.binary && obj.size_delta > 0 ? obj.size_delta : 0; + const size_delta_deleted = + obj.binary && obj.size_delta < 0 ? obj.size_delta : 0; + + return { + inserted: acc.inserted + inserted, + deleted: acc.deleted + deleted, + size_delta_inserted: acc.size_delta_inserted + size_delta_inserted, + size_delta_deleted: acc.size_delta_deleted + size_delta_deleted, + total_size: acc.total_size + total_size, + }; + }, {inserted: 0, deleted: 0, size_delta_inserted: 0, + size_delta_deleted: 0, total_size: 0}); + } + + _getDiffPreferences() { + return this.$.restAPI.getDiffPreferences(); + } + + _getPreferences() { + return this.$.restAPI.getPreferences(); + } + + _togglePathExpanded(path) { + // Is the path in the list of expanded diffs? IF so remove it, otherwise + // add it to the list. + const pathIndex = this._expandedFilePaths.indexOf(path); + if (pathIndex === -1) { + this.push('_expandedFilePaths', path); + } else { + this.splice('_expandedFilePaths', pathIndex, 1); + } + } + + _togglePathExpandedByIndex(index) { + this._togglePathExpanded(this._files[index].__path); + } + + _updateDiffPreferences() { + if (!this.diffs.length) { return; } + // Re-render all expanded diffs sequentially. + this.$.reporting.time(EXPAND_ALL_TIMING_LABEL); + this._renderInOrder(this._expandedFilePaths, this.diffs, + this._expandedFilePaths.length); + } + + _forEachDiff(fn) { + const diffs = this.diffs; + for (let i = 0; i < diffs.length; i++) { + fn(diffs[i]); + } + } + + expandAllDiffs() { + this._showInlineDiffs = true; + + // Find the list of paths that are in the file list, but not in the + // expanded list. + const newPaths = []; + let path; + for (let i = 0; i < this._shownFiles.length; i++) { + path = this._shownFiles[i].__path; + if (!this._expandedFilePaths.includes(path)) { + newPaths.push(path); + } + } + + this.splice(...['_expandedFilePaths', 0, 0].concat(newPaths)); + } + + collapseAllDiffs() { + this._showInlineDiffs = false; + this._expandedFilePaths = []; + this.filesExpanded = this._computeExpandedFiles( + this._expandedFilePaths.length, this._files.length); + this.$.diffCursor.handleDiffUpdate(); + } + + /** + * Computes a string with the number of comments and unresolved comments. + * + * @param {!Object} changeComments + * @param {!Object} patchRange + * @param {string} path + * @return {string} + */ + _computeCommentsString(changeComments, patchRange, path) { + const unresolvedCount = + changeComments.computeUnresolvedNum(patchRange.basePatchNum, path) + + changeComments.computeUnresolvedNum(patchRange.patchNum, path); + const commentCount = + changeComments.computeCommentCount(patchRange.basePatchNum, path) + + changeComments.computeCommentCount(patchRange.patchNum, path); + const commentString = GrCountStringFormatter.computePluralString( + commentCount, 'comment'); + const unresolvedString = GrCountStringFormatter.computeString( + unresolvedCount, 'unresolved'); + + return commentString + + // Add a space if both comments and unresolved + (commentString && unresolvedString ? ' ' : '') + + // Add parentheses around unresolved if it exists. + (unresolvedString ? `(${unresolvedString})` : ''); + } + + /** + * Computes a string with the number of drafts. + * + * @param {!Object} changeComments + * @param {!Object} patchRange + * @param {string} path + * @return {string} + */ + _computeDraftsString(changeComments, patchRange, path) { + const draftCount = + changeComments.computeDraftCount(patchRange.basePatchNum, path) + + changeComments.computeDraftCount(patchRange.patchNum, path); + return GrCountStringFormatter.computePluralString(draftCount, 'draft'); + } + + /** + * Computes a shortened string with the number of drafts. + * + * @param {!Object} changeComments + * @param {!Object} patchRange + * @param {string} path + * @return {string} + */ + _computeDraftsStringMobile(changeComments, patchRange, path) { + const draftCount = + changeComments.computeDraftCount(patchRange.basePatchNum, path) + + changeComments.computeDraftCount(patchRange.patchNum, path); + return GrCountStringFormatter.computeShortString(draftCount, 'd'); + } + + /** + * Computes a shortened string with the number of comments. + * + * @param {!Object} changeComments + * @param {!Object} patchRange + * @param {string} path + * @return {string} + */ + _computeCommentsStringMobile(changeComments, patchRange, path) { + const commentCount = + changeComments.computeCommentCount(patchRange.basePatchNum, path) + + changeComments.computeCommentCount(patchRange.patchNum, path); + return GrCountStringFormatter.computeShortString(commentCount, 'c'); + } + + /** + * @param {string} path + * @param {boolean=} opt_reviewed + */ + _reviewFile(path, opt_reviewed) { + if (this.editMode) { return; } + const index = this._files.findIndex(file => file.__path === path); + const reviewed = opt_reviewed || !this._files[index].isReviewed; + + this.set(['_files', index, 'isReviewed'], reviewed); + if (index < this._shownFiles.length) { + this.notifyPath(`_shownFiles.${index}.isReviewed`); + } + + this._saveReviewedState(path, reviewed); + } + + _saveReviewedState(path, reviewed) { + return this.$.restAPI.saveFileReviewed(this.changeNum, + this.patchRange.patchNum, path, reviewed); + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _getReviewedFiles() { + if (this.editMode) { return Promise.resolve([]); } + return this.$.restAPI.getReviewedFiles(this.changeNum, + this.patchRange.patchNum); + } + + _getFiles() { + return this.$.restAPI.getChangeOrEditFiles( + this.changeNum, this.patchRange); + } + + /** + * The closure compiler doesn't realize this.specialFilePathCompare is + * valid. + * + * @suppress {checkTypes} + */ + _normalizeChangeFilesResponse(response) { + if (!response) { return []; } + const paths = Object.keys(response).sort(this.specialFilePathCompare); + const files = []; + for (let i = 0; i < paths.length; i++) { + const info = response[paths[i]]; + info.__path = paths[i]; + info.lines_inserted = info.lines_inserted || 0; + info.lines_deleted = info.lines_deleted || 0; + files.push(info); + } + return files; + } + + /** + * Handle all events from the file list dom-repeat so event handleers don't + * have to get registered for potentially very long lists. + */ + _handleFileListClick(e) { + // Traverse upwards to find the row element if the target is not the row. + let row = e.target; + while (!row.classList.contains('row') && row.parentElement) { + row = row.parentElement; + } + + const path = row.dataset.path; + // Handle checkbox mark as reviewed. + if (e.target.classList.contains('markReviewed')) { + e.preventDefault(); + return this._reviewFile(path); + } + + // If a path cannot be interpreted from the click target (meaning it's not + // somewhere in the row, e.g. diff content) or if the user clicked the + // link, defer to the native behavior. + if (!path || this.descendedFromClass(e.target, 'pathLink')) { return; } + + // Disregard the event if the click target is in the edit controls. + if (this.descendedFromClass(e.target, 'editFileControls')) { return; } + + e.preventDefault(); + this._togglePathExpanded(path); + } + + _handleLeftPane(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) { + return; + } + + e.preventDefault(); + this.$.diffCursor.moveLeft(); + } + + _handleRightPane(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this._noDiffsExpanded()) { + return; + } + + e.preventDefault(); + this.$.diffCursor.moveRight(); + } + + _handleToggleInlineDiff(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e) || + this.$.fileCursor.index === -1) { return; } + + e.preventDefault(); + this._togglePathExpandedByIndex(this.$.fileCursor.index); + } + + _handleToggleAllInlineDiffs(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._toggleInlineDiffs(); + } + + _handleCursorNext(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { + return; + } + + if (this._showInlineDiffs) { + e.preventDefault(); + this.$.diffCursor.moveDown(); + this._displayLine = true; + } else { + // Down key + if (this.getKeyboardEvent(e).keyCode === 40) { return; } + e.preventDefault(); + this.$.fileCursor.next(); + this.selectedIndex = this.$.fileCursor.index; + } + } + + _handleCursorPrev(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { + return; + } + + if (this._showInlineDiffs) { + e.preventDefault(); + this.$.diffCursor.moveUp(); + this._displayLine = true; + } else { + // Up key + if (this.getKeyboardEvent(e).keyCode === 38) { return; } + e.preventDefault(); + this.$.fileCursor.previous(); + this.selectedIndex = this.$.fileCursor.index; + } + } + + _handleNewComment(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + e.preventDefault(); + this.$.diffCursor.createCommentInPlace(); + } + + _handleOpenLastFile(e) { + // Check for meta key to avoid overriding native chrome shortcut. + if (this.shouldSuppressKeyboardShortcut(e) || + this.getKeyboardEvent(e).metaKey) { return; } + + e.preventDefault(); + this._openSelectedFile(this._files.length - 1); + } + + _handleOpenFirstFile(e) { + // Check for meta key to avoid overriding native chrome shortcut. + if (this.shouldSuppressKeyboardShortcut(e) || + this.getKeyboardEvent(e).metaKey) { return; } + + e.preventDefault(); + this._openSelectedFile(0); + } + + _handleOpenFile(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + e.preventDefault(); + + if (this._showInlineDiffs) { + this._openCursorFile(); + return; + } + + this._openSelectedFile(); + } + + _handleNextChunk(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) || + this._noDiffsExpanded()) { + return; + } + + e.preventDefault(); + if (this.isModifierPressed(e, 'shiftKey')) { + this.$.diffCursor.moveToNextCommentThread(); + } else { + this.$.diffCursor.moveToNextChunk(); + } + } + + _handlePrevChunk(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + (this.modifierPressed(e) && !this.isModifierPressed(e, 'shiftKey')) || + this._noDiffsExpanded()) { + return; + } + + e.preventDefault(); + if (this.isModifierPressed(e, 'shiftKey')) { + this.$.diffCursor.moveToPreviousCommentThread(); + } else { + this.$.diffCursor.moveToPreviousChunk(); + } + } + + _handleToggleFileReviewed(e) { + if (this.shouldSuppressKeyboardShortcut(e) || this.modifierPressed(e)) { + return; + } + + e.preventDefault(); + if (!this._files[this.$.fileCursor.index]) { return; } + this._reviewFile(this._files[this.$.fileCursor.index].__path); + } + + _handleToggleLeftPane(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this._forEachDiff(diff => { + diff.toggleLeftDiff(); + }); + } + + _toggleInlineDiffs() { + if (this._showInlineDiffs) { + this.collapseAllDiffs(); + } else { + this.expandAllDiffs(); + } + } + + _openCursorFile() { + const diff = this.$.diffCursor.getTargetDiffElement(); + Gerrit.Nav.navigateToDiff(this.change, diff.path, + diff.patchRange.patchNum, this.patchRange.basePatchNum); + } + + /** + * @param {number=} opt_index + */ + _openSelectedFile(opt_index) { + if (opt_index != null) { + this.$.fileCursor.setCursorAtIndex(opt_index); + } + if (!this._files[this.$.fileCursor.index]) { return; } + Gerrit.Nav.navigateToDiff(this.change, + this._files[this.$.fileCursor.index].__path, this.patchRange.patchNum, + this.patchRange.basePatchNum); + } + + _addDraftAtTarget() { + const diff = this.$.diffCursor.getTargetDiffElement(); + const target = this.$.diffCursor.getTargetLineElement(); + if (diff && target) { + diff.addDraftAtLine(target); + } + } + + _shouldHideChangeTotals(_patchChange) { + return _patchChange.inserted === 0 && _patchChange.deleted === 0; + } + + _shouldHideBinaryChangeTotals(_patchChange) { + return _patchChange.size_delta_inserted === 0 && + _patchChange.size_delta_deleted === 0; + } + + _computeFileStatus(status) { + return status || 'M'; + } + + _computeDiffURL(change, patchRange, path, editMode) { + // Polymer 2: check for undefined + if ([change, patchRange, path, editMode] + .some(arg => arg === undefined)) { + return; + } + // TODO(kaspern): Fix editing for commit messages and merge lists. + if (editMode && path !== this.COMMIT_MESSAGE_PATH && + path !== this.MERGE_LIST_PATH) { + return Gerrit.Nav.getEditUrlForDiff(change, path, patchRange.patchNum, + patchRange.basePatchNum); + } + return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum, + patchRange.basePatchNum); + } + + _formatBytes(bytes) { + if (bytes == 0) return '+/-0 B'; + const bits = 1024; + const decimals = 1; + const sizes = + ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + const exponent = Math.floor(Math.log(Math.abs(bytes)) / Math.log(bits)); + const prepend = bytes > 0 ? '+' : ''; + return prepend + parseFloat((bytes / Math.pow(bits, exponent)) + .toFixed(decimals)) + ' ' + sizes[exponent]; + } + + _formatPercentage(size, delta) { + const oldSize = size - delta; + + if (oldSize === 0) { return ''; } + + const percentage = Math.round(Math.abs(delta * 100 / oldSize)); + return '(' + (delta > 0 ? '+' : '-') + percentage + '%)'; + } + + _computeBinaryClass(delta) { + if (delta === 0) { return; } + return delta >= 0 ? 'added' : 'removed'; + } + + /** + * @param {string} baseClass + * @param {string} path + */ + _computeClass(baseClass, path) { + const classes = []; + if (baseClass) { + classes.push(baseClass); + } + if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) { + classes.push('invisible'); + } + return classes.join(' '); + } + + _computePathClass(path, expandedFilesRecord) { + return this._isFileExpanded(path, expandedFilesRecord) ? 'expanded' : ''; + } + + _computeShowHideIcon(path, expandedFilesRecord) { + return this._isFileExpanded(path, expandedFilesRecord) ? + 'gr-icons:expand-less' : 'gr-icons:expand-more'; + } + + _computeFiles(filesByPath, changeComments, patchRange, reviewed, loading) { + // Polymer 2: check for undefined + if ([ + filesByPath, + changeComments, + patchRange, + reviewed, + loading, + ].some(arg => arg === undefined)) { + return; + } + + // Await all promises resolving from reload. @See Issue 9057 + if (loading || !changeComments) { return; } + + const commentedPaths = changeComments.getPaths(patchRange); + const files = Object.assign({}, filesByPath); + Object.keys(commentedPaths).forEach(commentedPath => { + if (files.hasOwnProperty(commentedPath)) { return; } + files[commentedPath] = {status: 'U'}; + }); + const reviewedSet = new Set(reviewed || []); + for (const filePath in files) { + if (!files.hasOwnProperty(filePath)) { continue; } + files[filePath].isReviewed = reviewedSet.has(filePath); + } + + this._files = this._normalizeChangeFilesResponse(files); + } + + _computeFilesShown(numFilesShown, files) { + // Polymer 2: check for undefined + if ([numFilesShown, files].some(arg => arg === undefined)) { + return undefined; + } + + const previousNumFilesShown = this._shownFiles ? + this._shownFiles.length : 0; + + const filesShown = files.slice(0, numFilesShown); + this.fire('files-shown-changed', {length: filesShown.length}); + + // Start the timer for the rendering work hwere because this is where the + // _shownFiles property is being set, and _shownFiles is used in the + // dom-repeat binding. + this.$.reporting.time(RENDER_TIMING_LABEL); + + // How many more files are being shown (if it's an increase). + this._reportinShownFilesIncrement = + Math.max(0, filesShown.length - previousNumFilesShown); + + return filesShown; + } + + _updateDiffCursor() { + // Overwrite the cursor's list of diffs: + this.$.diffCursor.splice( + ...['diffs', 0, this.$.diffCursor.diffs.length].concat(this.diffs)); + } + + _filesChanged() { + if (this._files && this._files.length > 0) { + flush(); + const files = Array.from( + dom(this.root).querySelectorAll('.file-row')); + this.$.fileCursor.stops = files; + this.$.fileCursor.setCursorAtIndex(this.selectedIndex, true); + } + } + + _incrementNumFilesShown() { + this.numFilesShown += this.fileListIncrement; + } + + _computeFileListControlClass(numFilesShown, files) { + return numFilesShown >= files.length ? 'invisible' : ''; + } + + _computeIncrementText(numFilesShown, files) { + if (!files) { return ''; } + const text = + Math.min(this.fileListIncrement, files.length - numFilesShown); + return 'Show ' + text + ' more'; + } + + _computeShowAllText(files) { + if (!files) { return ''; } + return 'Show all ' + files.length + ' files'; + } + + _computeWarnShowAll(files) { + return files.length > WARN_SHOW_ALL_THRESHOLD; + } + + _computeShowAllWarning(files) { + if (!this._computeWarnShowAll(files)) { return ''; } + return 'Warning: showing all ' + files.length + + ' files may take several seconds.'; + } + + _showAllFiles() { + this.numFilesShown = this._files.length; + } + + _computePatchSetDescription(revisions, patchNum) { + // Polymer 2: check for undefined + if ([revisions, patchNum].some(arg => arg === undefined)) { + return ''; + } + + const rev = this.getRevisionByPatchNum(revisions, patchNum); + return (rev && rev.description) ? + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + } + + /** + * Get a descriptive label for use in the status indicator's tooltip and + * ARIA label. + * + * @param {string} status + * @return {string} + */ + _computeFileStatusLabel(status) { + const statusCode = this._computeFileStatus(status); + return FileStatus.hasOwnProperty(statusCode) ? + FileStatus[statusCode] : 'Status Unknown'; + } + + _isFileExpanded(path, expandedFilesRecord) { + return expandedFilesRecord.base.includes(path); + } + + _onLineSelected(e, detail) { + this.$.diffCursor.moveToLineNumber(detail.number, detail.side, + detail.path); + } + + _computeExpandedFiles(expandedCount, totalCount) { + if (expandedCount === 0) { + return GrFileListConstants.FilesExpandedState.NONE; + } else if (expandedCount === totalCount) { + return GrFileListConstants.FilesExpandedState.ALL; + } + return GrFileListConstants.FilesExpandedState.SOME; + } + + /** + * Handle splices to the list of expanded file paths. If there are any new + * entries in the expanded list, then render each diff corresponding in + * order by waiting for the previous diff to finish before starting the next + * one. + * + * @param {!Array} record The splice record in the expanded paths list. + */ + _expandedPathsChanged(record) { + // Clear content for any diffs that are not open so if they get re-opened + // the stale content does not flash before it is cleared and reloaded. + const collapsedDiffs = this.diffs.filter(diff => + this._expandedFilePaths.indexOf(diff.path) === -1); + this._clearCollapsedDiffs(collapsedDiffs); + + if (!record) { return; } // Happens after "Collapse all" clicked. + + this.filesExpanded = this._computeExpandedFiles( + this._expandedFilePaths.length, this._files.length); + + // Find the paths introduced by the new index splices: + const newPaths = record.indexSplices + .map(splice => splice.object.slice( + splice.index, splice.index + splice.addedCount)) + .reduce((acc, paths) => acc.concat(paths), []); + + // Required so that the newly created diff view is included in this.diffs. + flush(); + + this.$.reporting.time(EXPAND_ALL_TIMING_LABEL); + + if (newPaths.length) { + this._renderInOrder(newPaths, this.diffs, newPaths.length); + } + + this._updateDiffCursor(); + this.$.diffCursor.handleDiffUpdate(); + } + + _clearCollapsedDiffs(collapsedDiffs) { + for (const diff of collapsedDiffs) { + diff.cancel(); + diff.clearDiffContent(); + } + } + + /** + * Given an array of paths and a NodeList of diff elements, render the diff + * for each path in order, awaiting the previous render to complete before + * continung. + * + * @param {!Array<string>} paths + * @param {!NodeList<!Object>} diffElements (GrDiffHostElement) + * @param {number} initialCount The total number of paths in the pass. This + * is used to generate log messages. + * @return {!Promise} + */ + _renderInOrder(paths, diffElements, initialCount) { + let iter = 0; + + return (new Promise(resolve => { + this.fire('reload-drafts', {resolve}); + })).then(() => this.asyncForeach(paths, (path, cancel) => { + this._cancelForEachDiff = cancel; + + iter++; + console.log('Expanding diff', iter, 'of', initialCount, ':', + path); + const diffElem = this._findDiffByPath(path, diffElements); + if (!diffElem) { + console.warn(`Did not find <gr-diff-host> element for ${path}`); + return Promise.resolve(); + } + diffElem.comments = this.changeComments.getCommentsBySideForPath( + path, this.patchRange, this.projectConfig); + const promises = [diffElem.reload()]; + if (this._loggedIn && !this.diffPrefs.manual_review) { + promises.push(this._reviewFile(path, true)); + } + return Promise.all(promises); + }).then(() => { + this._cancelForEachDiff = null; + this._nextRenderParams = null; + console.log('Finished expanding', initialCount, 'diff(s)'); + this.$.reporting.timeEndWithAverage(EXPAND_ALL_TIMING_LABEL, + EXPAND_ALL_AVG_TIMING_LABEL, initialCount); + this.$.diffCursor.handleDiffUpdate(); + })); + } + + /** Cancel the rendering work of every diff in the list */ + _cancelDiffs() { + if (this._cancelForEachDiff) { this._cancelForEachDiff(); } + this._forEachDiff(d => d.cancel()); + } + + /** + * In the given NodeList of diff elements, find the diff for the given path. + * + * @param {string} path + * @param {!NodeList<!Object>} diffElements (GrDiffElement) + * @return {!Object|undefined} (GrDiffElement) + */ + _findDiffByPath(path, diffElements) { + for (let i = 0; i < diffElements.length; i++) { + if (diffElements[i].path === path) { + return diffElements[i]; + } + } + } + + /** + * Reset the comments of a modified thread + * + * @param {string} rootId + * @param {string} path + */ + reloadCommentsForThreadWithRootId(rootId, path) { + // Don't bother continuing if we already know that the path that contains + // the updated comment thread is not expanded. + if (!this._expandedFilePaths.includes(path)) { return; } + const diff = this.diffs.find(d => d.path === path); + + const threadEl = diff.getThreadEls().find(t => t.rootId === rootId); + if (!threadEl) { return; } + + const newComments = this.changeComments.getCommentsForThread(rootId); + + // If newComments is null, it means that a single draft was + // removed from a thread in the thread view, and the thread should + // no longer exist. Remove the existing thread element in the diff + // view. + if (!newComments) { + threadEl.fireRemoveSelf(); + return; + } + + // Comments are not returned with the commentSide attribute from + // the api, but it's necessary to be stored on the diff's + // comments due to use in the _handleCommentUpdate function. + // The comment thread already has a side associated with it, so + // set the comment's side to match. + threadEl.comments = newComments.map(c => Object.assign( + c, {__commentSide: threadEl.commentSide} + )); + flush(); + return; + } + + _handleEscKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + e.preventDefault(); + this._displayLine = false; + } + + /** + * Update the loading class for the file list rows. The update is inside a + * debouncer so that the file list doesn't flash gray when the API requests + * are reasonably fast. + * + * @param {boolean} loading + */ + _loadingChanged(loading) { + this.debounce('loading-change', () => { + // Only show set the loading if there have been files loaded to show. In + // this way, the gray loading style is not shown on initial loads. + this.classList.toggle('loading', loading && !!this._files.length); + }, LOADING_DEBOUNCE_INTERVAL); + } + + _editModeChanged(editMode) { + this.classList.toggle('editMode', editMode); + } + + _computeReviewedClass(isReviewed) { + return isReviewed ? 'isReviewed' : ''; + } + + _computeReviewedText(isReviewed) { + return isReviewed ? 'MARK UNREVIEWED' : 'MARK REVIEWED'; + } + + /** + * Given a file path, return whether that path should have visible size bars + * and be included in the size bars calculation. + * + * @param {string} path + * @return {boolean} + */ + _showBarsForPath(path) { + return path !== this.COMMIT_MESSAGE_PATH && path !== this.MERGE_LIST_PATH; + } + + /** + * Compute size bar layout values from the file list. + * + * @return {Gerrit.LayoutStats|undefined} + * + */ + _computeSizeBarLayout(shownFilesRecord) { + if (!shownFilesRecord || !shownFilesRecord.base) { return undefined; } + const stats = { + maxInserted: 0, + maxDeleted: 0, + maxAdditionWidth: 0, + maxDeletionWidth: 0, + deletionOffset: 0, + }; + shownFilesRecord.base + .filter(f => this._showBarsForPath(f.__path)) + .forEach(f => { + if (f.lines_inserted) { + stats.maxInserted = Math.max(stats.maxInserted, f.lines_inserted); + } + if (f.lines_deleted) { + stats.maxDeleted = Math.max(stats.maxDeleted, f.lines_deleted); + } + }); + const ratio = stats.maxInserted / (stats.maxInserted + stats.maxDeleted); + if (!isNaN(ratio)) { + stats.maxAdditionWidth = + (SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH) * ratio; + stats.maxDeletionWidth = + SIZE_BAR_MAX_WIDTH - SIZE_BAR_GAP_WIDTH - stats.maxAdditionWidth; + stats.deletionOffset = stats.maxAdditionWidth + SIZE_BAR_GAP_WIDTH; + } + return stats; + } + + /** + * Get the width of the addition bar for a file. + * + * @param {Object} file + * @param {Gerrit.LayoutStats} stats + * @return {number} + */ + _computeBarAdditionWidth(file, stats) { + if (stats.maxInserted === 0 || + !file.lines_inserted || + !this._showBarsForPath(file.__path)) { + return 0; + } + const width = + stats.maxAdditionWidth * file.lines_inserted / stats.maxInserted; + return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width); + } + + /** + * Get the x-offset of the addition bar for a file. + * + * @param {Object} file + * @param {Gerrit.LayoutStats} stats + * @return {number} + */ + _computeBarAdditionX(file, stats) { + return stats.maxAdditionWidth - + this._computeBarAdditionWidth(file, stats); + } + + /** + * Get the width of the deletion bar for a file. + * + * @param {Object} file + * @param {Gerrit.LayoutStats} stats + * @return {number} + */ + _computeBarDeletionWidth(file, stats) { + if (stats.maxDeleted === 0 || + !file.lines_deleted || + !this._showBarsForPath(file.__path)) { + return 0; + } + const width = + stats.maxDeletionWidth * file.lines_deleted / stats.maxDeleted; + return width === 0 ? 0 : Math.max(SIZE_BAR_MIN_WIDTH, width); + } + + /** + * Get the x-offset of the deletion bar for a file. + * + * @param {Gerrit.LayoutStats} stats + * + * @return {number} + */ + _computeBarDeletionX(stats) { + return stats.deletionOffset; + } + + _computeShowSizeBars(userPrefs) { + return !!userPrefs.size_bar_in_change_table; + } + + _computeSizeBarsClass(showSizeBars, path) { + let hideClass = ''; + if (!showSizeBars) { + hideClass = 'hide'; + } else if (!this._showBarsForPath(path)) { + hideClass = 'invisible'; + } + return `sizeBars desktop ${hideClass}`; + } + + /** + * Shows registered dynamic columns iff the 'header', 'content' and + * 'summary' endpoints are regiestered the exact same number of times. + * Ideally, there should be a better way to enforce the expectation of the + * dependencies between dynamic endpoints. + */ + _computeShowDynamicColumns( + headerEndpoints, contentEndpoints, summaryEndpoints) { + return headerEndpoints && contentEndpoints && summaryEndpoints && + headerEndpoints.length === contentEndpoints.length && + headerEndpoints.length === summaryEndpoints.length; + } + + /** + * Returns true if none of the inline diffs have been expanded. + * + * @return {boolean} + */ + _noDiffsExpanded() { + return this.filesExpanded === GrFileListConstants.FilesExpandedState.NONE; + } + + /** + * Method to call via binding when each file list row is rendered. This + * allows approximate detection of when the dom-repeat has completed + * rendering. + * + * @param {number} index The index of the row being rendered. + * @return {string} an empty string. + */ + _reportRenderedRow(index) { + if (index === this._shownFiles.length - 1) { + this.async(() => { + this.$.reporting.timeEndWithAverage(RENDER_TIMING_LABEL, + RENDER_AVG_TIMING_LABEL, this._reportinShownFilesIncrement); + }, 1); + } + return ''; + } + + _reviewedTitle(reviewed) { + if (reviewed) { + return 'Mark as not reviewed (shortcut: r)'; + } + + return 'Mark as reviewed (shortcut: r)'; + } + + _handleReloadingDiffPreference() { + this._getDiffPreferences().then(prefs => { + this.diffPrefs = prefs; + }); + } +} + +customElements.define(GrFileList.is, GrFileList);
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js index 289e3f4..9652156 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_html.js
@@ -1,46 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/async-foreach-behavior/async-foreach-behavior.html"> -<link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../diff/gr-diff-cursor/gr-diff-cursor.html"> -<link rel="import" href="../../diff/gr-diff-host/gr-diff-host.html"> -<link rel="import" href="../../diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.html"> -<link rel="import" href="../../edit/gr-edit-file-controls/gr-edit-file-controls.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-linked-text/gr-linked-text.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> -<link rel="import" href="../gr-file-list-constants.html"> - -<dom-module id="gr-file-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -298,9 +274,7 @@ } } </style> - <div - id="container" - on-click="_handleFileListClick"> + <div id="container" on-click="_handleFileListClick"> <div class="header-row row"> <div class="status"></div> <div class="path">File</div> @@ -309,55 +283,38 @@ <div class="header-stats">Delta</div> <template is="dom-if" if="[[_showDynamicColumns]]"> <template is="dom-repeat" items="[[_dynamicHeaderEndpoints]]" as="headerEndpoint"> - <gr-endpoint-decorator name$="[[headerEndpoint]]"> + <gr-endpoint-decorator name\$="[[headerEndpoint]]"> </gr-endpoint-decorator> </template> </template> <!-- Empty div here exists to keep spacing in sync with file rows. --> - <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div> + <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]"></div> <div class="editFileControls showOnEdit"></div> <div class="show-hide"></div> </div> - <template is="dom-repeat" - items="[[_shownFiles]]" - id="files" - as="file" - initial-count="[[fileListIncrement]]" - target-framerate="1"> + <template is="dom-repeat" items="[[_shownFiles]]" id="files" as="file" initial-count="[[fileListIncrement]]" target-framerate="1"> [[_reportRenderedRow(index)]] <div class="stickyArea"> - <div class$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]" - data-path$="[[file.__path]]" tabindex="-1"> - <div class$="[[_computeClass('status', file.__path)]]" - tabindex="0" - title$="[[_computeFileStatusLabel(file.status)]]" - aria-label$="[[_computeFileStatusLabel(file.status)]]"> + <div class\$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" tabindex="-1"> + <div class\$="[[_computeClass('status', file.__path)]]" tabindex="0" title\$="[[_computeFileStatusLabel(file.status)]]" aria-label\$="[[_computeFileStatusLabel(file.status)]]"> [[_computeFileStatus(file.status)]] </div> <!-- TODO: Remove data-url as it appears its not used --> - <span - data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]" - class="path"> - <a class="pathLink" href$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"> - <span title$="[[computeDisplayPath(file.__path)]]" - class="fullFileName"> + <span data-url="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]" class="path"> + <a class="pathLink" href\$="[[_computeDiffURL(change, patchRange, file.__path, editMode)]]"> + <span title\$="[[computeDisplayPath(file.__path)]]" class="fullFileName"> [[computeDisplayPath(file.__path)]] </span> - <span title$="[[computeDisplayPath(file.__path)]]" - class="truncatedFileName"> + <span title\$="[[computeDisplayPath(file.__path)]]" class="truncatedFileName"> [[computeTruncatedPath(file.__path)]] </span> - <gr-copy-clipboard - hide-input - text="[[file.__path]]"></gr-copy-clipboard> + <gr-copy-clipboard hide-input="" text="[[file.__path]]"></gr-copy-clipboard> </a> <template is="dom-if" if="[[file.old_path]]"> - <div class="oldPath" title$="[[file.old_path]]"> + <div class="oldPath" title\$="[[file.old_path]]"> [[file.old_path]] - <gr-copy-clipboard - hide-input - text="[[file.old_path]]"></gr-copy-clipboard> + <gr-copy-clipboard hide-input="" text="[[file.old_path]]"></gr-copy-clipboard> </div> </template> </span> @@ -375,46 +332,27 @@ [[_computeCommentsStringMobile(changeComments, patchRange, file.__path)]] </div> - <div class$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"> + <div class\$="[[_computeSizeBarsClass(_showSizeBars, file.__path)]]"> <svg width="61" height="8"> - <rect - x$="[[_computeBarAdditionX(file, _sizeBarLayout)]]" - y="0" - height="8" - fill="#388E3C" - width$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]" /> - <rect - x$="[[_computeBarDeletionX(_sizeBarLayout)]]" - y="0" - height="8" - fill="#D32F2F" - width$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]" /> + <rect x\$="[[_computeBarAdditionX(file, _sizeBarLayout)]]" y="0" height="8" fill="#388E3C" width\$="[[_computeBarAdditionWidth(file, _sizeBarLayout)]]"></rect> + <rect x\$="[[_computeBarDeletionX(_sizeBarLayout)]]" y="0" height="8" fill="#D32F2F" width\$="[[_computeBarDeletionWidth(file, _sizeBarLayout)]]"></rect> </svg> </div> - <div class$="[[_computeClass('stats', file.__path)]]"> - <span - class="added" - tabindex="0" - aria-label$="[[file.lines_inserted]] lines added" - hidden$=[[file.binary]]> + <div class\$="[[_computeClass('stats', file.__path)]]"> + <span class="added" tabindex="0" aria-label\$="[[file.lines_inserted]] lines added" hidden\$="[[file.binary]]"> +[[file.lines_inserted]] </span> - <span - class="removed" - tabindex="0" - aria-label$="[[file.lines_deleted]] lines removed" - hidden$=[[file.binary]]> + <span class="removed" tabindex="0" aria-label\$="[[file.lines_deleted]] lines removed" hidden\$="[[file.binary]]"> -[[file.lines_deleted]] </span> - <span class$="[[_computeBinaryClass(file.size_delta)]]" - hidden$=[[!file.binary]]> + <span class\$="[[_computeBinaryClass(file.size_delta)]]" hidden\$="[[!file.binary]]"> [[_formatBytes(file.size_delta)]] [[_formatPercentage(file.size, file.size_delta)]] </span> </div> <template is="dom-if" if="[[_showDynamicColumns]]"> <template is="dom-repeat" items="[[_dynamicContentEndpoints]]" as="contentEndpoint"> - <div class$="[[_computeClass('', file.__path)]]"> + <div class\$="[[_computeClass('', file.__path)]]"> <gr-endpoint-decorator name="[[contentEndpoint]]"> <gr-endpoint-param name="changeNum" value="[[changeNum]]"> </gr-endpoint-param> @@ -426,66 +364,38 @@ </div> </template> </template> - <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]" hidden> - <span class$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span> + <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]" hidden=""> + <span class\$="reviewedLabel [[_computeReviewedClass(file.isReviewed)]]">Reviewed</span> <label> <input class="reviewed" type="checkbox" checked="[[file.isReviewed]]"> - <span class="markReviewed" title$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span> + <span class="markReviewed" title\$="[[_reviewedTitle(file.isReviewed)]]">[[_computeReviewedText(file.isReviewed)]]</span> </label> </div> <div class="editFileControls showOnEdit"> <template is="dom-if" if="[[editMode]]"> - <gr-edit-file-controls - class$="[[_computeClass('', file.__path)]]" - file-path="[[file.__path]]"></gr-edit-file-controls> + <gr-edit-file-controls class\$="[[_computeClass('', file.__path)]]" file-path="[[file.__path]]"></gr-edit-file-controls> </template> </div> <div class="show-hide"> - <label class="show-hide" data-path$="[[file.__path]]" - data-expand=true> - <input type="checkbox" class="show-hide" - checked$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" - data-path$="[[file.__path]]" data-expand=true> - <iron-icon - id="icon" - icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]"> + <label class="show-hide" data-path\$="[[file.__path]]" data-expand="true"> + <input type="checkbox" class="show-hide" checked\$="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]" data-path\$="[[file.__path]]" data-expand="true"> + <iron-icon id="icon" icon="[[_computeShowHideIcon(file.__path, _expandedFilePaths.*)]]"> </iron-icon> </label> </div> </div> - <template is="dom-if" - if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"> - <gr-diff-host - no-auto-render - show-load-failure - display-line="[[_displayLine]]" - hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" - change-num="[[changeNum]]" - patch-range="[[patchRange]]" - path="[[file.__path]]" - prefs="[[diffPrefs]]" - project-name="[[change.project]]" - on-line-selected="_onLineSelected" - no-render-on-prefs-change - view-mode="[[diffViewMode]]"></gr-diff-host> + <template is="dom-if" if="[[_isFileExpanded(file.__path, _expandedFilePaths.*)]]"> + <gr-diff-host no-auto-render="" show-load-failure="" display-line="[[_displayLine]]" hidden="[[!_isFileExpanded(file.__path, _expandedFilePaths.*)]]" change-num="[[changeNum]]" patch-range="[[patchRange]]" path="[[file.__path]]" prefs="[[diffPrefs]]" project-name="[[change.project]]" on-line-selected="_onLineSelected" no-render-on-prefs-change="" view-mode="[[diffViewMode]]"></gr-diff-host> </template> </div> </template> </div> - <div - class="row totalChanges" - hidden$="[[_hideChangeTotals]]"> + <div class="row totalChanges" hidden\$="[[_hideChangeTotals]]"> <div class="total-stats"> - <span - class="added" - tabindex="0" - aria-label$="[[_patchChange.inserted]] lines added"> + <span class="added" tabindex="0" aria-label\$="[[_patchChange.inserted]] lines added"> +[[_patchChange.inserted]] </span> - <span - class="removed" - tabindex="0" - aria-label$="[[_patchChange.deleted]] lines removed"> + <span class="removed" tabindex="0" aria-label\$="[[_patchChange.deleted]] lines removed"> -[[_patchChange.deleted]] </span> </div> @@ -496,13 +406,11 @@ </template> </template> <!-- Empty div here exists to keep spacing in sync with file rows. --> - <div class="reviewed hideOnEdit" hidden$="[[!_loggedIn]]"></div> + <div class="reviewed hideOnEdit" hidden\$="[[!_loggedIn]]"></div> <div class="editFileControls showOnEdit"></div> <div class="show-hide"></div> </div> - <div - class="row totalChanges" - hidden$="[[_hideBinaryChangeTotals]]"> + <div class="row totalChanges" hidden\$="[[_hideBinaryChangeTotals]]"> <div class="total-stats"> <span class="added" aria-label="Total lines added"> [[_formatBytes(_patchChange.size_delta_inserted)]] @@ -516,39 +424,21 @@ </span> </div> </div> - <div class$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]"> - <gr-button - class="fileListButton" - id="incrementButton" - link on-click="_incrementNumFilesShown"> + <div class\$="row controlRow [[_computeFileListControlClass(numFilesShown, _files)]]"> + <gr-button class="fileListButton" id="incrementButton" link="" on-click="_incrementNumFilesShown"> [[_computeIncrementText(numFilesShown, _files)]] </gr-button> - <gr-tooltip-content - has-tooltip="[[_computeWarnShowAll(_files)]]" - show-icon="[[_computeWarnShowAll(_files)]]" - title$="[[_computeShowAllWarning(_files)]]"> - <gr-button - class="fileListButton" - id="showAllButton" - link on-click="_showAllFiles"> + <gr-tooltip-content has-tooltip="[[_computeWarnShowAll(_files)]]" show-icon="[[_computeWarnShowAll(_files)]]" title\$="[[_computeShowAllWarning(_files)]]"> + <gr-button class="fileListButton" id="showAllButton" link="" on-click="_showAllFiles"> [[_computeShowAllText(_files)]] </gr-button><!-- --></gr-tooltip-content> </div> - <gr-diff-preferences-dialog - id="diffPreferencesDialog" - diff-prefs="{{diffPrefs}}" - on-reload-diff-preference="_handleReloadingDiffPreference"> + <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{diffPrefs}}" on-reload-diff-preference="_handleReloadingDiffPreference"> </gr-diff-preferences-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> <gr-diff-cursor id="diffCursor"></gr-diff-cursor> - <gr-cursor-manager - id="fileCursor" - scroll-behavior="keep-visible" - focus-on-move - cursor-target-class="selected"></gr-cursor-manager> + <gr-cursor-manager id="fileCursor" scroll-behavior="keep-visible" focus-on-move="" cursor-target-class="selected"></gr-cursor-manager> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-file-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html index af80ca8..84dfc9c 100644 --- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -19,21 +19,30 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-file-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> <script src="/components/web-component-tester/data/a11ySuite.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> -<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html"> -<script src="../../../scripts/util.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> +<script type="module" src="../../diff/gr-comment-api/gr-comment-api.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> -<link rel="import" href="gr-file-list.html"> +<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script> +<script type="module" src="./gr-file-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../diff/gr-comment-api/gr-comment-api.js'; +import '../../../scripts/util.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-file-list.js'; +import '../../diff/gr-comment-api/gr-comment-api-mock_test.js'; +void(0); +</script> <dom-module id="comment-api-mock"> <template> @@ -42,7 +51,7 @@ on-reload-drafts="_reloadDraftsWithCallback"></gr-file-list> <gr-comment-api id="commentAPI"></gr-comment-api> </template> - <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script> + <script type="module" src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script> </dom-module> <test-fixture id="basic"> @@ -51,1389 +60,1790 @@ </template> </test-fixture> -<script> - suite('gr-file-list tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../diff/gr-comment-api/gr-comment-api.js'; +import '../../../scripts/util.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-file-list.js'; +import '../../diff/gr-comment-api/gr-comment-api-mock_test.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-file-list tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left'); + kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right'); + kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup'); + kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup'); + kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down'); + kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up'); + kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down'); + kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up'); + kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c'); + kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '['); + kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']'); + kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o'); + kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n'); + kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p'); + kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r'); + kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); - const kb = window.Gerrit.KeyboardShortcutBinder; - kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left'); - kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right'); - kb.bindShortcut(kb.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup'); - kb.bindShortcut(kb.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup'); - kb.bindShortcut(kb.Shortcut.CURSOR_NEXT_FILE, 'j', 'down'); - kb.bindShortcut(kb.Shortcut.CURSOR_PREV_FILE, 'k', 'up'); - kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down'); - kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up'); - kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c'); - kb.bindShortcut(kb.Shortcut.OPEN_LAST_FILE, '['); - kb.bindShortcut(kb.Shortcut.OPEN_FIRST_FILE, ']'); - kb.bindShortcut(kb.Shortcut.OPEN_FILE, 'o'); - kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n'); - kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p'); - kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r'); - kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); + let element; + let commentApiWrapper; + let sandbox; + let saveStub; + let loadCommentSpy; - let element; - let commentApiWrapper; - let sandbox; - let saveStub; - let loadCommentSpy; - - suite('basic tests', () => { - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getPreferences() { return Promise.resolve({}); }, - getDiffPreferences() { return Promise.resolve({}); }, - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - getAccountCapabilities() { return Promise.resolve({}); }, - }); - stub('gr-date-formatter', { - _loadTimeFormat() { return Promise.resolve(''); }, - }); - stub('gr-diff-host', { - reload() { return Promise.resolve(); }, - }); - - // Element must be wrapped in an element with direct access to the - // comment API. - commentApiWrapper = fixture('basic'); - element = commentApiWrapper.$.fileList; - loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll'); - - // Stub methods on the changeComments object after changeComments has - // been initialized. - commentApiWrapper.loadComments().then(() => { - sandbox.stub(element.changeComments, 'getPaths').returns({}); - sandbox.stub(element.changeComments, 'getCommentsBySideForPath') - .returns({meta: {}, left: [], right: []}); - done(); - }); - element._loading = false; - element.diffPrefs = {}; - element.numFilesShown = 200; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - saveStub = sandbox.stub(element, '_saveReviewedState', - () => Promise.resolve()); + suite('basic tests', () => { + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getPreferences() { return Promise.resolve({}); }, + getDiffPreferences() { return Promise.resolve({}); }, + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + getAccountCapabilities() { return Promise.resolve({}); }, + }); + stub('gr-date-formatter', { + _loadTimeFormat() { return Promise.resolve(''); }, + }); + stub('gr-diff-host', { + reload() { return Promise.resolve(); }, }); - teardown(() => { - sandbox.restore(); + // Element must be wrapped in an element with direct access to the + // comment API. + commentApiWrapper = fixture('basic'); + element = commentApiWrapper.$.fileList; + loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll'); + + // Stub methods on the changeComments object after changeComments has + // been initialized. + commentApiWrapper.loadComments().then(() => { + sandbox.stub(element.changeComments, 'getPaths').returns({}); + sandbox.stub(element.changeComments, 'getCommentsBySideForPath') + .returns({meta: {}, left: [], right: []}); + done(); }); + element._loading = false; + element.diffPrefs = {}; + element.numFilesShown = 200; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + saveStub = sandbox.stub(element, '_saveReviewedState', + () => Promise.resolve()); + }); - test('correct number of files are shown', () => { - element.fileListIncrement = 300; - element._filesByPath = _.range(500) - .reduce((_filesByPath, i) => { - _filesByPath['/file' + i] = {lines_inserted: 9}; - return _filesByPath; - }, {}); + teardown(() => { + sandbox.restore(); + }); - flushAsynchronousOperations(); - assert.equal( - Polymer.dom(element.root).querySelectorAll('.file-row').length, - element.numFilesShown); - const controlRow = element.shadowRoot - .querySelector('.controlRow'); - assert.isFalse(controlRow.classList.contains('invisible')); - assert.equal(element.$.incrementButton.textContent.trim(), - 'Show 300 more'); - assert.equal(element.$.showAllButton.textContent.trim(), - 'Show all 500 files'); + test('correct number of files are shown', () => { + element.fileListIncrement = 300; + element._filesByPath = _.range(500) + .reduce((_filesByPath, i) => { + _filesByPath['/file' + i] = {lines_inserted: 9}; + return _filesByPath; + }, {}); - MockInteractions.tap(element.$.showAllButton); - flushAsynchronousOperations(); + flushAsynchronousOperations(); + assert.equal( + dom(element.root).querySelectorAll('.file-row').length, + element.numFilesShown); + const controlRow = element.shadowRoot + .querySelector('.controlRow'); + assert.isFalse(controlRow.classList.contains('invisible')); + assert.equal(element.$.incrementButton.textContent.trim(), + 'Show 300 more'); + assert.equal(element.$.showAllButton.textContent.trim(), + 'Show all 500 files'); - assert.equal(element.numFilesShown, 500); - assert.equal(element._shownFiles.length, 500); - assert.isTrue(controlRow.classList.contains('invisible')); + MockInteractions.tap(element.$.showAllButton); + flushAsynchronousOperations(); + + assert.equal(element.numFilesShown, 500); + assert.equal(element._shownFiles.length, 500); + assert.isTrue(controlRow.classList.contains('invisible')); + }); + + test('rendering each row calls the _reportRenderedRow method', () => { + const renderedStub = sandbox.stub(element, '_reportRenderedRow'); + element._filesByPath = _.range(10) + .reduce((_filesByPath, i) => { + _filesByPath['/file' + i] = {lines_inserted: 9}; + return _filesByPath; + }, {}); + flushAsynchronousOperations(); + assert.equal( + dom(element.root).querySelectorAll('.file-row').length, 10); + assert.equal(renderedStub.callCount, 10); + }); + + test('calculate totals for patch number', () => { + element._filesByPath = { + '/COMMIT_MSG': { + lines_inserted: 9, + }, + '/MERGE_LIST': { + lines_inserted: 9, + }, + 'file_added_in_rev2.txt': { + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + 'myfile.txt': { + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + }; + + assert.deepEqual(element._patchChange, { + inserted: 2, + deleted: 2, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); - test('rendering each row calls the _reportRenderedRow method', () => { - const renderedStub = sandbox.stub(element, '_reportRenderedRow'); - element._filesByPath = _.range(10) - .reduce((_filesByPath, i) => { - _filesByPath['/file' + i] = {lines_inserted: 9}; - return _filesByPath; - }, {}); - flushAsynchronousOperations(); - assert.equal( - Polymer.dom(element.root).querySelectorAll('.file-row').length, 10); - assert.equal(renderedStub.callCount, 10); + // Test with a commit message that isn't the first file. + element._filesByPath = { + 'file_added_in_rev2.txt': { + lines_inserted: 1, + lines_deleted: 1, + }, + '/COMMIT_MSG': { + lines_inserted: 9, + }, + '/MERGE_LIST': { + lines_inserted: 9, + }, + 'myfile.txt': { + lines_inserted: 1, + lines_deleted: 1, + }, + }; + + assert.deepEqual(element._patchChange, { + inserted: 2, + deleted: 2, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); - test('calculate totals for patch number', () => { - element._filesByPath = { - '/COMMIT_MSG': { - lines_inserted: 9, - }, - '/MERGE_LIST': { - lines_inserted: 9, - }, - 'file_added_in_rev2.txt': { - lines_inserted: 1, - lines_deleted: 1, - size_delta: 10, - size: 100, - }, - 'myfile.txt': { - lines_inserted: 1, - lines_deleted: 1, - size_delta: 10, - size: 100, - }, - }; + // Test with no commit message. + element._filesByPath = { + 'file_added_in_rev2.txt': { + lines_inserted: 1, + lines_deleted: 1, + }, + 'myfile.txt': { + lines_inserted: 1, + lines_deleted: 1, + }, + }; - assert.deepEqual(element._patchChange, { - inserted: 2, - deleted: 2, - size_delta_inserted: 0, - size_delta_deleted: 0, - total_size: 0, - }); - assert.isTrue(element._hideBinaryChangeTotals); - assert.isFalse(element._hideChangeTotals); - - // Test with a commit message that isn't the first file. - element._filesByPath = { - 'file_added_in_rev2.txt': { - lines_inserted: 1, - lines_deleted: 1, - }, - '/COMMIT_MSG': { - lines_inserted: 9, - }, - '/MERGE_LIST': { - lines_inserted: 9, - }, - 'myfile.txt': { - lines_inserted: 1, - lines_deleted: 1, - }, - }; - - assert.deepEqual(element._patchChange, { - inserted: 2, - deleted: 2, - size_delta_inserted: 0, - size_delta_deleted: 0, - total_size: 0, - }); - assert.isTrue(element._hideBinaryChangeTotals); - assert.isFalse(element._hideChangeTotals); - - // Test with no commit message. - element._filesByPath = { - 'file_added_in_rev2.txt': { - lines_inserted: 1, - lines_deleted: 1, - }, - 'myfile.txt': { - lines_inserted: 1, - lines_deleted: 1, - }, - }; - - assert.deepEqual(element._patchChange, { - inserted: 2, - deleted: 2, - size_delta_inserted: 0, - size_delta_deleted: 0, - total_size: 0, - }); - assert.isTrue(element._hideBinaryChangeTotals); - assert.isFalse(element._hideChangeTotals); - - // Test with files missing either lines_inserted or lines_deleted. - element._filesByPath = { - 'file_added_in_rev2.txt': {lines_inserted: 1}, - 'myfile.txt': {lines_deleted: 1}, - }; - assert.deepEqual(element._patchChange, { - inserted: 1, - deleted: 1, - size_delta_inserted: 0, - size_delta_deleted: 0, - total_size: 0, - }); - assert.isTrue(element._hideBinaryChangeTotals); - assert.isFalse(element._hideChangeTotals); + assert.deepEqual(element._patchChange, { + inserted: 2, + deleted: 2, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); - test('binary only files', () => { - element._filesByPath = { - '/COMMIT_MSG': {lines_inserted: 9}, - 'file_binary_1': {binary: true, size_delta: 10, size: 100}, - 'file_binary_2': {binary: true, size_delta: -5, size: 120}, - }; - assert.deepEqual(element._patchChange, { - inserted: 0, - deleted: 0, - size_delta_inserted: 10, - size_delta_deleted: -5, - total_size: 220, - }); - assert.isFalse(element._hideBinaryChangeTotals); - assert.isTrue(element._hideChangeTotals); + // Test with files missing either lines_inserted or lines_deleted. + element._filesByPath = { + 'file_added_in_rev2.txt': {lines_inserted: 1}, + 'myfile.txt': {lines_deleted: 1}, + }; + assert.deepEqual(element._patchChange, { + inserted: 1, + deleted: 1, + size_delta_inserted: 0, + size_delta_deleted: 0, + total_size: 0, }); + assert.isTrue(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); + }); - test('binary and regular files', () => { - element._filesByPath = { - '/COMMIT_MSG': {lines_inserted: 9}, - 'file_binary_1': {binary: true, size_delta: 10, size: 100}, - 'file_binary_2': {binary: true, size_delta: -5, size: 120}, - 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100}, - 'myfile2.txt': {lines_inserted: 10}, - }; - assert.deepEqual(element._patchChange, { - inserted: 10, - deleted: 5, - size_delta_inserted: 10, - size_delta_deleted: -5, - total_size: 220, - }); - assert.isFalse(element._hideBinaryChangeTotals); - assert.isFalse(element._hideChangeTotals); + test('binary only files', () => { + element._filesByPath = { + '/COMMIT_MSG': {lines_inserted: 9}, + 'file_binary_1': {binary: true, size_delta: 10, size: 100}, + 'file_binary_2': {binary: true, size_delta: -5, size: 120}, + }; + assert.deepEqual(element._patchChange, { + inserted: 0, + deleted: 0, + size_delta_inserted: 10, + size_delta_deleted: -5, + total_size: 220, }); + assert.isFalse(element._hideBinaryChangeTotals); + assert.isTrue(element._hideChangeTotals); + }); - test('_formatBytes function', () => { - const table = { - '64': '+64 B', - '1023': '+1023 B', - '1024': '+1 KiB', - '4096': '+4 KiB', - '1073741824': '+1 GiB', - '-64': '-64 B', - '-1023': '-1023 B', - '-1024': '-1 KiB', - '-4096': '-4 KiB', - '-1073741824': '-1 GiB', - '0': '+/-0 B', - }; + test('binary and regular files', () => { + element._filesByPath = { + '/COMMIT_MSG': {lines_inserted: 9}, + 'file_binary_1': {binary: true, size_delta: 10, size: 100}, + 'file_binary_2': {binary: true, size_delta: -5, size: 120}, + 'myfile.txt': {lines_deleted: 5, size_delta: -10, size: 100}, + 'myfile2.txt': {lines_inserted: 10}, + }; + assert.deepEqual(element._patchChange, { + inserted: 10, + deleted: 5, + size_delta_inserted: 10, + size_delta_deleted: -5, + total_size: 220, + }); + assert.isFalse(element._hideBinaryChangeTotals); + assert.isFalse(element._hideChangeTotals); + }); - for (const bytes in table) { - if (table.hasOwnProperty(bytes)) { - assert.equal(element._formatBytes(bytes), table[bytes]); - } + test('_formatBytes function', () => { + const table = { + '64': '+64 B', + '1023': '+1023 B', + '1024': '+1 KiB', + '4096': '+4 KiB', + '1073741824': '+1 GiB', + '-64': '-64 B', + '-1023': '-1023 B', + '-1024': '-1 KiB', + '-4096': '-4 KiB', + '-1073741824': '-1 GiB', + '0': '+/-0 B', + }; + + for (const bytes in table) { + if (table.hasOwnProperty(bytes)) { + assert.equal(element._formatBytes(bytes), table[bytes]); } - }); + } + }); - test('_formatPercentage function', () => { - const table = [ - {size: 100, - delta: 100, - display: '', + test('_formatPercentage function', () => { + const table = [ + {size: 100, + delta: 100, + display: '', + }, + {size: 195060, + delta: 64, + display: '(+0%)', + }, + {size: 195060, + delta: -64, + display: '(-0%)', + }, + {size: 394892, + delta: -7128, + display: '(-2%)', + }, + {size: 90, + delta: -10, + display: '(-10%)', + }, + {size: 110, + delta: 10, + display: '(+10%)', + }, + ]; + + for (const item of table) { + assert.equal(element._formatPercentage( + item.size, item.delta), item.display); + } + }); + + test('comment filtering', () => { + element.changeComments._comments = { + '/COMMIT_MSG': [ + {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'}, + {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'}, + {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'}, + ], + 'myfile.txt': [ + {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'}, + {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'}, + {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'}, + ], + 'unresolved.file': [ + { + patch_set: 2, + message: 'wat!?', + updated: '2017-02-09 16:40:49', + id: '1', + unresolved: true, }, - {size: 195060, - delta: 64, - display: '(+0%)', + { + patch_set: 2, + message: 'hi', + updated: '2017-02-10 16:40:49', + id: '2', + in_reply_to: '1', + unresolved: false, }, - {size: 195060, - delta: -64, - display: '(-0%)', + { + patch_set: 2, + message: 'good news!', + updated: '2017-02-08 16:40:49', + id: '3', + unresolved: true, }, - {size: 394892, - delta: -7128, - display: '(-2%)', + ], + }; + element.changeComments._drafts = { + '/COMMIT_MSG': [ + { + patch_set: 1, + message: 'hi', + updated: '2017-02-15 16:40:49', + id: '5', + unresolved: true, }, - {size: 90, - delta: -10, - display: '(-10%)', + { + patch_set: 1, + message: 'fyi', + updated: '2017-02-15 16:40:49', + id: '6', + unresolved: false, }, - {size: 110, - delta: 10, - display: '(+10%)', + ], + 'unresolved.file': [ + { + patch_set: 1, + message: 'hi', + updated: '2017-02-11 16:40:49', + id: '4', + unresolved: false, }, - ]; + ], + }; - for (const item of table) { - assert.equal(element._formatPercentage( - item.size, item.delta), item.display); - } - }); + const parentTo1 = { + basePatchNum: 'PARENT', + patchNum: '1', + }; - test('comment filtering', () => { - element.changeComments._comments = { - '/COMMIT_MSG': [ - {patch_set: 1, message: 'Done', updated: '2017-02-08 16:40:49'}, - {patch_set: 1, message: 'oh hay', updated: '2017-02-09 16:40:49'}, - {patch_set: 2, message: 'hello', updated: '2017-02-10 16:40:49'}, - ], - 'myfile.txt': [ - {patch_set: 1, message: 'good news!', updated: '2017-02-08 16:40:49'}, - {patch_set: 2, message: 'wat!?', updated: '2017-02-09 16:40:49'}, - {patch_set: 2, message: 'hi', updated: '2017-02-10 16:40:49'}, - ], - 'unresolved.file': [ - { - patch_set: 2, - message: 'wat!?', - updated: '2017-02-09 16:40:49', - id: '1', - unresolved: true, - }, - { - patch_set: 2, - message: 'hi', - updated: '2017-02-10 16:40:49', - id: '2', - in_reply_to: '1', - unresolved: false, - }, - { - patch_set: 2, - message: 'good news!', - updated: '2017-02-08 16:40:49', - id: '3', - unresolved: true, - }, - ], - }; - element.changeComments._drafts = { - '/COMMIT_MSG': [ - { - patch_set: 1, - message: 'hi', - updated: '2017-02-15 16:40:49', - id: '5', - unresolved: true, - }, - { - patch_set: 1, - message: 'fyi', - updated: '2017-02-15 16:40:49', - id: '6', - unresolved: false, - }, - ], - 'unresolved.file': [ - { - patch_set: 1, - message: 'hi', - updated: '2017-02-11 16:40:49', - id: '4', - unresolved: false, - }, - ], - }; + const parentTo2 = { + basePatchNum: 'PARENT', + patchNum: '2', + }; - const parentTo1 = { - basePatchNum: 'PARENT', - patchNum: '1', - }; + const _1To2 = { + basePatchNum: '1', + patchNum: '2', + }; - const parentTo2 = { - basePatchNum: 'PARENT', - patchNum: '2', - }; + assert.equal( + element._computeCommentsString(element.changeComments, parentTo1, + '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)'); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)'); + assert.equal( + element._computeCommentsStringMobile(element.changeComments, parentTo1 + , '/COMMIT_MSG'), '2c'); + assert.equal( + element._computeCommentsStringMobile(element.changeComments, _1To2 + , '/COMMIT_MSG'), '3c'); + assert.equal( + element._computeDraftsString(element.changeComments, parentTo1, + 'unresolved.file'), '1 draft'); + assert.equal( + element._computeDraftsString(element.changeComments, _1To2, + 'unresolved.file'), '1 draft'); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, parentTo1, + 'unresolved.file'), '1d'); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, _1To2, + 'unresolved.file'), '1d'); + assert.equal( + element._computeCommentsString(element.changeComments, parentTo1, + 'myfile.txt', 'comment'), '1 comment'); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + 'myfile.txt', 'comment'), '3 comments'); + assert.equal( + element._computeCommentsStringMobile( + element.changeComments, + parentTo1, + 'myfile.txt' + ), '1c'); + assert.equal( + element._computeCommentsStringMobile(element.changeComments, _1To2, + 'myfile.txt'), '3c'); + assert.equal( + element._computeDraftsString(element.changeComments, parentTo1, + 'myfile.txt'), ''); + assert.equal( + element._computeDraftsString(element.changeComments, _1To2, + 'myfile.txt'), ''); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, parentTo1, + 'myfile.txt'), ''); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, _1To2, + 'myfile.txt'), ''); + assert.equal( + element._computeCommentsString(element.changeComments, parentTo1, + 'file_added_in_rev2.txt', 'comment'), ''); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + 'file_added_in_rev2.txt', 'comment'), ''); + assert.equal( + element._computeCommentsStringMobile( + element.changeComments, + parentTo1, + 'file_added_in_rev2.txt' + ), ''); + assert.equal( + element._computeCommentsStringMobile(element.changeComments, _1To2, + 'file_added_in_rev2.txt'), ''); + assert.equal( + element._computeDraftsString(element.changeComments, parentTo1, + 'file_added_in_rev2.txt'), ''); + assert.equal( + element._computeDraftsString(element.changeComments, _1To2, + 'file_added_in_rev2.txt'), ''); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, parentTo1, + 'file_added_in_rev2.txt'), ''); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, _1To2, + 'file_added_in_rev2.txt'), ''); + assert.equal( + element._computeCommentsString(element.changeComments, parentTo2, + '/COMMIT_MSG', 'comment'), '1 comment'); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)'); + assert.equal( + element._computeCommentsStringMobile( + element.changeComments, + parentTo2, + '/COMMIT_MSG' + ), '1c'); + assert.equal( + element._computeCommentsStringMobile(element.changeComments, _1To2, + '/COMMIT_MSG'), '3c'); + assert.equal( + element._computeDraftsString(element.changeComments, parentTo1, + '/COMMIT_MSG'), '2 drafts'); + assert.equal( + element._computeDraftsString(element.changeComments, _1To2, + '/COMMIT_MSG'), '2 drafts'); + assert.equal( + element._computeDraftsStringMobile( + element.changeComments, + parentTo1, + '/COMMIT_MSG' + ), '2d'); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, _1To2, + '/COMMIT_MSG'), '2d'); + assert.equal( + element._computeCommentsString(element.changeComments, parentTo2, + 'myfile.txt', 'comment'), '2 comments'); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + 'myfile.txt', 'comment'), '3 comments'); + assert.equal( + element._computeCommentsStringMobile( + element.changeComments, + parentTo2, + 'myfile.txt' + ), '2c'); + assert.equal( + element._computeCommentsStringMobile(element.changeComments, _1To2, + 'myfile.txt'), '3c'); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, parentTo2, + 'myfile.txt'), ''); + assert.equal( + element._computeDraftsStringMobile(element.changeComments, _1To2, + 'myfile.txt'), ''); + assert.equal( + element._computeCommentsString(element.changeComments, parentTo2, + 'file_added_in_rev2.txt', 'comment'), ''); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + 'file_added_in_rev2.txt', 'comment'), ''); + assert.equal( + element._computeCommentsString(element.changeComments, parentTo2, + 'unresolved.file', 'comment'), '3 comments (1 unresolved)'); + assert.equal( + element._computeCommentsString(element.changeComments, _1To2, + 'unresolved.file', 'comment'), '3 comments (1 unresolved)'); + }); - const _1To2 = { - basePatchNum: '1', - patchNum: '2', - }; + test('_reviewedTitle', () => { + assert.equal( + element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)'); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo1, - '/COMMIT_MSG', 'comment'), '2 comments (1 unresolved)'); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)'); - assert.equal( - element._computeCommentsStringMobile(element.changeComments, parentTo1 - , '/COMMIT_MSG'), '2c'); - assert.equal( - element._computeCommentsStringMobile(element.changeComments, _1To2 - , '/COMMIT_MSG'), '3c'); - assert.equal( - element._computeDraftsString(element.changeComments, parentTo1, - 'unresolved.file'), '1 draft'); - assert.equal( - element._computeDraftsString(element.changeComments, _1To2, - 'unresolved.file'), '1 draft'); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, parentTo1, - 'unresolved.file'), '1d'); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, _1To2, - 'unresolved.file'), '1d'); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo1, - 'myfile.txt', 'comment'), '1 comment'); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - 'myfile.txt', 'comment'), '3 comments'); - assert.equal( - element._computeCommentsStringMobile( - element.changeComments, - parentTo1, - 'myfile.txt' - ), '1c'); - assert.equal( - element._computeCommentsStringMobile(element.changeComments, _1To2, - 'myfile.txt'), '3c'); - assert.equal( - element._computeDraftsString(element.changeComments, parentTo1, - 'myfile.txt'), ''); - assert.equal( - element._computeDraftsString(element.changeComments, _1To2, - 'myfile.txt'), ''); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, parentTo1, - 'myfile.txt'), ''); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, _1To2, - 'myfile.txt'), ''); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo1, - 'file_added_in_rev2.txt', 'comment'), ''); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - 'file_added_in_rev2.txt', 'comment'), ''); - assert.equal( - element._computeCommentsStringMobile( - element.changeComments, - parentTo1, - 'file_added_in_rev2.txt' - ), ''); - assert.equal( - element._computeCommentsStringMobile(element.changeComments, _1To2, - 'file_added_in_rev2.txt'), ''); - assert.equal( - element._computeDraftsString(element.changeComments, parentTo1, - 'file_added_in_rev2.txt'), ''); - assert.equal( - element._computeDraftsString(element.changeComments, _1To2, - 'file_added_in_rev2.txt'), ''); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, parentTo1, - 'file_added_in_rev2.txt'), ''); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, _1To2, - 'file_added_in_rev2.txt'), ''); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo2, - '/COMMIT_MSG', 'comment'), '1 comment'); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - '/COMMIT_MSG', 'comment'), '3 comments (1 unresolved)'); - assert.equal( - element._computeCommentsStringMobile( - element.changeComments, - parentTo2, - '/COMMIT_MSG' - ), '1c'); - assert.equal( - element._computeCommentsStringMobile(element.changeComments, _1To2, - '/COMMIT_MSG'), '3c'); - assert.equal( - element._computeDraftsString(element.changeComments, parentTo1, - '/COMMIT_MSG'), '2 drafts'); - assert.equal( - element._computeDraftsString(element.changeComments, _1To2, - '/COMMIT_MSG'), '2 drafts'); - assert.equal( - element._computeDraftsStringMobile( - element.changeComments, - parentTo1, - '/COMMIT_MSG' - ), '2d'); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, _1To2, - '/COMMIT_MSG'), '2d'); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo2, - 'myfile.txt', 'comment'), '2 comments'); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - 'myfile.txt', 'comment'), '3 comments'); - assert.equal( - element._computeCommentsStringMobile( - element.changeComments, - parentTo2, - 'myfile.txt' - ), '2c'); - assert.equal( - element._computeCommentsStringMobile(element.changeComments, _1To2, - 'myfile.txt'), '3c'); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, parentTo2, - 'myfile.txt'), ''); - assert.equal( - element._computeDraftsStringMobile(element.changeComments, _1To2, - 'myfile.txt'), ''); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo2, - 'file_added_in_rev2.txt', 'comment'), ''); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - 'file_added_in_rev2.txt', 'comment'), ''); - assert.equal( - element._computeCommentsString(element.changeComments, parentTo2, - 'unresolved.file', 'comment'), '3 comments (1 unresolved)'); - assert.equal( - element._computeCommentsString(element.changeComments, _1To2, - 'unresolved.file', 'comment'), '3 comments (1 unresolved)'); - }); + assert.equal( + element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)'); + }); - test('_reviewedTitle', () => { - assert.equal( - element._reviewedTitle(true), 'Mark as not reviewed (shortcut: r)'); - - assert.equal( - element._reviewedTitle(false), 'Mark as reviewed (shortcut: r)'); - }); - - suite('keyboard shortcuts', () => { - setup(() => { - element._filesByPath = { - '/COMMIT_MSG': {}, - 'file_added_in_rev2.txt': {}, - 'myfile.txt': {}, - }; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - element.change = {_number: 42}; - element.$.fileCursor.setCursorAtIndex(0); - }); - - test('toggle left diff via shortcut', () => { - const toggleLeftDiffStub = sandbox.stub(); - // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon. - // https://github.com/sinonjs/sinon/issues/781 - const diffsStub = sinon.stub(element, 'diffs', { - get() { - return [{toggleLeftDiff: toggleLeftDiffStub}]; - }, - }); - MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); - assert.isTrue(toggleLeftDiffStub.calledOnce); - diffsStub.restore(); - }); - - test('keyboard shortcuts', () => { - flushAsynchronousOperations(); - - const items = Polymer.dom(element.root).querySelectorAll('.file-row'); - element.$.fileCursor.stops = items; - element.$.fileCursor.setCursorAtIndex(0); - assert.equal(items.length, 3); - assert.isTrue(items[0].classList.contains('selected')); - assert.isFalse(items[1].classList.contains('selected')); - assert.isFalse(items[2].classList.contains('selected')); - // j with a modifier should not move the cursor. - MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j'); - assert.equal(element.$.fileCursor.index, 0); - // down should not move the cursor. - MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down'); - assert.equal(element.$.fileCursor.index, 0); - - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - assert.equal(element.$.fileCursor.index, 1); - assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - - const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - assert.equal(element.$.fileCursor.index, 2); - assert.equal(element.selectedIndex, 2); - - // k with a modifier should not move the cursor. - MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k'); - assert.equal(element.$.fileCursor.index, 2); - - // up should not move the cursor. - MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down'); - assert.equal(element.$.fileCursor.index, 2); - - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - assert.equal(element.$.fileCursor.index, 1); - assert.equal(element.selectedIndex, 1); - MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o'); - - assert(navStub.lastCall.calledWith(element.change, - 'file_added_in_rev2.txt', '2'), - 'Should navigate to /c/42/2/file_added_in_rev2.txt'); - - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); - assert.equal(element.$.fileCursor.index, 0); - assert.equal(element.selectedIndex, 0); - - const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor, - 'createCommentInPlace'); - MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c'); - assert.isTrue(createCommentInPlaceStub.called); - }); - - test('i key shows/hides selected inline diff', () => { - const paths = Object.keys(element._filesByPath); - sandbox.stub(element, '_expandedPathsChanged'); - flushAsynchronousOperations(); - const files = Polymer.dom(element.root).querySelectorAll('.file-row'); - element.$.fileCursor.stops = files; - element.$.fileCursor.setCursorAtIndex(0); - assert.equal(element.diffs.length, 0); - assert.equal(element._expandedFilePaths.length, 0); - - MockInteractions.keyUpOn(element, 73, null, 'i'); - flushAsynchronousOperations(); - assert.equal(element.diffs.length, 1); - assert.equal(element.diffs[0].path, paths[0]); - assert.equal(element._expandedFilePaths.length, 1); - assert.equal(element._expandedFilePaths[0], paths[0]); - - MockInteractions.keyUpOn(element, 73, null, 'i'); - flushAsynchronousOperations(); - assert.equal(element.diffs.length, 0); - assert.equal(element._expandedFilePaths.length, 0); - - element.$.fileCursor.setCursorAtIndex(1); - MockInteractions.keyUpOn(element, 73, null, 'i'); - flushAsynchronousOperations(); - assert.equal(element.diffs.length, 1); - assert.equal(element.diffs[0].path, paths[1]); - assert.equal(element._expandedFilePaths.length, 1); - assert.equal(element._expandedFilePaths[0], paths[1]); - - MockInteractions.keyUpOn(element, 73, 'shift', 'i'); - flushAsynchronousOperations(); - assert.equal(element.diffs.length, paths.length); - assert.equal(element._expandedFilePaths.length, paths.length); - for (const index in element.diffs) { - if (!element.diffs.hasOwnProperty(index)) { continue; } - assert.include(element._expandedFilePaths, element.diffs[index].path); - } - - MockInteractions.keyUpOn(element, 73, 'shift', 'i'); - flushAsynchronousOperations(); - assert.equal(element.diffs.length, 0); - assert.equal(element._expandedFilePaths.length, 0); - }); - - test('r key toggles reviewed flag', () => { - const reducer = (accum, file) => (file.isReviewed ? ++accum : accum); - const getNumReviewed = () => element._files.reduce(reducer, 0); - flushAsynchronousOperations(); - - // Default state should be unreviewed. - assert.equal(getNumReviewed(), 0); - - // Press the review key to toggle it (set the flag). - MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); - flushAsynchronousOperations(); - assert.equal(getNumReviewed(), 1); - - // Press the review key to toggle it (clear the flag). - MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); - assert.equal(getNumReviewed(), 0); - }); - - suite('_handleOpenFile', () => { - let interact; - - setup(() => { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut') - .returns(false); - sandbox.stub(element, 'modifierPressed').returns(false); - const openCursorStub = sandbox.stub(element, '_openCursorFile'); - const openSelectedStub = sandbox.stub(element, '_openSelectedFile'); - const expandStub = sandbox.stub(element, '_togglePathExpanded'); - - interact = function(opt_payload) { - openCursorStub.reset(); - openSelectedStub.reset(); - expandStub.reset(); - - const e = new CustomEvent('fake-keyboard-event', opt_payload); - sinon.stub(e, 'preventDefault'); - element._handleOpenFile(e); - assert.isTrue(e.preventDefault.called); - const result = {}; - if (openCursorStub.called) { - result.opened_cursor = true; - } - if (openSelectedStub.called) { - result.opened_selected = true; - } - if (expandStub.called) { - result.expanded = true; - } - return result; - }; - }); - - test('open from selected file', () => { - element._showInlineDiffs = false; - assert.deepEqual(interact(), {opened_selected: true}); - }); - - test('open from diff cursor', () => { - element._showInlineDiffs = true; - assert.deepEqual(interact(), {opened_cursor: true}); - }); - - test('expand when user prefers', () => { - element._showInlineDiffs = false; - assert.deepEqual(interact(), {opened_selected: true}); - element._userPrefs = {}; - assert.deepEqual(interact(), {opened_selected: true}); - }); - }); - - test('shift+left/shift+right', () => { - const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft'); - const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight'); - - let noDiffsExpanded = true; - sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded); - - MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left'); - assert.isFalse(moveLeftStub.called); - MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right'); - assert.isFalse(moveRightStub.called); - - noDiffsExpanded = false; - - MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left'); - assert.isTrue(moveLeftStub.called); - MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right'); - assert.isTrue(moveRightStub.called); - }); - }); - - test('computed properties', () => { - assert.equal(element._computeFileStatus('A'), 'A'); - assert.equal(element._computeFileStatus(undefined), 'M'); - assert.equal(element._computeFileStatus(null), 'M'); - - assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz'); - assert.equal(element._computeClass('clazz', '/COMMIT_MSG'), - 'clazz invisible'); - }); - - test('file review status', () => { - element._reviewed = ['/COMMIT_MSG', 'myfile.txt']; + suite('keyboard shortcuts', () => { + setup(() => { element._filesByPath = { '/COMMIT_MSG': {}, 'file_added_in_rev2.txt': {}, 'myfile.txt': {}, }; - element._loggedIn = true; element.changeNum = '42'; element.patchRange = { basePatchNum: 'PARENT', patchNum: '2', }; + element.change = {_number: 42}; element.$.fileCursor.setCursorAtIndex(0); + }); + test('toggle left diff via shortcut', () => { + const toggleLeftDiffStub = sandbox.stub(); + // Property getter cannot be stubbed w/ sandbox due to a bug in Sinon. + // https://github.com/sinonjs/sinon/issues/781 + const diffsStub = sinon.stub(element, 'diffs', { + get() { + return [{toggleLeftDiff: toggleLeftDiffStub}]; + }, + }); + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); + assert.isTrue(toggleLeftDiffStub.calledOnce); + diffsStub.restore(); + }); + + test('keyboard shortcuts', () => { flushAsynchronousOperations(); - const fileRows = - Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)'); - const checkSelector = 'input.reviewed[type="checkbox"]'; - const commitMsg = fileRows[0].querySelector(checkSelector); - const fileAdded = fileRows[1].querySelector(checkSelector); - const myFile = fileRows[2].querySelector(checkSelector); - assert.isTrue(commitMsg.checked); - assert.isFalse(fileAdded.checked); - assert.isTrue(myFile.checked); + const items = dom(element.root).querySelectorAll('.file-row'); + element.$.fileCursor.stops = items; + element.$.fileCursor.setCursorAtIndex(0); + assert.equal(items.length, 3); + assert.isTrue(items[0].classList.contains('selected')); + assert.isFalse(items[1].classList.contains('selected')); + assert.isFalse(items[2].classList.contains('selected')); + // j with a modifier should not move the cursor. + MockInteractions.pressAndReleaseKeyOn(element, 74, 'shift', 'j'); + assert.equal(element.$.fileCursor.index, 0); + // down should not move the cursor. + MockInteractions.pressAndReleaseKeyOn(element, 40, null, 'down'); + assert.equal(element.$.fileCursor.index, 0); - const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel'); - const markReviewLabel = commitMsg.nextElementSibling; - assert.isTrue(commitReviewLabel.classList.contains('isReviewed')); - assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED'); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + assert.equal(element.$.fileCursor.index, 1); + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - const clickSpy = sandbox.spy(element, '_handleFileListClick'); - MockInteractions.tap(markReviewLabel); - assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false)); - assert.isFalse(commitReviewLabel.classList.contains('isReviewed')); - assert.equal(markReviewLabel.textContent, 'MARK REVIEWED'); - assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented); + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + assert.equal(element.$.fileCursor.index, 2); + assert.equal(element.selectedIndex, 2); - MockInteractions.tap(markReviewLabel); - assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true)); - assert.isTrue(commitReviewLabel.classList.contains('isReviewed')); - assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED'); - assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented); + // k with a modifier should not move the cursor. + MockInteractions.pressAndReleaseKeyOn(element, 75, 'shift', 'k'); + assert.equal(element.$.fileCursor.index, 2); + + // up should not move the cursor. + MockInteractions.pressAndReleaseKeyOn(element, 38, null, 'down'); + assert.equal(element.$.fileCursor.index, 2); + + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + assert.equal(element.$.fileCursor.index, 1); + assert.equal(element.selectedIndex, 1); + MockInteractions.pressAndReleaseKeyOn(element, 79, null, 'o'); + + assert(navStub.lastCall.calledWith(element.change, + 'file_added_in_rev2.txt', '2'), + 'Should navigate to /c/42/2/file_added_in_rev2.txt'); + + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + MockInteractions.pressAndReleaseKeyOn(element, 75, null, 'k'); + assert.equal(element.$.fileCursor.index, 0); + assert.equal(element.selectedIndex, 0); + + const createCommentInPlaceStub = sandbox.stub(element.$.diffCursor, + 'createCommentInPlace'); + MockInteractions.pressAndReleaseKeyOn(element, 67, null, 'c'); + assert.isTrue(createCommentInPlaceStub.called); }); - test('_computeFileStatusLabel', () => { - assert.equal(element._computeFileStatusLabel('A'), 'Added'); - assert.equal(element._computeFileStatusLabel('M'), 'Modified'); + test('i key shows/hides selected inline diff', () => { + const paths = Object.keys(element._filesByPath); + sandbox.stub(element, '_expandedPathsChanged'); + flushAsynchronousOperations(); + const files = dom(element.root).querySelectorAll('.file-row'); + element.$.fileCursor.stops = files; + element.$.fileCursor.setCursorAtIndex(0); + assert.equal(element.diffs.length, 0); + assert.equal(element._expandedFilePaths.length, 0); + + MockInteractions.keyUpOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + assert.equal(element.diffs.length, 1); + assert.equal(element.diffs[0].path, paths[0]); + assert.equal(element._expandedFilePaths.length, 1); + assert.equal(element._expandedFilePaths[0], paths[0]); + + MockInteractions.keyUpOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + assert.equal(element.diffs.length, 0); + assert.equal(element._expandedFilePaths.length, 0); + + element.$.fileCursor.setCursorAtIndex(1); + MockInteractions.keyUpOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + assert.equal(element.diffs.length, 1); + assert.equal(element.diffs[0].path, paths[1]); + assert.equal(element._expandedFilePaths.length, 1); + assert.equal(element._expandedFilePaths[0], paths[1]); + + MockInteractions.keyUpOn(element, 73, 'shift', 'i'); + flushAsynchronousOperations(); + assert.equal(element.diffs.length, paths.length); + assert.equal(element._expandedFilePaths.length, paths.length); + for (const index in element.diffs) { + if (!element.diffs.hasOwnProperty(index)) { continue; } + assert.include(element._expandedFilePaths, element.diffs[index].path); + } + + MockInteractions.keyUpOn(element, 73, 'shift', 'i'); + flushAsynchronousOperations(); + assert.equal(element.diffs.length, 0); + assert.equal(element._expandedFilePaths.length, 0); }); - test('_handleFileListClick', () => { - element._filesByPath = { - '/COMMIT_MSG': {}, - 'f1.txt': {}, - 'f2.txt': {}, - }; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; + test('r key toggles reviewed flag', () => { + const reducer = (accum, file) => (file.isReviewed ? ++accum : accum); + const getNumReviewed = () => element._files.reduce(reducer, 0); + flushAsynchronousOperations(); - const clickSpy = sandbox.spy(element, '_handleFileListClick'); - const reviewStub = sandbox.stub(element, '_reviewFile'); - const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded'); + // Default state should be unreviewed. + assert.equal(getNumReviewed(), 0); - const row = Polymer.dom(element.root) - .querySelector('.row[data-path="f1.txt"]'); + // Press the review key to toggle it (set the flag). + MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); + flushAsynchronousOperations(); + assert.equal(getNumReviewed(), 1); - // Click on the expand button, resulting in _togglePathExpanded being - // called and not resulting in a call to _reviewFile. - row.querySelector('div.show-hide').click(); - assert.isTrue(clickSpy.calledOnce); - assert.isTrue(toggleExpandSpy.calledOnce); + // Press the review key to toggle it (clear the flag). + MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); + assert.equal(getNumReviewed(), 0); + }); + + suite('_handleOpenFile', () => { + let interact; + + setup(() => { + sandbox.stub(element, 'shouldSuppressKeyboardShortcut') + .returns(false); + sandbox.stub(element, 'modifierPressed').returns(false); + const openCursorStub = sandbox.stub(element, '_openCursorFile'); + const openSelectedStub = sandbox.stub(element, '_openSelectedFile'); + const expandStub = sandbox.stub(element, '_togglePathExpanded'); + + interact = function(opt_payload) { + openCursorStub.reset(); + openSelectedStub.reset(); + expandStub.reset(); + + const e = new CustomEvent('fake-keyboard-event', opt_payload); + sinon.stub(e, 'preventDefault'); + element._handleOpenFile(e); + assert.isTrue(e.preventDefault.called); + const result = {}; + if (openCursorStub.called) { + result.opened_cursor = true; + } + if (openSelectedStub.called) { + result.opened_selected = true; + } + if (expandStub.called) { + result.expanded = true; + } + return result; + }; + }); + + test('open from selected file', () => { + element._showInlineDiffs = false; + assert.deepEqual(interact(), {opened_selected: true}); + }); + + test('open from diff cursor', () => { + element._showInlineDiffs = true; + assert.deepEqual(interact(), {opened_cursor: true}); + }); + + test('expand when user prefers', () => { + element._showInlineDiffs = false; + assert.deepEqual(interact(), {opened_selected: true}); + element._userPrefs = {}; + assert.deepEqual(interact(), {opened_selected: true}); + }); + }); + + test('shift+left/shift+right', () => { + const moveLeftStub = sandbox.stub(element.$.diffCursor, 'moveLeft'); + const moveRightStub = sandbox.stub(element.$.diffCursor, 'moveRight'); + + let noDiffsExpanded = true; + sandbox.stub(element, '_noDiffsExpanded', () => noDiffsExpanded); + + MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left'); + assert.isFalse(moveLeftStub.called); + MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right'); + assert.isFalse(moveRightStub.called); + + noDiffsExpanded = false; + + MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'left'); + assert.isTrue(moveLeftStub.called); + MockInteractions.pressAndReleaseKeyOn(element, 73, 'shift', 'right'); + assert.isTrue(moveRightStub.called); + }); + }); + + test('computed properties', () => { + assert.equal(element._computeFileStatus('A'), 'A'); + assert.equal(element._computeFileStatus(undefined), 'M'); + assert.equal(element._computeFileStatus(null), 'M'); + + assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz'); + assert.equal(element._computeClass('clazz', '/COMMIT_MSG'), + 'clazz invisible'); + }); + + test('file review status', () => { + element._reviewed = ['/COMMIT_MSG', 'myfile.txt']; + element._filesByPath = { + '/COMMIT_MSG': {}, + 'file_added_in_rev2.txt': {}, + 'myfile.txt': {}, + }; + element._loggedIn = true; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + element.$.fileCursor.setCursorAtIndex(0); + + flushAsynchronousOperations(); + const fileRows = + dom(element.root).querySelectorAll('.row:not(.header-row)'); + const checkSelector = 'input.reviewed[type="checkbox"]'; + const commitMsg = fileRows[0].querySelector(checkSelector); + const fileAdded = fileRows[1].querySelector(checkSelector); + const myFile = fileRows[2].querySelector(checkSelector); + + assert.isTrue(commitMsg.checked); + assert.isFalse(fileAdded.checked); + assert.isTrue(myFile.checked); + + const commitReviewLabel = fileRows[0].querySelector('.reviewedLabel'); + const markReviewLabel = commitMsg.nextElementSibling; + assert.isTrue(commitReviewLabel.classList.contains('isReviewed')); + assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED'); + + const clickSpy = sandbox.spy(element, '_handleFileListClick'); + MockInteractions.tap(markReviewLabel); + assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', false)); + assert.isFalse(commitReviewLabel.classList.contains('isReviewed')); + assert.equal(markReviewLabel.textContent, 'MARK REVIEWED'); + assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented); + + MockInteractions.tap(markReviewLabel); + assert.isTrue(saveStub.lastCall.calledWithExactly('/COMMIT_MSG', true)); + assert.isTrue(commitReviewLabel.classList.contains('isReviewed')); + assert.equal(markReviewLabel.textContent, 'MARK UNREVIEWED'); + assert.isTrue(clickSpy.lastCall.args[0].defaultPrevented); + }); + + test('_computeFileStatusLabel', () => { + assert.equal(element._computeFileStatusLabel('A'), 'Added'); + assert.equal(element._computeFileStatusLabel('M'), 'Modified'); + }); + + test('_handleFileListClick', () => { + element._filesByPath = { + '/COMMIT_MSG': {}, + 'f1.txt': {}, + 'f2.txt': {}, + }; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + + const clickSpy = sandbox.spy(element, '_handleFileListClick'); + const reviewStub = sandbox.stub(element, '_reviewFile'); + const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded'); + + const row = dom(element.root) + .querySelector('.row[data-path="f1.txt"]'); + + // Click on the expand button, resulting in _togglePathExpanded being + // called and not resulting in a call to _reviewFile. + row.querySelector('div.show-hide').click(); + assert.isTrue(clickSpy.calledOnce); + assert.isTrue(toggleExpandSpy.calledOnce); + assert.isFalse(reviewStub.called); + + // Click inside the diff. This should result in no additional calls to + // _togglePathExpanded or _reviewFile. + dom(element.root).querySelector('gr-diff-host') + .click(); + assert.isTrue(clickSpy.calledTwice); + assert.isTrue(toggleExpandSpy.calledOnce); + assert.isFalse(reviewStub.called); + + // Click the reviewed checkbox, resulting in a call to _reviewFile, but + // no additional call to _togglePathExpanded. + row.querySelector('.markReviewed').click(); + assert.isTrue(clickSpy.calledThrice); + assert.isTrue(toggleExpandSpy.calledOnce); + assert.isTrue(reviewStub.calledOnce); + }); + + test('_handleFileListClick editMode', () => { + element._filesByPath = { + '/COMMIT_MSG': {}, + 'f1.txt': {}, + 'f2.txt': {}, + }; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + element.editMode = true; + flushAsynchronousOperations(); + const clickSpy = sandbox.spy(element, '_handleFileListClick'); + const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded'); + + // Tap the edit controls. Should be ignored by _handleFileListClick. + MockInteractions.tap(element.shadowRoot + .querySelector('.editFileControls')); + assert.isTrue(clickSpy.calledOnce); + assert.isFalse(toggleExpandSpy.called); + }); + + test('patch set from revisions', () => { + const expected = [ + {num: 4, desc: 'test', sha: 'rev4'}, + {num: 3, desc: 'test', sha: 'rev3'}, + {num: 2, desc: 'test', sha: 'rev2'}, + {num: 1, desc: 'test', sha: 'rev1'}, + ]; + const patchNums = element.computeAllPatchSets({ + revisions: { + rev3: {_number: 3, description: 'test', date: 3}, + rev1: {_number: 1, description: 'test', date: 1}, + rev4: {_number: 4, description: 'test', date: 4}, + rev2: {_number: 2, description: 'test', date: 2}, + }, + }); + assert.equal(patchNums.length, expected.length); + for (let i = 0; i < expected.length; i++) { + assert.deepEqual(patchNums[i], expected[i]); + } + }); + + test('checkbox shows/hides diff inline', () => { + element._filesByPath = { + 'myfile.txt': {}, + }; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + element.$.fileCursor.setCursorAtIndex(0); + sandbox.stub(element, '_expandedPathsChanged'); + flushAsynchronousOperations(); + const fileRows = + dom(element.root).querySelectorAll('.row:not(.header-row)'); + // Because the label surrounds the input, the tap event is triggered + // there first. + const showHideLabel = fileRows[0].querySelector('label.show-hide'); + const showHideCheck = fileRows[0].querySelector( + 'input.show-hide[type="checkbox"]'); + assert.isNotOk(showHideCheck.checked); + MockInteractions.tap(showHideLabel); + assert.isOk(showHideCheck.checked); + assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1); + }); + + test('diff mode correctly toggles the diffs', () => { + element._filesByPath = { + 'myfile.txt': {}, + }; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + sandbox.spy(element, '_updateDiffPreferences'); + element.$.fileCursor.setCursorAtIndex(0); + flushAsynchronousOperations(); + + // Tap on a file to generate the diff. + const row = dom(element.root) + .querySelectorAll('.row:not(.header-row) label.show-hide')[0]; + + MockInteractions.tap(row); + flushAsynchronousOperations(); + const diffDisplay = element.diffs[0]; + element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; + element.set('diffViewMode', 'UNIFIED_DIFF'); + assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF'); + assert.isTrue(element._updateDiffPreferences.called); + }); + + test('expanded attribute not set on path when not expanded', () => { + element._filesByPath = { + '/COMMIT_MSG': {}, + }; + assert.isNotOk(element.shadowRoot + .querySelector('.expanded')); + }); + + test('tapping row ignores links', () => { + element._filesByPath = { + '/COMMIT_MSG': {}, + }; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + sandbox.stub(element, '_expandedPathsChanged'); + flushAsynchronousOperations(); + const commitMsgFile = dom(element.root) + .querySelectorAll('.row:not(.header-row) a.pathLink')[0]; + + // Remove href attribute so the app doesn't route to a diff view + commitMsgFile.removeAttribute('href'); + const togglePathSpy = sandbox.spy(element, '_togglePathExpanded'); + + MockInteractions.tap(commitMsgFile); + flushAsynchronousOperations(); + assert(togglePathSpy.notCalled, 'file is opened as diff view'); + assert.isNotOk(element.shadowRoot + .querySelector('.expanded')); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.show-hide')).display, + 'none'); + }); + + test('_togglePathExpanded', () => { + const path = 'path/to/my/file.txt'; + element._filesByPath = {[path]: {}}; + const renderSpy = sandbox.spy(element, '_renderInOrder'); + const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs'); + + assert.equal(element.shadowRoot + .querySelector('iron-icon').icon, 'gr-icons:expand-more'); + assert.equal(element._expandedFilePaths.length, 0); + element._togglePathExpanded(path); + flushAsynchronousOperations(); + assert.equal(collapseStub.lastCall.args[0].length, 0); + assert.equal(element.shadowRoot + .querySelector('iron-icon').icon, 'gr-icons:expand-less'); + + assert.equal(renderSpy.callCount, 1); + assert.include(element._expandedFilePaths, path); + element._togglePathExpanded(path); + flushAsynchronousOperations(); + + assert.equal(element.shadowRoot + .querySelector('iron-icon').icon, 'gr-icons:expand-more'); + assert.equal(renderSpy.callCount, 1); + assert.notInclude(element._expandedFilePaths, path); + assert.equal(collapseStub.lastCall.args[0].length, 1); + }); + + test('expandAllDiffs and collapseAllDiffs', () => { + const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs'); + const cursorUpdateStub = sandbox.stub(element.$.diffCursor, + 'handleDiffUpdate'); + + const path = 'path/to/my/file.txt'; + element._filesByPath = {[path]: {}}; + element.expandAllDiffs(); + flushAsynchronousOperations(); + assert.isTrue(element._showInlineDiffs); + assert.isTrue(cursorUpdateStub.calledOnce); + assert.equal(collapseStub.lastCall.args[0].length, 0); + + element.collapseAllDiffs(); + flushAsynchronousOperations(); + assert.equal(element._expandedFilePaths.length, 0); + assert.isFalse(element._showInlineDiffs); + assert.isTrue(cursorUpdateStub.calledTwice); + assert.equal(collapseStub.lastCall.args[0].length, 1); + }); + + test('_expandedPathsChanged', done => { + sandbox.stub(element, '_reviewFile'); + const path = 'path/to/my/file.txt'; + const diffs = [{ + path, + style: {}, + reload() { + done(); + }, + cancel() {}, + getCursorStops() { return []; }, + addEventListener(eventName, callback) { + callback(new Event(eventName)); + }, + }]; + sinon.stub(element, 'diffs', { + get() { return diffs; }, + }); + element.push('_expandedFilePaths', path); + }); + + test('_clearCollapsedDiffs', () => { + const diff = { + cancel: sinon.stub(), + clearDiffContent: sinon.stub(), + }; + element._clearCollapsedDiffs([diff]); + assert.isTrue(diff.cancel.calledOnce); + assert.isTrue(diff.clearDiffContent.calledOnce); + }); + + test('filesExpanded value updates to correct enum', () => { + element._filesByPath = { + 'foo.bar': {}, + 'baz.bar': {}, + }; + flushAsynchronousOperations(); + assert.equal(element.filesExpanded, + GrFileListConstants.FilesExpandedState.NONE); + element.push('_expandedFilePaths', 'baz.bar'); + flushAsynchronousOperations(); + assert.equal(element.filesExpanded, + GrFileListConstants.FilesExpandedState.SOME); + element.push('_expandedFilePaths', 'foo.bar'); + flushAsynchronousOperations(); + assert.equal(element.filesExpanded, + GrFileListConstants.FilesExpandedState.ALL); + element.collapseAllDiffs(); + flushAsynchronousOperations(); + assert.equal(element.filesExpanded, + GrFileListConstants.FilesExpandedState.NONE); + element.expandAllDiffs(); + flushAsynchronousOperations(); + assert.equal(element.filesExpanded, + GrFileListConstants.FilesExpandedState.ALL); + }); + + test('_renderInOrder', done => { + const reviewStub = sandbox.stub(element, '_reviewFile'); + let callCount = 0; + const diffs = [{ + path: 'p0', + style: {}, + reload() { + assert.equal(callCount++, 2); + return Promise.resolve(); + }, + }, { + path: 'p1', + style: {}, + reload() { + assert.equal(callCount++, 1); + return Promise.resolve(); + }, + }, { + path: 'p2', + style: {}, + reload() { + assert.equal(callCount++, 0); + return Promise.resolve(); + }, + }]; + element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3) + .then(() => { + assert.isFalse(reviewStub.called); + assert.isTrue(loadCommentSpy.called); + done(); + }); + }); + + test('_renderInOrder logged in', done => { + element._loggedIn = true; + const reviewStub = sandbox.stub(element, '_reviewFile'); + let callCount = 0; + const diffs = [{ + path: 'p0', + style: {}, + reload() { + assert.equal(reviewStub.callCount, 2); + assert.equal(callCount++, 2); + return Promise.resolve(); + }, + }, { + path: 'p1', + style: {}, + reload() { + assert.equal(reviewStub.callCount, 1); + assert.equal(callCount++, 1); + return Promise.resolve(); + }, + }, { + path: 'p2', + style: {}, + reload() { + assert.equal(reviewStub.callCount, 0); + assert.equal(callCount++, 0); + return Promise.resolve(); + }, + }]; + element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3) + .then(() => { + assert.equal(reviewStub.callCount, 3); + done(); + }); + }); + + test('_renderInOrder respects diffPrefs.manual_review', () => { + element._loggedIn = true; + element.diffPrefs = {manual_review: true}; + const reviewStub = sandbox.stub(element, '_reviewFile'); + const diffs = [{ + path: 'p', + style: {}, + reload() { return Promise.resolve(); }, + }]; + + return element._renderInOrder(['p'], diffs, 1).then(() => { assert.isFalse(reviewStub.called); + delete element.diffPrefs.manual_review; + return element._renderInOrder(['p'], diffs, 1).then(() => { + assert.isTrue(reviewStub.called); + assert.isTrue(reviewStub.calledWithExactly('p', true)); + }); + }); + }); - // Click inside the diff. This should result in no additional calls to - // _togglePathExpanded or _reviewFile. - Polymer.dom(element.root).querySelector('gr-diff-host') - .click(); - assert.isTrue(clickSpy.calledTwice); - assert.isTrue(toggleExpandSpy.calledOnce); - assert.isFalse(reviewStub.called); + test('_loadingChanged fired from reload in debouncer', done => { + sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([])); + element.changeNum = 123; + element.patchRange = {patchNum: 12}; + element._filesByPath = {'foo.bar': {}}; - // Click the reviewed checkbox, resulting in a call to _reviewFile, but - // no additional call to _togglePathExpanded. - row.querySelector('.markReviewed').click(); - assert.isTrue(clickSpy.calledThrice); - assert.isTrue(toggleExpandSpy.calledOnce); - assert.isTrue(reviewStub.calledOnce); + element.reload().then(() => { + assert.isFalse(element._loading); + element.flushDebouncer('loading-change'); + assert.isFalse(element.classList.contains('loading')); + done(); + }); + assert.isTrue(element._loading); + assert.isFalse(element.classList.contains('loading')); + element.flushDebouncer('loading-change'); + assert.isTrue(element.classList.contains('loading')); + }); + + test('_loadingChanged does not set class when there are no files', () => { + sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([])); + element.changeNum = 123; + element.patchRange = {patchNum: 12}; + element.reload(); + assert.isTrue(element._loading); + element.flushDebouncer('loading-change'); + assert.isFalse(element.classList.contains('loading')); + }); + }); + + suite('diff url file list', () => { + test('diff url', () => { + const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff') + .returns('/c/gerrit/+/1/1/index.php'); + const change = { + _number: 1, + project: 'gerrit', + }; + const path = 'index.php'; + const patchRange = { + patchNum: 1, + }; + assert.equal( + element._computeDiffURL(change, patchRange, path, false), + '/c/gerrit/+/1/1/index.php'); + diffStub.restore(); + }); + + test('diff url commit msg', () => { + const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff') + .returns('/c/gerrit/+/1/1//COMMIT_MSG'); + const change = { + _number: 1, + project: 'gerrit', + }; + const path = '/COMMIT_MSG'; + const patchRange = { + patchNum: 1, + }; + assert.equal( + element._computeDiffURL(change, patchRange, path, false), + '/c/gerrit/+/1/1//COMMIT_MSG'); + diffStub.restore(); + }); + }); + + suite('size bars', () => { + test('_computeSizeBarLayout', () => { + assert.isUndefined(element._computeSizeBarLayout(null)); + assert.isUndefined(element._computeSizeBarLayout({})); + assert.deepEqual(element._computeSizeBarLayout({base: []}), { + maxInserted: 0, + maxDeleted: 0, + maxAdditionWidth: 0, + maxDeletionWidth: 0, + deletionOffset: 0, }); - test('_handleFileListClick editMode', () => { - element._filesByPath = { - '/COMMIT_MSG': {}, - 'f1.txt': {}, - 'f2.txt': {}, - }; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; + const files = [ + {__path: '/COMMIT_MSG', lines_inserted: 10000}, + {__path: 'foo', lines_inserted: 4, lines_deleted: 10}, + {__path: 'bar', lines_inserted: 5, lines_deleted: 8}, + ]; + const layout = element._computeSizeBarLayout({base: files}); + assert.equal(layout.maxInserted, 5); + assert.equal(layout.maxDeleted, 10); + }); + + test('_computeBarAdditionWidth', () => { + const file = { + __path: 'foo/bar.baz', + lines_inserted: 5, + lines_deleted: 0, + }; + const stats = { + maxInserted: 10, + maxDeleted: 0, + maxAdditionWidth: 60, + maxDeletionWidth: 0, + deletionOffset: 60, + }; + + // Uses half the space when file is half the largest addition and there + // are no deletions. + assert.equal(element._computeBarAdditionWidth(file, stats), 30); + + // If there are no insetions, there is no width. + stats.maxInserted = 0; + assert.equal(element._computeBarAdditionWidth(file, stats), 0); + + // If the insertions is not present on the file, there is no width. + stats.maxInserted = 10; + file.lines_inserted = undefined; + assert.equal(element._computeBarAdditionWidth(file, stats), 0); + + // If the file is a commit message, returns zero. + file.lines_inserted = 5; + file.__path = '/COMMIT_MSG'; + assert.equal(element._computeBarAdditionWidth(file, stats), 0); + + // Width bottoms-out at the minimum width. + file.__path = 'stuff.txt'; + file.lines_inserted = 1; + stats.maxInserted = 1000000; + assert.equal(element._computeBarAdditionWidth(file, stats), 1.5); + }); + + test('_computeBarAdditionX', () => { + const file = { + __path: 'foo/bar.baz', + lines_inserted: 5, + lines_deleted: 0, + }; + const stats = { + maxInserted: 10, + maxDeleted: 0, + maxAdditionWidth: 60, + maxDeletionWidth: 0, + deletionOffset: 60, + }; + assert.equal(element._computeBarAdditionX(file, stats), 30); + }); + + test('_computeBarDeletionWidth', () => { + const file = { + __path: 'foo/bar.baz', + lines_inserted: 0, + lines_deleted: 5, + }; + const stats = { + maxInserted: 10, + maxDeleted: 10, + maxAdditionWidth: 30, + maxDeletionWidth: 30, + deletionOffset: 31, + }; + + // Uses a quarter the space when file is half the largest deletions and + // there are equal additions. + assert.equal(element._computeBarDeletionWidth(file, stats), 15); + + // If there are no deletions, there is no width. + stats.maxDeleted = 0; + assert.equal(element._computeBarDeletionWidth(file, stats), 0); + + // If the deletions is not present on the file, there is no width. + stats.maxDeleted = 10; + file.lines_deleted = undefined; + assert.equal(element._computeBarDeletionWidth(file, stats), 0); + + // If the file is a commit message, returns zero. + file.lines_deleted = 5; + file.__path = '/COMMIT_MSG'; + assert.equal(element._computeBarDeletionWidth(file, stats), 0); + + // Width bottoms-out at the minimum width. + file.__path = 'stuff.txt'; + file.lines_deleted = 1; + stats.maxDeleted = 1000000; + assert.equal(element._computeBarDeletionWidth(file, stats), 1.5); + }); + + test('_computeSizeBarsClass', () => { + assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'), + 'sizeBars desktop hide'); + assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'), + 'sizeBars desktop invisible'); + assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'), + 'sizeBars desktop '); + }); + }); + + suite('gr-file-list inline diff tests', () => { + let element; + let sandbox; + + const commitMsgComments = [ + { + patch_set: 2, + id: 'ecf0b9fa_fe1a5f62', + line: 20, + updated: '2018-02-08 18:49:18.000000000', + message: 'another comment', + unresolved: true, + }, + { + patch_set: 2, + id: '503008e2_0ab203ee', + line: 10, + updated: '2018-02-14 22:07:43.000000000', + message: 'a comment', + unresolved: true, + }, + { + patch_set: 2, + id: 'cc788d2c_cb1d728c', + line: 20, + in_reply_to: 'ecf0b9fa_fe1a5f62', + updated: '2018-02-13 22:07:43.000000000', + message: 'response', + unresolved: true, + }, + ]; + + const setupDiff = function(diff) { + const mock = document.createElement('mock-diff-response'); + diff.comments = { + left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [], + right: [], + meta: { + changeNum: 1, + patchRange: { + basePatchNum: 'PARENT', + patchNum: 2, + }, + }, + }; + diff.prefs = { + context: 10, + tab_size: 8, + font_size: 12, + line_length: 100, + cursor_blink_rate: 0, + line_wrapping: false, + intraline_difference: true, + show_line_endings: true, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + auto_hide_diff_table_header: true, + theme: 'DEFAULT', + ignore_whitespace: 'IGNORE_NONE', + }; + diff.diff = mock.diffResponse; + diff.$.diff.flushDebouncer('renderDiffTable'); + }; + + const renderAndGetNewDiffs = function(index) { + const diffs = + dom(element.root).querySelectorAll('gr-diff-host'); + + for (let i = index; i < diffs.length; i++) { + setupDiff(diffs[i]); + } + + element._updateDiffCursor(); + element.$.diffCursor.handleDiffUpdate(); + return diffs; + }; + + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getPreferences() { return Promise.resolve({}); }, + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + }); + stub('gr-date-formatter', { + _loadTimeFormat() { return Promise.resolve(''); }, + }); + stub('gr-diff-host', { + reload() { return Promise.resolve(); }, + }); + + // Element must be wrapped in an element with direct access to the + // comment API. + commentApiWrapper = fixture('basic'); + element = commentApiWrapper.$.fileList; + loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll'); + element.diffPrefs = {}; + sandbox.stub(element, '_reviewFile'); + + // Stub methods on the changeComments object after changeComments has + // been initialized. + commentApiWrapper.loadComments().then(() => { + sandbox.stub(element.changeComments, 'getPaths').returns({}); + sandbox.stub(element.changeComments, 'getCommentsBySideForPath') + .returns({meta: {}, left: [], right: []}); + done(); + }); + element._loading = false; + element.numFilesShown = 75; + element.selectedIndex = 0; + element._filesByPath = { + '/COMMIT_MSG': {lines_inserted: 9}, + 'file_added_in_rev2.txt': { + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + 'myfile.txt': { + lines_inserted: 1, + lines_deleted: 1, + size_delta: 10, + size: 100, + }, + }; + element._reviewed = ['/COMMIT_MSG', 'myfile.txt']; + element._loggedIn = true; + element.changeNum = '42'; + element.patchRange = { + basePatchNum: 'PARENT', + patchNum: '2', + }; + sandbox.stub(window, 'fetch', () => Promise.resolve()); + flushAsynchronousOperations(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('cursor with individually opened files', () => { + MockInteractions.keyUpOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + let diffs = renderAndGetNewDiffs(0); + const diffStops = diffs[0].getCursorStops(); + + // 1 diff should be rendered. + assert.equal(diffs.length, 1); + + // No line number is selected. + assert.isFalse(diffStops[10].classList.contains('target-row')); + + // Tapping content on a line selects the line number. + MockInteractions.tap(dom( + diffStops[10]).querySelectorAll('.contentText')[0]); + flushAsynchronousOperations(); + assert.isTrue(diffStops[10].classList.contains('target-row')); + + // Keyboard shortcuts are still moving the file cursor, not the diff + // cursor. + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + flushAsynchronousOperations(); + assert.isTrue(diffStops[10].classList.contains('target-row')); + assert.isFalse(diffStops[11].classList.contains('target-row')); + + // The file cusor is now at 1. + assert.equal(element.$.fileCursor.index, 1); + MockInteractions.keyUpOn(element, 73, null, 'i'); + flushAsynchronousOperations(); + + diffs = renderAndGetNewDiffs(1); + // Two diffs should be rendered. + assert.equal(diffs.length, 2); + const diffStopsFirst = diffs[0].getCursorStops(); + const diffStopsSecond = diffs[1].getCursorStops(); + + // The line on the first diff is stil selected + assert.isTrue(diffStopsFirst[10].classList.contains('target-row')); + assert.isFalse(diffStopsSecond[10].classList.contains('target-row')); + }); + + test('cursor with toggle all files', () => { + MockInteractions.keyUpOn(element, 73, 'shift', 'i'); + flushAsynchronousOperations(); + + const diffs = renderAndGetNewDiffs(0); + const diffStops = diffs[0].getCursorStops(); + + // 1 diff should be rendered. + assert.equal(diffs.length, 3); + + // No line number is selected. + assert.isFalse(diffStops[10].classList.contains('target-row')); + + // Tapping content on a line selects the line number. + MockInteractions.tap(dom( + diffStops[10]).querySelectorAll('.contentText')[0]); + flushAsynchronousOperations(); + assert.isTrue(diffStops[10].classList.contains('target-row')); + + // Keyboard shortcuts are still moving the file cursor, not the diff + // cursor. + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + flushAsynchronousOperations(); + assert.isFalse(diffStops[10].classList.contains('target-row')); + assert.isTrue(diffStops[11].classList.contains('target-row')); + + // The file cusor is still at 0. + assert.equal(element.$.fileCursor.index, 0); + }); + + suite('n key presses', () => { + let nKeySpy; + let nextCommentStub; + let nextChunkStub; + let fileRows; + + setup(() => { + sandbox.stub(element, '_renderInOrder').returns(Promise.resolve()); + nKeySpy = sandbox.spy(element, '_handleNextChunk'); + nextCommentStub = sandbox.stub(element.$.diffCursor, + 'moveToNextCommentThread'); + nextChunkStub = sandbox.stub(element.$.diffCursor, + 'moveToNextChunk'); + fileRows = + dom(element.root).querySelectorAll('.row:not(.header-row)'); + }); + + test('n key with some files expanded and no shift key', () => { + MockInteractions.keyUpOn(fileRows[0], 73, null, 'i'); + flushAsynchronousOperations(); + assert.equal(nextChunkStub.callCount, 1); + + // Handle N key should return before calling diff cursor functions. + MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); + assert.isTrue(nKeySpy.called); + assert.isFalse(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 2); + assert.equal(element.filesExpanded, 'some'); + }); + + test('n key with some files expanded and shift key', () => { + MockInteractions.keyUpOn(fileRows[0], 73, null, 'i'); + flushAsynchronousOperations(); + assert.equal(nextChunkStub.callCount, 1); + + MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); + assert.isTrue(nKeySpy.called); + assert.isTrue(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 1); + assert.equal(element.filesExpanded, 'some'); + }); + + test('n key without all files expanded and shift key', () => { + MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i'); + flushAsynchronousOperations(); + + MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); + assert.isTrue(nKeySpy.called); + assert.isFalse(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 2); + assert.isTrue(element._showInlineDiffs); + }); + + test('n key without all files expanded and no shift key', () => { + MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i'); + flushAsynchronousOperations(); + + MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); + assert.isTrue(nKeySpy.called); + assert.isTrue(nextCommentStub.called); + + // This is also called in diffCursor.moveToFirstChunk. + assert.equal(nextChunkStub.callCount, 1); + assert.isTrue(element._showInlineDiffs); + }); + }); + + test('_openSelectedFile behavior', () => { + const _filesByPath = element._filesByPath; + element.set('_filesByPath', {}); + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + // Noop when there are no files. + element._openSelectedFile(); + assert.isFalse(navStub.called); + + element.set('_filesByPath', _filesByPath); + flushAsynchronousOperations(); + // Navigates when a file is selected. + element._openSelectedFile(); + assert.isTrue(navStub.called); + }); + + test('_displayLine', () => { + sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false); + sandbox.stub(element, 'modifierPressed', () => false); + element._showInlineDiffs = true; + const mockEvent = {preventDefault() {}}; + + element._displayLine = false; + element._handleCursorNext(mockEvent); + assert.isTrue(element._displayLine); + + element._displayLine = false; + element._handleCursorPrev(mockEvent); + assert.isTrue(element._displayLine); + + element._displayLine = true; + element._handleEscKey(mockEvent); + assert.isFalse(element._displayLine); + }); + + suite('editMode behavior', () => { + test('reviewed checkbox', () => { + element._reviewFile.restore(); + const saveReviewStub = sandbox.stub(element, '_saveReviewedState'); + + element.editMode = false; + MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); + assert.isTrue(saveReviewStub.calledOnce); + element.editMode = true; flushAsynchronousOperations(); - const clickSpy = sandbox.spy(element, '_handleFileListClick'); - const toggleExpandSpy = sandbox.spy(element, '_togglePathExpanded'); - // Tap the edit controls. Should be ignored by _handleFileListClick. - MockInteractions.tap(element.shadowRoot - .querySelector('.editFileControls')); - assert.isTrue(clickSpy.calledOnce); - assert.isFalse(toggleExpandSpy.called); + MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); + assert.isTrue(saveReviewStub.calledOnce); }); - test('patch set from revisions', () => { - const expected = [ - {num: 4, desc: 'test', sha: 'rev4'}, - {num: 3, desc: 'test', sha: 'rev3'}, - {num: 2, desc: 'test', sha: 'rev2'}, - {num: 1, desc: 'test', sha: 'rev1'}, - ]; - const patchNums = element.computeAllPatchSets({ - revisions: { - rev3: {_number: 3, description: 'test', date: 3}, - rev1: {_number: 1, description: 'test', date: 1}, - rev4: {_number: 4, description: 'test', date: 4}, - rev2: {_number: 2, description: 'test', date: 2}, - }, + test('_getReviewedFiles does not call API', () => { + const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles'); + element.editMode = true; + return element._getReviewedFiles().then(files => { + assert.equal(files.length, 0); + assert.isFalse(apiSpy.called); }); - assert.equal(patchNums.length, expected.length); - for (let i = 0; i < expected.length; i++) { - assert.deepEqual(patchNums[i], expected[i]); - } - }); - - test('checkbox shows/hides diff inline', () => { - element._filesByPath = { - 'myfile.txt': {}, - }; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - element.$.fileCursor.setCursorAtIndex(0); - sandbox.stub(element, '_expandedPathsChanged'); - flushAsynchronousOperations(); - const fileRows = - Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)'); - // Because the label surrounds the input, the tap event is triggered - // there first. - const showHideLabel = fileRows[0].querySelector('label.show-hide'); - const showHideCheck = fileRows[0].querySelector( - 'input.show-hide[type="checkbox"]'); - assert.isNotOk(showHideCheck.checked); - MockInteractions.tap(showHideLabel); - assert.isOk(showHideCheck.checked); - assert.notEqual(element._expandedFilePaths.indexOf('myfile.txt'), -1); - }); - - test('diff mode correctly toggles the diffs', () => { - element._filesByPath = { - 'myfile.txt': {}, - }; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - sandbox.spy(element, '_updateDiffPreferences'); - element.$.fileCursor.setCursorAtIndex(0); - flushAsynchronousOperations(); - - // Tap on a file to generate the diff. - const row = Polymer.dom(element.root) - .querySelectorAll('.row:not(.header-row) label.show-hide')[0]; - - MockInteractions.tap(row); - flushAsynchronousOperations(); - const diffDisplay = element.diffs[0]; - element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; - element.set('diffViewMode', 'UNIFIED_DIFF'); - assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF'); - assert.isTrue(element._updateDiffPreferences.called); - }); - - test('expanded attribute not set on path when not expanded', () => { - element._filesByPath = { - '/COMMIT_MSG': {}, - }; - assert.isNotOk(element.shadowRoot - .querySelector('.expanded')); - }); - - test('tapping row ignores links', () => { - element._filesByPath = { - '/COMMIT_MSG': {}, - }; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - sandbox.stub(element, '_expandedPathsChanged'); - flushAsynchronousOperations(); - const commitMsgFile = Polymer.dom(element.root) - .querySelectorAll('.row:not(.header-row) a.pathLink')[0]; - - // Remove href attribute so the app doesn't route to a diff view - commitMsgFile.removeAttribute('href'); - const togglePathSpy = sandbox.spy(element, '_togglePathExpanded'); - - MockInteractions.tap(commitMsgFile); - flushAsynchronousOperations(); - assert(togglePathSpy.notCalled, 'file is opened as diff view'); - assert.isNotOk(element.shadowRoot - .querySelector('.expanded')); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.show-hide')).display, - 'none'); - }); - - test('_togglePathExpanded', () => { - const path = 'path/to/my/file.txt'; - element._filesByPath = {[path]: {}}; - const renderSpy = sandbox.spy(element, '_renderInOrder'); - const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs'); - - assert.equal(element.shadowRoot - .querySelector('iron-icon').icon, 'gr-icons:expand-more'); - assert.equal(element._expandedFilePaths.length, 0); - element._togglePathExpanded(path); - flushAsynchronousOperations(); - assert.equal(collapseStub.lastCall.args[0].length, 0); - assert.equal(element.shadowRoot - .querySelector('iron-icon').icon, 'gr-icons:expand-less'); - - assert.equal(renderSpy.callCount, 1); - assert.include(element._expandedFilePaths, path); - element._togglePathExpanded(path); - flushAsynchronousOperations(); - - assert.equal(element.shadowRoot - .querySelector('iron-icon').icon, 'gr-icons:expand-more'); - assert.equal(renderSpy.callCount, 1); - assert.notInclude(element._expandedFilePaths, path); - assert.equal(collapseStub.lastCall.args[0].length, 1); - }); - - test('expandAllDiffs and collapseAllDiffs', () => { - const collapseStub = sandbox.stub(element, '_clearCollapsedDiffs'); - const cursorUpdateStub = sandbox.stub(element.$.diffCursor, - 'handleDiffUpdate'); - - const path = 'path/to/my/file.txt'; - element._filesByPath = {[path]: {}}; - element.expandAllDiffs(); - flushAsynchronousOperations(); - assert.isTrue(element._showInlineDiffs); - assert.isTrue(cursorUpdateStub.calledOnce); - assert.equal(collapseStub.lastCall.args[0].length, 0); - - element.collapseAllDiffs(); - flushAsynchronousOperations(); - assert.equal(element._expandedFilePaths.length, 0); - assert.isFalse(element._showInlineDiffs); - assert.isTrue(cursorUpdateStub.calledTwice); - assert.equal(collapseStub.lastCall.args[0].length, 1); - }); - - test('_expandedPathsChanged', done => { - sandbox.stub(element, '_reviewFile'); - const path = 'path/to/my/file.txt'; - const diffs = [{ - path, - style: {}, - reload() { - done(); - }, - cancel() {}, - getCursorStops() { return []; }, - addEventListener(eventName, callback) { - callback(new Event(eventName)); - }, - }]; - sinon.stub(element, 'diffs', { - get() { return diffs; }, - }); - element.push('_expandedFilePaths', path); - }); - - test('_clearCollapsedDiffs', () => { - const diff = { - cancel: sinon.stub(), - clearDiffContent: sinon.stub(), - }; - element._clearCollapsedDiffs([diff]); - assert.isTrue(diff.cancel.calledOnce); - assert.isTrue(diff.clearDiffContent.calledOnce); - }); - - test('filesExpanded value updates to correct enum', () => { - element._filesByPath = { - 'foo.bar': {}, - 'baz.bar': {}, - }; - flushAsynchronousOperations(); - assert.equal(element.filesExpanded, - GrFileListConstants.FilesExpandedState.NONE); - element.push('_expandedFilePaths', 'baz.bar'); - flushAsynchronousOperations(); - assert.equal(element.filesExpanded, - GrFileListConstants.FilesExpandedState.SOME); - element.push('_expandedFilePaths', 'foo.bar'); - flushAsynchronousOperations(); - assert.equal(element.filesExpanded, - GrFileListConstants.FilesExpandedState.ALL); - element.collapseAllDiffs(); - flushAsynchronousOperations(); - assert.equal(element.filesExpanded, - GrFileListConstants.FilesExpandedState.NONE); - element.expandAllDiffs(); - flushAsynchronousOperations(); - assert.equal(element.filesExpanded, - GrFileListConstants.FilesExpandedState.ALL); - }); - - test('_renderInOrder', done => { - const reviewStub = sandbox.stub(element, '_reviewFile'); - let callCount = 0; - const diffs = [{ - path: 'p0', - style: {}, - reload() { - assert.equal(callCount++, 2); - return Promise.resolve(); - }, - }, { - path: 'p1', - style: {}, - reload() { - assert.equal(callCount++, 1); - return Promise.resolve(); - }, - }, { - path: 'p2', - style: {}, - reload() { - assert.equal(callCount++, 0); - return Promise.resolve(); - }, - }]; - element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3) - .then(() => { - assert.isFalse(reviewStub.called); - assert.isTrue(loadCommentSpy.called); - done(); - }); - }); - - test('_renderInOrder logged in', done => { - element._loggedIn = true; - const reviewStub = sandbox.stub(element, '_reviewFile'); - let callCount = 0; - const diffs = [{ - path: 'p0', - style: {}, - reload() { - assert.equal(reviewStub.callCount, 2); - assert.equal(callCount++, 2); - return Promise.resolve(); - }, - }, { - path: 'p1', - style: {}, - reload() { - assert.equal(reviewStub.callCount, 1); - assert.equal(callCount++, 1); - return Promise.resolve(); - }, - }, { - path: 'p2', - style: {}, - reload() { - assert.equal(reviewStub.callCount, 0); - assert.equal(callCount++, 0); - return Promise.resolve(); - }, - }]; - element._renderInOrder(['p2', 'p1', 'p0'], diffs, 3) - .then(() => { - assert.equal(reviewStub.callCount, 3); - done(); - }); - }); - - test('_renderInOrder respects diffPrefs.manual_review', () => { - element._loggedIn = true; - element.diffPrefs = {manual_review: true}; - const reviewStub = sandbox.stub(element, '_reviewFile'); - const diffs = [{ - path: 'p', - style: {}, - reload() { return Promise.resolve(); }, - }]; - - return element._renderInOrder(['p'], diffs, 1).then(() => { - assert.isFalse(reviewStub.called); - delete element.diffPrefs.manual_review; - return element._renderInOrder(['p'], diffs, 1).then(() => { - assert.isTrue(reviewStub.called); - assert.isTrue(reviewStub.calledWithExactly('p', true)); - }); - }); - }); - - test('_loadingChanged fired from reload in debouncer', done => { - sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([])); - element.changeNum = 123; - element.patchRange = {patchNum: 12}; - element._filesByPath = {'foo.bar': {}}; - - element.reload().then(() => { - assert.isFalse(element._loading); - element.flushDebouncer('loading-change'); - assert.isFalse(element.classList.contains('loading')); - done(); - }); - assert.isTrue(element._loading); - assert.isFalse(element.classList.contains('loading')); - element.flushDebouncer('loading-change'); - assert.isTrue(element.classList.contains('loading')); - }); - - test('_loadingChanged does not set class when there are no files', () => { - sandbox.stub(element, '_getReviewedFiles').returns(Promise.resolve([])); - element.changeNum = 123; - element.patchRange = {patchNum: 12}; - element.reload(); - assert.isTrue(element._loading); - element.flushDebouncer('loading-change'); - assert.isFalse(element.classList.contains('loading')); }); }); - suite('diff url file list', () => { - test('diff url', () => { - const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff') - .returns('/c/gerrit/+/1/1/index.php'); - const change = { - _number: 1, - project: 'gerrit', - }; - const path = 'index.php'; - const patchRange = { - patchNum: 1, - }; - assert.equal( - element._computeDiffURL(change, patchRange, path, false), - '/c/gerrit/+/1/1/index.php'); - diffStub.restore(); - }); + test('editing actions', () => { + // Edit controls are guarded behind a dom-if initially and not rendered. + assert.isNotOk(dom(element.root) + .querySelector('gr-edit-file-controls')); - test('diff url commit msg', () => { - const diffStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiff') - .returns('/c/gerrit/+/1/1//COMMIT_MSG'); - const change = { - _number: 1, - project: 'gerrit', - }; - const path = '/COMMIT_MSG'; - const patchRange = { - patchNum: 1, - }; - assert.equal( - element._computeDiffURL(change, patchRange, path, false), - '/c/gerrit/+/1/1//COMMIT_MSG'); - diffStub.restore(); - }); + element.editMode = true; + flushAsynchronousOperations(); + + // Commit message should not have edit controls. + const editControls = + Array.from( + dom(element.root) + .querySelectorAll('.row:not(.header-row)')) + .map(row => row.querySelector('gr-edit-file-controls')); + assert.isTrue(editControls[0].classList.contains('invisible')); }); - suite('size bars', () => { - test('_computeSizeBarLayout', () => { - assert.isUndefined(element._computeSizeBarLayout(null)); - assert.isUndefined(element._computeSizeBarLayout({})); - assert.deepEqual(element._computeSizeBarLayout({base: []}), { - maxInserted: 0, - maxDeleted: 0, - maxAdditionWidth: 0, - maxDeletionWidth: 0, - deletionOffset: 0, - }); + test('reloadCommentsForThreadWithRootId', () => { + // Expand the commit message diff + MockInteractions.keyUpOn(element, 73, 'shift', 'i'); + const diffs = renderAndGetNewDiffs(0); + flushAsynchronousOperations(); - const files = [ - {__path: '/COMMIT_MSG', lines_inserted: 10000}, - {__path: 'foo', lines_inserted: 4, lines_deleted: 10}, - {__path: 'bar', lines_inserted: 5, lines_deleted: 8}, - ]; - const layout = element._computeSizeBarLayout({base: files}); - assert.equal(layout.maxInserted, 5); - assert.equal(layout.maxDeleted, 10); - }); + // Two comment threads should be generated by renderAndGetNewDiffs + const threadEls = diffs[0].getThreadEls(); + assert.equal(threadEls.length, 2); + const threadElsByRootId = new Map( + threadEls.map(threadEl => [threadEl.rootId, threadEl])); - test('_computeBarAdditionWidth', () => { - const file = { - __path: 'foo/bar.baz', - lines_inserted: 5, - lines_deleted: 0, - }; - const stats = { - maxInserted: 10, - maxDeleted: 0, - maxAdditionWidth: 60, - maxDeletionWidth: 0, - deletionOffset: 60, - }; + const thread1 = threadElsByRootId.get('503008e2_0ab203ee'); + assert.equal(thread1.comments.length, 1); + assert.equal(thread1.comments[0].message, 'a comment'); + assert.equal(thread1.comments[0].line, 10); - // Uses half the space when file is half the largest addition and there - // are no deletions. - assert.equal(element._computeBarAdditionWidth(file, stats), 30); + const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62'); + assert.equal(thread2.comments.length, 2); + assert.isTrue(thread2.comments[0].unresolved); + assert.equal(thread2.comments[0].message, 'another comment'); + assert.equal(thread2.comments[0].line, 20); - // If there are no insetions, there is no width. - stats.maxInserted = 0; - assert.equal(element._computeBarAdditionWidth(file, stats), 0); - - // If the insertions is not present on the file, there is no width. - stats.maxInserted = 10; - file.lines_inserted = undefined; - assert.equal(element._computeBarAdditionWidth(file, stats), 0); - - // If the file is a commit message, returns zero. - file.lines_inserted = 5; - file.__path = '/COMMIT_MSG'; - assert.equal(element._computeBarAdditionWidth(file, stats), 0); - - // Width bottoms-out at the minimum width. - file.__path = 'stuff.txt'; - file.lines_inserted = 1; - stats.maxInserted = 1000000; - assert.equal(element._computeBarAdditionWidth(file, stats), 1.5); - }); - - test('_computeBarAdditionX', () => { - const file = { - __path: 'foo/bar.baz', - lines_inserted: 5, - lines_deleted: 0, - }; - const stats = { - maxInserted: 10, - maxDeleted: 0, - maxAdditionWidth: 60, - maxDeletionWidth: 0, - deletionOffset: 60, - }; - assert.equal(element._computeBarAdditionX(file, stats), 30); - }); - - test('_computeBarDeletionWidth', () => { - const file = { - __path: 'foo/bar.baz', - lines_inserted: 0, - lines_deleted: 5, - }; - const stats = { - maxInserted: 10, - maxDeleted: 10, - maxAdditionWidth: 30, - maxDeletionWidth: 30, - deletionOffset: 31, - }; - - // Uses a quarter the space when file is half the largest deletions and - // there are equal additions. - assert.equal(element._computeBarDeletionWidth(file, stats), 15); - - // If there are no deletions, there is no width. - stats.maxDeleted = 0; - assert.equal(element._computeBarDeletionWidth(file, stats), 0); - - // If the deletions is not present on the file, there is no width. - stats.maxDeleted = 10; - file.lines_deleted = undefined; - assert.equal(element._computeBarDeletionWidth(file, stats), 0); - - // If the file is a commit message, returns zero. - file.lines_deleted = 5; - file.__path = '/COMMIT_MSG'; - assert.equal(element._computeBarDeletionWidth(file, stats), 0); - - // Width bottoms-out at the minimum width. - file.__path = 'stuff.txt'; - file.lines_deleted = 1; - stats.maxDeleted = 1000000; - assert.equal(element._computeBarDeletionWidth(file, stats), 1.5); - }); - - test('_computeSizeBarsClass', () => { - assert.equal(element._computeSizeBarsClass(false, 'foo/bar.baz'), - 'sizeBars desktop hide'); - assert.equal(element._computeSizeBarsClass(true, '/COMMIT_MSG'), - 'sizeBars desktop invisible'); - assert.equal(element._computeSizeBarsClass(true, 'foo/bar.baz'), - 'sizeBars desktop '); - }); - }); - - suite('gr-file-list inline diff tests', () => { - let element; - let sandbox; - - const commitMsgComments = [ + const commentStub = + sandbox.stub(element.changeComments, 'getCommentsForThread'); + const commentStubRes1 = [ + { + patch_set: 2, + id: '503008e2_0ab203ee', + line: 20, + updated: '2018-02-08 18:49:18.000000000', + message: 'edited text', + unresolved: false, + }, + ]; + const commentStubRes2 = [ { patch_set: 2, id: 'ecf0b9fa_fe1a5f62', @@ -1446,453 +1856,58 @@ patch_set: 2, id: '503008e2_0ab203ee', line: 10, + in_reply_to: 'ecf0b9fa_fe1a5f62', updated: '2018-02-14 22:07:43.000000000', - message: 'a comment', + message: 'response', unresolved: true, }, { patch_set: 2, - id: 'cc788d2c_cb1d728c', + id: '503008e2_0ab203ef', line: 20, - in_reply_to: 'ecf0b9fa_fe1a5f62', - updated: '2018-02-13 22:07:43.000000000', - message: 'response', + in_reply_to: '503008e2_0ab203ee', + updated: '2018-02-15 22:07:43.000000000', + message: 'a third comment in the thread', unresolved: true, }, ]; + commentStub.withArgs('503008e2_0ab203ee').returns( + commentStubRes1); + commentStub.withArgs('ecf0b9fa_fe1a5f62').returns( + commentStubRes2); - const setupDiff = function(diff) { - const mock = document.createElement('mock-diff-response'); - diff.comments = { - left: diff.path === '/COMMIT_MSG' ? commitMsgComments : [], - right: [], - meta: { - changeNum: 1, - patchRange: { - basePatchNum: 'PARENT', - patchNum: 2, - }, - }, - }; - diff.prefs = { - context: 10, - tab_size: 8, - font_size: 12, - line_length: 100, - cursor_blink_rate: 0, - line_wrapping: false, - intraline_difference: true, - show_line_endings: true, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - auto_hide_diff_table_header: true, - theme: 'DEFAULT', - ignore_whitespace: 'IGNORE_NONE', - }; - diff.diff = mock.diffResponse; - diff.$.diff.flushDebouncer('renderDiffTable'); - }; + // Reload comments from the first comment thread, which should have a + // an updated message and a toggled resolve state. + element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee', + '/COMMIT_MSG'); + assert.equal(thread1.comments.length, 1); + assert.isFalse(thread1.comments[0].unresolved); + assert.equal(thread1.comments[0].message, 'edited text'); - const renderAndGetNewDiffs = function(index) { - const diffs = - Polymer.dom(element.root).querySelectorAll('gr-diff-host'); + // Reload comments from the second comment thread, which should have a new + // reply. + element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62', + '/COMMIT_MSG'); + assert.equal(thread2.comments.length, 3); - for (let i = index; i < diffs.length; i++) { - setupDiff(diffs[i]); - } + const commentStubCount = commentStub.callCount; + const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls'); - element._updateDiffCursor(); - element.$.diffCursor.handleDiffUpdate(); - return diffs; - }; + // Should not be getting threads when the file is not expanded. + element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62', + 'other/file'); + assert.isFalse(getThreadsSpy.called); + assert.equal(commentStubCount, commentStub.callCount); - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getPreferences() { return Promise.resolve({}); }, - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - }); - stub('gr-date-formatter', { - _loadTimeFormat() { return Promise.resolve(''); }, - }); - stub('gr-diff-host', { - reload() { return Promise.resolve(); }, - }); - - // Element must be wrapped in an element with direct access to the - // comment API. - commentApiWrapper = fixture('basic'); - element = commentApiWrapper.$.fileList; - loadCommentSpy = sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll'); - element.diffPrefs = {}; - sandbox.stub(element, '_reviewFile'); - - // Stub methods on the changeComments object after changeComments has - // been initialized. - commentApiWrapper.loadComments().then(() => { - sandbox.stub(element.changeComments, 'getPaths').returns({}); - sandbox.stub(element.changeComments, 'getCommentsBySideForPath') - .returns({meta: {}, left: [], right: []}); - done(); - }); - element._loading = false; - element.numFilesShown = 75; - element.selectedIndex = 0; - element._filesByPath = { - '/COMMIT_MSG': {lines_inserted: 9}, - 'file_added_in_rev2.txt': { - lines_inserted: 1, - lines_deleted: 1, - size_delta: 10, - size: 100, - }, - 'myfile.txt': { - lines_inserted: 1, - lines_deleted: 1, - size_delta: 10, - size: 100, - }, - }; - element._reviewed = ['/COMMIT_MSG', 'myfile.txt']; - element._loggedIn = true; - element.changeNum = '42'; - element.patchRange = { - basePatchNum: 'PARENT', - patchNum: '2', - }; - sandbox.stub(window, 'fetch', () => Promise.resolve()); - flushAsynchronousOperations(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('cursor with individually opened files', () => { - MockInteractions.keyUpOn(element, 73, null, 'i'); - flushAsynchronousOperations(); - let diffs = renderAndGetNewDiffs(0); - const diffStops = diffs[0].getCursorStops(); - - // 1 diff should be rendered. - assert.equal(diffs.length, 1); - - // No line number is selected. - assert.isFalse(diffStops[10].classList.contains('target-row')); - - // Tapping content on a line selects the line number. - MockInteractions.tap(Polymer.dom( - diffStops[10]).querySelectorAll('.contentText')[0]); - flushAsynchronousOperations(); - assert.isTrue(diffStops[10].classList.contains('target-row')); - - // Keyboard shortcuts are still moving the file cursor, not the diff - // cursor. - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - flushAsynchronousOperations(); - assert.isTrue(diffStops[10].classList.contains('target-row')); - assert.isFalse(diffStops[11].classList.contains('target-row')); - - // The file cusor is now at 1. - assert.equal(element.$.fileCursor.index, 1); - MockInteractions.keyUpOn(element, 73, null, 'i'); - flushAsynchronousOperations(); - - diffs = renderAndGetNewDiffs(1); - // Two diffs should be rendered. - assert.equal(diffs.length, 2); - const diffStopsFirst = diffs[0].getCursorStops(); - const diffStopsSecond = diffs[1].getCursorStops(); - - // The line on the first diff is stil selected - assert.isTrue(diffStopsFirst[10].classList.contains('target-row')); - assert.isFalse(diffStopsSecond[10].classList.contains('target-row')); - }); - - test('cursor with toggle all files', () => { - MockInteractions.keyUpOn(element, 73, 'shift', 'i'); - flushAsynchronousOperations(); - - const diffs = renderAndGetNewDiffs(0); - const diffStops = diffs[0].getCursorStops(); - - // 1 diff should be rendered. - assert.equal(diffs.length, 3); - - // No line number is selected. - assert.isFalse(diffStops[10].classList.contains('target-row')); - - // Tapping content on a line selects the line number. - MockInteractions.tap(Polymer.dom( - diffStops[10]).querySelectorAll('.contentText')[0]); - flushAsynchronousOperations(); - assert.isTrue(diffStops[10].classList.contains('target-row')); - - // Keyboard shortcuts are still moving the file cursor, not the diff - // cursor. - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - flushAsynchronousOperations(); - assert.isFalse(diffStops[10].classList.contains('target-row')); - assert.isTrue(diffStops[11].classList.contains('target-row')); - - // The file cusor is still at 0. - assert.equal(element.$.fileCursor.index, 0); - }); - - suite('n key presses', () => { - let nKeySpy; - let nextCommentStub; - let nextChunkStub; - let fileRows; - - setup(() => { - sandbox.stub(element, '_renderInOrder').returns(Promise.resolve()); - nKeySpy = sandbox.spy(element, '_handleNextChunk'); - nextCommentStub = sandbox.stub(element.$.diffCursor, - 'moveToNextCommentThread'); - nextChunkStub = sandbox.stub(element.$.diffCursor, - 'moveToNextChunk'); - fileRows = - Polymer.dom(element.root).querySelectorAll('.row:not(.header-row)'); - }); - - test('n key with some files expanded and no shift key', () => { - MockInteractions.keyUpOn(fileRows[0], 73, null, 'i'); - flushAsynchronousOperations(); - assert.equal(nextChunkStub.callCount, 1); - - // Handle N key should return before calling diff cursor functions. - MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); - assert.isTrue(nKeySpy.called); - assert.isFalse(nextCommentStub.called); - - // This is also called in diffCursor.moveToFirstChunk. - assert.equal(nextChunkStub.callCount, 2); - assert.equal(element.filesExpanded, 'some'); - }); - - test('n key with some files expanded and shift key', () => { - MockInteractions.keyUpOn(fileRows[0], 73, null, 'i'); - flushAsynchronousOperations(); - assert.equal(nextChunkStub.callCount, 1); - - MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); - assert.isTrue(nKeySpy.called); - assert.isTrue(nextCommentStub.called); - - // This is also called in diffCursor.moveToFirstChunk. - assert.equal(nextChunkStub.callCount, 1); - assert.equal(element.filesExpanded, 'some'); - }); - - test('n key without all files expanded and shift key', () => { - MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i'); - flushAsynchronousOperations(); - - MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); - assert.isTrue(nKeySpy.called); - assert.isFalse(nextCommentStub.called); - - // This is also called in diffCursor.moveToFirstChunk. - assert.equal(nextChunkStub.callCount, 2); - assert.isTrue(element._showInlineDiffs); - }); - - test('n key without all files expanded and no shift key', () => { - MockInteractions.keyUpOn(fileRows[0], 73, 'shift', 'i'); - flushAsynchronousOperations(); - - MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); - assert.isTrue(nKeySpy.called); - assert.isTrue(nextCommentStub.called); - - // This is also called in diffCursor.moveToFirstChunk. - assert.equal(nextChunkStub.callCount, 1); - assert.isTrue(element._showInlineDiffs); - }); - }); - - test('_openSelectedFile behavior', () => { - const _filesByPath = element._filesByPath; - element.set('_filesByPath', {}); - const navStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - // Noop when there are no files. - element._openSelectedFile(); - assert.isFalse(navStub.called); - - element.set('_filesByPath', _filesByPath); - flushAsynchronousOperations(); - // Navigates when a file is selected. - element._openSelectedFile(); - assert.isTrue(navStub.called); - }); - - test('_displayLine', () => { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut', () => false); - sandbox.stub(element, 'modifierPressed', () => false); - element._showInlineDiffs = true; - const mockEvent = {preventDefault() {}}; - - element._displayLine = false; - element._handleCursorNext(mockEvent); - assert.isTrue(element._displayLine); - - element._displayLine = false; - element._handleCursorPrev(mockEvent); - assert.isTrue(element._displayLine); - - element._displayLine = true; - element._handleEscKey(mockEvent); - assert.isFalse(element._displayLine); - }); - - suite('editMode behavior', () => { - test('reviewed checkbox', () => { - element._reviewFile.restore(); - const saveReviewStub = sandbox.stub(element, '_saveReviewedState'); - - element.editMode = false; - MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); - assert.isTrue(saveReviewStub.calledOnce); - - element.editMode = true; - flushAsynchronousOperations(); - - MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); - assert.isTrue(saveReviewStub.calledOnce); - }); - - test('_getReviewedFiles does not call API', () => { - const apiSpy = sandbox.spy(element.$.restAPI, 'getReviewedFiles'); - element.editMode = true; - return element._getReviewedFiles().then(files => { - assert.equal(files.length, 0); - assert.isFalse(apiSpy.called); - }); - }); - }); - - test('editing actions', () => { - // Edit controls are guarded behind a dom-if initially and not rendered. - assert.isNotOk(Polymer.dom(element.root) - .querySelector('gr-edit-file-controls')); - - element.editMode = true; - flushAsynchronousOperations(); - - // Commit message should not have edit controls. - const editControls = - Array.from( - Polymer.dom(element.root) - .querySelectorAll('.row:not(.header-row)')) - .map(row => row.querySelector('gr-edit-file-controls')); - assert.isTrue(editControls[0].classList.contains('invisible')); - }); - - test('reloadCommentsForThreadWithRootId', () => { - // Expand the commit message diff - MockInteractions.keyUpOn(element, 73, 'shift', 'i'); - const diffs = renderAndGetNewDiffs(0); - flushAsynchronousOperations(); - - // Two comment threads should be generated by renderAndGetNewDiffs - const threadEls = diffs[0].getThreadEls(); - assert.equal(threadEls.length, 2); - const threadElsByRootId = new Map( - threadEls.map(threadEl => [threadEl.rootId, threadEl])); - - const thread1 = threadElsByRootId.get('503008e2_0ab203ee'); - assert.equal(thread1.comments.length, 1); - assert.equal(thread1.comments[0].message, 'a comment'); - assert.equal(thread1.comments[0].line, 10); - - const thread2 = threadElsByRootId.get('ecf0b9fa_fe1a5f62'); - assert.equal(thread2.comments.length, 2); - assert.isTrue(thread2.comments[0].unresolved); - assert.equal(thread2.comments[0].message, 'another comment'); - assert.equal(thread2.comments[0].line, 20); - - const commentStub = - sandbox.stub(element.changeComments, 'getCommentsForThread'); - const commentStubRes1 = [ - { - patch_set: 2, - id: '503008e2_0ab203ee', - line: 20, - updated: '2018-02-08 18:49:18.000000000', - message: 'edited text', - unresolved: false, - }, - ]; - const commentStubRes2 = [ - { - patch_set: 2, - id: 'ecf0b9fa_fe1a5f62', - line: 20, - updated: '2018-02-08 18:49:18.000000000', - message: 'another comment', - unresolved: true, - }, - { - patch_set: 2, - id: '503008e2_0ab203ee', - line: 10, - in_reply_to: 'ecf0b9fa_fe1a5f62', - updated: '2018-02-14 22:07:43.000000000', - message: 'response', - unresolved: true, - }, - { - patch_set: 2, - id: '503008e2_0ab203ef', - line: 20, - in_reply_to: '503008e2_0ab203ee', - updated: '2018-02-15 22:07:43.000000000', - message: 'a third comment in the thread', - unresolved: true, - }, - ]; - commentStub.withArgs('503008e2_0ab203ee').returns( - commentStubRes1); - commentStub.withArgs('ecf0b9fa_fe1a5f62').returns( - commentStubRes2); - - // Reload comments from the first comment thread, which should have a - // an updated message and a toggled resolve state. - element.reloadCommentsForThreadWithRootId('503008e2_0ab203ee', - '/COMMIT_MSG'); - assert.equal(thread1.comments.length, 1); - assert.isFalse(thread1.comments[0].unresolved); - assert.equal(thread1.comments[0].message, 'edited text'); - - // Reload comments from the second comment thread, which should have a new - // reply. - element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62', - '/COMMIT_MSG'); - assert.equal(thread2.comments.length, 3); - - const commentStubCount = commentStub.callCount; - const getThreadsSpy = sandbox.spy(diffs[0], 'getThreadEls'); - - // Should not be getting threads when the file is not expanded. - element.reloadCommentsForThreadWithRootId('ecf0b9fa_fe1a5f62', - 'other/file'); - assert.isFalse(getThreadsSpy.called); - assert.equal(commentStubCount, commentStub.callCount); - - // Should be query selecting diffs when the file is expanded. - // Should not be fetching change comments when the rootId is not found - // to match. - element.reloadCommentsForThreadWithRootId('acf0b9fa_fe1a5f62', - '/COMMIT_MSG'); - assert.isTrue(getThreadsSpy.called); - assert.equal(commentStubCount, commentStub.callCount); - }); + // Should be query selecting diffs when the file is expanded. + // Should not be fetching change comments when the rootId is not found + // to match. + element.reloadCommentsForThreadWithRootId('acf0b9fa_fe1a5f62', + '/COMMIT_MSG'); + assert.isTrue(getThreadsSpy.called); + assert.equal(commentStubCount, commentStub.callCount); }); - a11ySuite('basic'); }); + a11ySuite('basic'); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js index 01c9b6e..bdf8c1d 100644 --- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.js
@@ -14,98 +14,109 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-included-in-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrIncludedInDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-included-in-dialog'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the user presses the close button. + * + * @event close */ - class GrIncludedInDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-included-in-dialog'; } - /** - * Fired when the user presses the close button. - * - * @event close - */ - static get properties() { - return { + static get properties() { + return { + /** @type {?} */ + changeNum: { + type: Object, + observer: '_resetData', + }, /** @type {?} */ - changeNum: { - type: Object, - observer: '_resetData', - }, - /** @type {?} */ - _includedIn: Object, - _loaded: { - type: Boolean, - value: false, - }, - _filterText: { - type: String, - value: '', - }, - }; - } - - loadData() { - if (!this.changeNum) { return; } - this._filterText = ''; - return this.$.restAPI.getChangeIncludedIn(this.changeNum).then( - configs => { - if (!configs) { return; } - this._includedIn = configs; - this._loaded = true; - }); - } - - _resetData() { - this._includedIn = null; - this._loaded = false; - } - - _computeGroups(includedIn, filterText) { - if (!includedIn) { return []; } - - const filter = item => !filterText.length || - item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1; - - const groups = [ - {title: 'Branches', items: includedIn.branches.filter(filter)}, - {title: 'Tags', items: includedIn.tags.filter(filter)}, - ]; - if (includedIn.external) { - for (const externalKey of Object.keys(includedIn.external)) { - groups.push({ - title: externalKey, - items: includedIn.external[externalKey].filter(filter), - }); - } - } - return groups.filter(g => g.items.length); - } - - _handleCloseTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('close', null, {bubbles: false}); - } - - _computeLoadingClass(loaded) { - return loaded ? 'loading loaded' : 'loading'; - } - - _onFilterChanged() { - this.debounce('filter-change', () => { - this._filterText = this.$.filterInput.bindValue; - }, 100); - } + _includedIn: Object, + _loaded: { + type: Boolean, + value: false, + }, + _filterText: { + type: String, + value: '', + }, + }; } - customElements.define(GrIncludedInDialog.is, GrIncludedInDialog); -})(); + loadData() { + if (!this.changeNum) { return; } + this._filterText = ''; + return this.$.restAPI.getChangeIncludedIn(this.changeNum).then( + configs => { + if (!configs) { return; } + this._includedIn = configs; + this._loaded = true; + }); + } + + _resetData() { + this._includedIn = null; + this._loaded = false; + } + + _computeGroups(includedIn, filterText) { + if (!includedIn) { return []; } + + const filter = item => !filterText.length || + item.toLowerCase().indexOf(filterText.toLowerCase()) !== -1; + + const groups = [ + {title: 'Branches', items: includedIn.branches.filter(filter)}, + {title: 'Tags', items: includedIn.tags.filter(filter)}, + ]; + if (includedIn.external) { + for (const externalKey of Object.keys(includedIn.external)) { + groups.push({ + title: externalKey, + items: includedIn.external[externalKey].filter(filter), + }); + } + } + return groups.filter(g => g.items.length); + } + + _handleCloseTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('close', null, {bubbles: false}); + } + + _computeLoadingClass(loaded) { + return loaded ? 'loading loaded' : 'loading'; + } + + _onFilterChanged() { + this.debounce('filter-change', () => { + this._filterText = this.$.filterInput.bindValue; + }, 100); + } +} + +customElements.define(GrIncludedInDialog.is, GrIncludedInDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js index 075b41e..b7d455c 100644 --- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-included-in-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background-color: var(--dialog-background-color); @@ -76,25 +69,14 @@ <header> <h1 id="title">Included In:</h1> <span class="closeButtonContainer"> - <gr-button id="closeButton" - link - on-click="_handleCloseTap">Close</gr-button> + <gr-button id="closeButton" link="" on-click="_handleCloseTap">Close</gr-button> </span> - <iron-input - placeholder="Filter" - on-bind-value-changed="_onFilterChanged"> - <input - id="filterInput" - is="iron-input" - placeholder="Filter" - on-bind-value-changed="_onFilterChanged"> + <iron-input placeholder="Filter" on-bind-value-changed="_onFilterChanged"> + <input id="filterInput" is="iron-input" placeholder="Filter" on-bind-value-changed="_onFilterChanged"> </iron-input> </header> - <div class$="[[_computeLoadingClass(_loaded)]]">Loading...</div> - <template - is="dom-repeat" - items="[[_computeGroups(_includedIn, _filterText)]]" - as="group"> + <div class\$="[[_computeLoadingClass(_loaded)]]">Loading...</div> + <template is="dom-repeat" items="[[_computeGroups(_includedIn, _filterText)]]" as="group"> <div> <span>[[group.title]]:</span> <ul> @@ -105,6 +87,4 @@ </div> </template> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-included-in-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html index bec6c7b..deb00bd 100644 --- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-included-in-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-included-in-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-included-in-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-included-in-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,54 +40,56 @@ </template> </test-fixture> -<script> - suite('gr-included-in-dialog', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-included-in-dialog.js'; +suite('gr-included-in-dialog', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('_computeGroups', () => { - const includedIn = {branches: [], tags: []}; - let filterText = ''; - assert.deepEqual(element._computeGroups(includedIn, filterText), []); - - includedIn.branches.push('master', 'development', 'stable-2.0'); - includedIn.tags.push('v1.9', 'v2.0', 'v2.1'); - assert.deepEqual(element._computeGroups(includedIn, filterText), [ - {title: 'Branches', items: ['master', 'development', 'stable-2.0']}, - {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']}, - ]); - - includedIn.external = {}; - assert.deepEqual(element._computeGroups(includedIn, filterText), [ - {title: 'Branches', items: ['master', 'development', 'stable-2.0']}, - {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']}, - ]); - - includedIn.external.foo = ['abc', 'def', 'ghi']; - assert.deepEqual(element._computeGroups(includedIn, filterText), [ - {title: 'Branches', items: ['master', 'development', 'stable-2.0']}, - {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']}, - {title: 'foo', items: ['abc', 'def', 'ghi']}, - ]); - - filterText = 'v2'; - assert.deepEqual(element._computeGroups(includedIn, filterText), [ - {title: 'Tags', items: ['v2.0', 'v2.1']}, - ]); - - // Filtering is case-insensitive. - filterText = 'V2'; - assert.deepEqual(element._computeGroups(includedIn, filterText), [ - {title: 'Tags', items: ['v2.0', 'v2.1']}, - ]); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { sandbox.restore(); }); + + test('_computeGroups', () => { + const includedIn = {branches: [], tags: []}; + let filterText = ''; + assert.deepEqual(element._computeGroups(includedIn, filterText), []); + + includedIn.branches.push('master', 'development', 'stable-2.0'); + includedIn.tags.push('v1.9', 'v2.0', 'v2.1'); + assert.deepEqual(element._computeGroups(includedIn, filterText), [ + {title: 'Branches', items: ['master', 'development', 'stable-2.0']}, + {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']}, + ]); + + includedIn.external = {}; + assert.deepEqual(element._computeGroups(includedIn, filterText), [ + {title: 'Branches', items: ['master', 'development', 'stable-2.0']}, + {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']}, + ]); + + includedIn.external.foo = ['abc', 'def', 'ghi']; + assert.deepEqual(element._computeGroups(includedIn, filterText), [ + {title: 'Branches', items: ['master', 'development', 'stable-2.0']}, + {title: 'Tags', items: ['v1.9', 'v2.0', 'v2.1']}, + {title: 'foo', items: ['abc', 'def', 'ghi']}, + ]); + + filterText = 'v2'; + assert.deepEqual(element._computeGroups(includedIn, filterText), [ + {title: 'Tags', items: ['v2.0', 'v2.1']}, + ]); + + // Filtering is case-insensitive. + filterText = 'V2'; + assert.deepEqual(element._computeGroups(includedIn, filterText), [ + {title: 'Tags', items: ['v2.0', 'v2.1']}, + ]); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js index 0316428..8541840 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row.js
@@ -14,167 +14,176 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrLabelScoreRow extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-label-score-row'; } +import '@polymer/iron-selector/iron-selector.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../../styles/gr-voting-styles.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-label-score-row_html.js'; + +/** @extends Polymer.Element */ +class GrLabelScoreRow extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-label-score-row'; } + /** + * Fired when any label is changed. + * + * @event labels-changed + */ + + static get properties() { + return { /** - * Fired when any label is changed. - * - * @event labels-changed + * @type {{ name: string }} */ + label: Object, + labels: Object, + name: { + type: String, + reflectToAttribute: true, + }, + permittedLabels: Object, + labelValues: Object, + _selectedValueText: { + type: String, + value: 'No value selected', + }, + _items: { + type: Array, + computed: '_computePermittedLabelValues(permittedLabels, label.name)', + }, + }; + } - static get properties() { - return { - /** - * @type {{ name: string }} - */ - label: Object, - labels: Object, - name: { - type: String, - reflectToAttribute: true, - }, - permittedLabels: Object, - labelValues: Object, - _selectedValueText: { - type: String, - value: 'No value selected', - }, - _items: { - type: Array, - computed: '_computePermittedLabelValues(permittedLabels, label.name)', - }, - }; + get selectedItem() { + if (!this._ironSelector) { return undefined; } + return this._ironSelector.selectedItem; + } + + get selectedValue() { + if (!this._ironSelector) { return undefined; } + return this._ironSelector.selected; + } + + setSelectedValue(value) { + // The selector may not be present if it’s not at the latest patch set. + if (!this._ironSelector) { return; } + this._ironSelector.select(value); + } + + get _ironSelector() { + return this.$ && this.$.labelSelector; + } + + _computeBlankItems(permittedLabels, label, side) { + if (!permittedLabels || !permittedLabels[label] || + !permittedLabels[label].length || !this.labelValues || + !Object.keys(this.labelValues).length) { + return []; } - - get selectedItem() { - if (!this._ironSelector) { return undefined; } - return this._ironSelector.selectedItem; + const startPosition = this.labelValues[parseInt( + permittedLabels[label][0], 10)]; + if (side === 'start') { + return new Array(startPosition); } + const endPosition = this.labelValues[parseInt( + permittedLabels[label][permittedLabels[label].length - 1], 10)]; + return new Array(Object.keys(this.labelValues).length - endPosition - 1); + } - get selectedValue() { - if (!this._ironSelector) { return undefined; } - return this._ironSelector.selected; - } - - setSelectedValue(value) { - // The selector may not be present if it’s not at the latest patch set. - if (!this._ironSelector) { return; } - this._ironSelector.select(value); - } - - get _ironSelector() { - return this.$ && this.$.labelSelector; - } - - _computeBlankItems(permittedLabels, label, side) { - if (!permittedLabels || !permittedLabels[label] || - !permittedLabels[label].length || !this.labelValues || - !Object.keys(this.labelValues).length) { - return []; - } - const startPosition = this.labelValues[parseInt( - permittedLabels[label][0], 10)]; - if (side === 'start') { - return new Array(startPosition); - } - const endPosition = this.labelValues[parseInt( - permittedLabels[label][permittedLabels[label].length - 1], 10)]; - return new Array(Object.keys(this.labelValues).length - endPosition - 1); - } - - _getLabelValue(labels, permittedLabels, label) { - if (label.value) { - return label.value; - } else if (labels[label.name].hasOwnProperty('default_value') && - permittedLabels.hasOwnProperty(label.name)) { - // default_value is an int, convert it to string label, e.g. "+1". - return permittedLabels[label.name].find( - value => parseInt(value, 10) === labels[label.name].default_value); - } - } - - /** - * Maps the label value to exactly one of: min, max, positive, negative, - * neutral. Used for the 'vote' attribute, because we don't want to - * interfere with <iron-selector> using the 'class' attribute for setting - * 'iron-selected'. - */ - _computeVoteAttribute(value, index, totalItems) { - if (value < 0 && index === 0) { - return 'min'; - } else if (value < 0) { - return 'negative'; - } else if (value > 0 && index === totalItems - 1) { - return 'max'; - } else if (value > 0) { - return 'positive'; - } else { - return 'neutral'; - } - } - - _computeLabelValue(labels, permittedLabels, label) { - if ([labels, permittedLabels, label].some(arg => arg === undefined)) { - return null; - } - if (!labels[label.name]) { return null; } - const labelValue = this._getLabelValue(labels, permittedLabels, label); - const len = permittedLabels[label.name] != null ? - permittedLabels[label.name].length : 0; - for (let i = 0; i < len; i++) { - const val = permittedLabels[label.name][i]; - if (val === labelValue) { - return val; - } - } - return null; - } - - _setSelectedValueText(e) { - // Needed because when the selected item changes, it first changes to - // nothing and then to the new item. - if (!e.target.selectedItem) { return; } - this._selectedValueText = e.target.selectedItem.getAttribute('title'); - // Needed to update the style of the selected button. - this.updateStyles(); - const name = e.target.selectedItem.dataset.name; - const value = e.target.selectedItem.dataset.value; - this.dispatchEvent(new CustomEvent( - 'labels-changed', - {detail: {name, value}, bubbles: true, composed: true})); - } - - _computeAnyPermittedLabelValues(permittedLabels, label) { - return permittedLabels && permittedLabels.hasOwnProperty(label) && - permittedLabels[label].length; - } - - _computeHiddenClass(permittedLabels, label) { - return !this._computeAnyPermittedLabelValues(permittedLabels, label) ? - 'hidden' : ''; - } - - _computePermittedLabelValues(permittedLabels, label) { - // Polymer 2: check for undefined - if ([permittedLabels, label].some(arg => arg === undefined)) { - return undefined; - } - - return permittedLabels[label]; - } - - _computeLabelValueTitle(labels, label, value) { - return labels[label] && - labels[label].values && - labels[label].values[value]; + _getLabelValue(labels, permittedLabels, label) { + if (label.value) { + return label.value; + } else if (labels[label.name].hasOwnProperty('default_value') && + permittedLabels.hasOwnProperty(label.name)) { + // default_value is an int, convert it to string label, e.g. "+1". + return permittedLabels[label.name].find( + value => parseInt(value, 10) === labels[label.name].default_value); } } - customElements.define(GrLabelScoreRow.is, GrLabelScoreRow); -})(); + /** + * Maps the label value to exactly one of: min, max, positive, negative, + * neutral. Used for the 'vote' attribute, because we don't want to + * interfere with <iron-selector> using the 'class' attribute for setting + * 'iron-selected'. + */ + _computeVoteAttribute(value, index, totalItems) { + if (value < 0 && index === 0) { + return 'min'; + } else if (value < 0) { + return 'negative'; + } else if (value > 0 && index === totalItems - 1) { + return 'max'; + } else if (value > 0) { + return 'positive'; + } else { + return 'neutral'; + } + } + + _computeLabelValue(labels, permittedLabels, label) { + if ([labels, permittedLabels, label].some(arg => arg === undefined)) { + return null; + } + if (!labels[label.name]) { return null; } + const labelValue = this._getLabelValue(labels, permittedLabels, label); + const len = permittedLabels[label.name] != null ? + permittedLabels[label.name].length : 0; + for (let i = 0; i < len; i++) { + const val = permittedLabels[label.name][i]; + if (val === labelValue) { + return val; + } + } + return null; + } + + _setSelectedValueText(e) { + // Needed because when the selected item changes, it first changes to + // nothing and then to the new item. + if (!e.target.selectedItem) { return; } + this._selectedValueText = e.target.selectedItem.getAttribute('title'); + // Needed to update the style of the selected button. + this.updateStyles(); + const name = e.target.selectedItem.dataset.name; + const value = e.target.selectedItem.dataset.value; + this.dispatchEvent(new CustomEvent( + 'labels-changed', + {detail: {name, value}, bubbles: true, composed: true})); + } + + _computeAnyPermittedLabelValues(permittedLabels, label) { + return permittedLabels && permittedLabels.hasOwnProperty(label) && + permittedLabels[label].length; + } + + _computeHiddenClass(permittedLabels, label) { + return !this._computeAnyPermittedLabelValues(permittedLabels, label) ? + 'hidden' : ''; + } + + _computePermittedLabelValues(permittedLabels, label) { + // Polymer 2: check for undefined + if ([permittedLabels, label].some(arg => arg === undefined)) { + return undefined; + } + + return permittedLabels[label]; + } + + _computeLabelValueTitle(labels, label, value) { + return labels[label] && + labels[label].values && + labels[label].values[value]; + } +} + +customElements.define(GrLabelScoreRow.is, GrLabelScoreRow);
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js index 50c01aa..1b5b425 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-selector/iron-selector.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../../styles/gr-voting-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-label-score-row"> - <template> +export const htmlTemplate = html` <style include="gr-voting-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -99,42 +93,23 @@ </style> <span class="labelNameCell">[[label.name]]</span> <div class="buttonsCell"> - <template is="dom-repeat" - items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]" - as="value"> - <span class="placeholder" data-label$="[[label.name]]"></span> + <template is="dom-repeat" items="[[_computeBlankItems(permittedLabels, label.name, 'start')]]" as="value"> + <span class="placeholder" data-label\$="[[label.name]]"></span> </template> - <iron-selector - id="labelSelector" - attr-for-selected="data-value" - selected="[[_computeLabelValue(labels, permittedLabels, label)]]" - hidden$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]" - on-selected-item-changed="_setSelectedValueText"> - <template is="dom-repeat" - items="[[_items]]" - as="value"> - <gr-button - vote$="[[_computeVoteAttribute(value, index, _items.length)]]" - has-tooltip - data-name$="[[label.name]]" - data-value$="[[value]]" - title$="[[_computeLabelValueTitle(labels, label.name, value)]]"> + <iron-selector id="labelSelector" attr-for-selected="data-value" selected="[[_computeLabelValue(labels, permittedLabels, label)]]" hidden\$="[[!_computeAnyPermittedLabelValues(permittedLabels, label.name)]]" on-selected-item-changed="_setSelectedValueText"> + <template is="dom-repeat" items="[[_items]]" as="value"> + <gr-button vote\$="[[_computeVoteAttribute(value, index, _items.length)]]" has-tooltip="" data-name\$="[[label.name]]" data-value\$="[[value]]" title\$="[[_computeLabelValueTitle(labels, label.name, value)]]"> [[value]]</gr-button> </template> </iron-selector> - <template is="dom-repeat" - items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]" - as="value"> - <span class="placeholder" data-label$="[[label.name]]"></span> + <template is="dom-repeat" items="[[_computeBlankItems(permittedLabels, label.name, 'end')]]" as="value"> + <span class="placeholder" data-label\$="[[label.name]]"></span> </template> - <span class="labelMessage" - hidden$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"> + <span class="labelMessage" hidden\$="[[_computeAnyPermittedLabelValues(permittedLabels, label.name)]]"> You don't have permission to edit this label. </span> </div> - <div class$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]"> + <div class\$="selectedValueCell [[_computeHiddenClass(permittedLabels, label.name)]]"> <span id="selectedValueLabel">[[_selectedValueText]]</span> </div> - </template> - <script src="gr-label-score-row.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html index b10b932..18ba2a5 100644 --- a/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html +++ b/polygerrit-ui/app/elements/change/gr-label-score-row/gr-label-score-row_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-label-score-row</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-label-score-row.html"> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-label-score-row.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-label-score-row.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,340 +41,343 @@ </template> </test-fixture> -<script> - suite('gr-label-row-score tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-label-score-row.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-label-row-score tests', () => { + let element; + let sandbox; - setup(done => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.labels = { - 'Code-Review': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, - value: 1, - all: [{ - _account_id: 123, - value: 1, - }], + setup(done => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.labels = { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', }, - 'Verified': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, value: 1, - all: [{ - _account_id: 123, - value: 1, - }], + }], + }, + 'Verified': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', }, - }; + default_value: 0, + value: 1, + all: [{ + _account_id: 123, + value: 1, + }], + }, + }; + + element.permittedLabels = { + 'Code-Review': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; + + element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1}; + + element.label = { + name: 'Verified', + value: '+1', + }; + + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('label picker', () => { + const labelsChangedHandler = sandbox.stub(); + element.addEventListener('labels-changed', labelsChangedHandler); + assert.ok(element.$.labelSelector); + MockInteractions.tap(element.shadowRoot + .querySelector( + 'gr-button[data-value="-1"]')); + flushAsynchronousOperations(); + assert.strictEqual(element.selectedValue, '-1'); + assert.strictEqual(element.selectedItem + .textContent.trim(), '-1'); + assert.strictEqual( + element.$.selectedValueLabel.textContent.trim(), 'bad'); + const detail = labelsChangedHandler.args[0][0].detail; + assert.equal(detail.name, 'Verified'); + assert.equal(detail.value, '-1'); + }); + + test('_computeVoteAttribute', () => { + let value = 1; + let index = 0; + const totalItems = 5; + // positive and first position + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'positive'); + // negative and first position + value = -1; + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'min'); + // negative but not first position + index = 1; + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'negative'); + // neutral + value = 0; + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'neutral'); + // positive but not last position + value = 1; + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'positive'); + // positive and last position + index = 4; + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'max'); + // negative and last position + value = -1; + assert.equal(element._computeVoteAttribute(value, index, + totalItems), 'negative'); + }); + + test('correct item is selected', () => { + // 1 should be the value of the selected item + assert.strictEqual(element.$.labelSelector.selected, '+1'); + assert.strictEqual( + element.$.labelSelector.selectedItem + .textContent.trim(), '+1'); + assert.strictEqual( + element.$.selectedValueLabel.textContent.trim(), 'good'); + }); + + test('do not display tooltips on touch devices', () => { + const verifiedBtn = element.shadowRoot + .querySelector( + 'iron-selector > gr-button[data-value="-1"]'); + + // On touch devices, tooltips should not be shown. + verifiedBtn._isTouchDevice = true; + verifiedBtn._handleShowTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + verifiedBtn._handleHideTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + + // On other devices, tooltips should be shown. + verifiedBtn._isTouchDevice = false; + verifiedBtn._handleShowTooltip(); + assert.isOk(verifiedBtn._tooltip); + verifiedBtn._handleHideTooltip(); + assert.isNotOk(verifiedBtn._tooltip); + }); + + test('_computeLabelValue', () => { + assert.strictEqual(element._computeLabelValue(element.labels, + element.permittedLabels, + element.label), '+1'); + }); + + test('_computeBlankItems', () => { + element.labelValues = { + '-2': 0, + '-1': 1, + '0': 2, + '1': 3, + '2': 4, + }; + + assert.strictEqual(element._computeBlankItems(element.permittedLabels, + 'Code-Review').length, 0); + + assert.strictEqual(element._computeBlankItems(element.permittedLabels, + 'Verified').length, 1); + }); + + test('labelValues returns no keys', () => { + element.labelValues = {}; + + assert.deepEqual(element._computeBlankItems(element.permittedLabels, + 'Code-Review'), []); + }); + + test('changes in label score are reflected in the DOM', () => { + element.labels = { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + 'Verified': { + values: { + ' 0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + }; + const selector = element.$.labelSelector; + element.set('label', {name: 'Verified', value: ' 0'}); + flushAsynchronousOperations(); + assert.strictEqual(selector.selected, ' 0'); + assert.strictEqual( + element.$.selectedValueLabel.textContent.trim(), 'No score'); + }); + + test('without permitted labels', () => { + element.permittedLabels = { + Verified: [ + '-1', + ' 0', + '+1', + ], + }; + flushAsynchronousOperations(); + assert.isOk(element.$.labelSelector); + assert.isFalse(element.$.labelSelector.hidden); + + element.permittedLabels = {}; + flushAsynchronousOperations(); + assert.isOk(element.$.labelSelector); + assert.isTrue(element.$.labelSelector.hidden); + + element.permittedLabels = {Verified: []}; + flushAsynchronousOperations(); + assert.isOk(element.$.labelSelector); + assert.isTrue(element.$.labelSelector.hidden); + }); + + test('asymetrical labels', done => { + element.permittedLabels = { + 'Code-Review': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + 'Verified': [ + ' 0', + '+1', + ], + }; + flush(() => { + assert.strictEqual(element.$.labelSelector + .items.length, 2); + assert.strictEqual( + dom(element.root).querySelectorAll('.placeholder').length, + 3); element.permittedLabels = { 'Code-Review': [ + ' 0', + '+1', + ], + 'Verified': [ '-2', '-1', ' 0', '+1', '+2', ], - 'Verified': [ - '-1', - ' 0', - '+1', - ], - }; - - element.labelValues = {'0': 2, '1': 3, '2': 4, '-2': 0, '-1': 1}; - - element.label = { - name: 'Verified', - value: '+1', - }; - - flush(done); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('label picker', () => { - const labelsChangedHandler = sandbox.stub(); - element.addEventListener('labels-changed', labelsChangedHandler); - assert.ok(element.$.labelSelector); - MockInteractions.tap(element.shadowRoot - .querySelector( - 'gr-button[data-value="-1"]')); - flushAsynchronousOperations(); - assert.strictEqual(element.selectedValue, '-1'); - assert.strictEqual(element.selectedItem - .textContent.trim(), '-1'); - assert.strictEqual( - element.$.selectedValueLabel.textContent.trim(), 'bad'); - const detail = labelsChangedHandler.args[0][0].detail; - assert.equal(detail.name, 'Verified'); - assert.equal(detail.value, '-1'); - }); - - test('_computeVoteAttribute', () => { - let value = 1; - let index = 0; - const totalItems = 5; - // positive and first position - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'positive'); - // negative and first position - value = -1; - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'min'); - // negative but not first position - index = 1; - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'negative'); - // neutral - value = 0; - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'neutral'); - // positive but not last position - value = 1; - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'positive'); - // positive and last position - index = 4; - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'max'); - // negative and last position - value = -1; - assert.equal(element._computeVoteAttribute(value, index, - totalItems), 'negative'); - }); - - test('correct item is selected', () => { - // 1 should be the value of the selected item - assert.strictEqual(element.$.labelSelector.selected, '+1'); - assert.strictEqual( - element.$.labelSelector.selectedItem - .textContent.trim(), '+1'); - assert.strictEqual( - element.$.selectedValueLabel.textContent.trim(), 'good'); - }); - - test('do not display tooltips on touch devices', () => { - const verifiedBtn = element.shadowRoot - .querySelector( - 'iron-selector > gr-button[data-value="-1"]'); - - // On touch devices, tooltips should not be shown. - verifiedBtn._isTouchDevice = true; - verifiedBtn._handleShowTooltip(); - assert.isNotOk(verifiedBtn._tooltip); - verifiedBtn._handleHideTooltip(); - assert.isNotOk(verifiedBtn._tooltip); - - // On other devices, tooltips should be shown. - verifiedBtn._isTouchDevice = false; - verifiedBtn._handleShowTooltip(); - assert.isOk(verifiedBtn._tooltip); - verifiedBtn._handleHideTooltip(); - assert.isNotOk(verifiedBtn._tooltip); - }); - - test('_computeLabelValue', () => { - assert.strictEqual(element._computeLabelValue(element.labels, - element.permittedLabels, - element.label), '+1'); - }); - - test('_computeBlankItems', () => { - element.labelValues = { - '-2': 0, - '-1': 1, - '0': 2, - '1': 3, - '2': 4, - }; - - assert.strictEqual(element._computeBlankItems(element.permittedLabels, - 'Code-Review').length, 0); - - assert.strictEqual(element._computeBlankItems(element.permittedLabels, - 'Verified').length, 1); - }); - - test('labelValues returns no keys', () => { - element.labelValues = {}; - - assert.deepEqual(element._computeBlankItems(element.permittedLabels, - 'Code-Review'), []); - }); - - test('changes in label score are reflected in the DOM', () => { - element.labels = { - 'Code-Review': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, - }, - 'Verified': { - values: { - ' 0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, - }, - }; - const selector = element.$.labelSelector; - element.set('label', {name: 'Verified', value: ' 0'}); - flushAsynchronousOperations(); - assert.strictEqual(selector.selected, ' 0'); - assert.strictEqual( - element.$.selectedValueLabel.textContent.trim(), 'No score'); - }); - - test('without permitted labels', () => { - element.permittedLabels = { - Verified: [ - '-1', - ' 0', - '+1', - ], - }; - flushAsynchronousOperations(); - assert.isOk(element.$.labelSelector); - assert.isFalse(element.$.labelSelector.hidden); - - element.permittedLabels = {}; - flushAsynchronousOperations(); - assert.isOk(element.$.labelSelector); - assert.isTrue(element.$.labelSelector.hidden); - - element.permittedLabels = {Verified: []}; - flushAsynchronousOperations(); - assert.isOk(element.$.labelSelector); - assert.isTrue(element.$.labelSelector.hidden); - }); - - test('asymetrical labels', done => { - element.permittedLabels = { - 'Code-Review': [ - '-2', - '-1', - ' 0', - '+1', - '+2', - ], - 'Verified': [ - ' 0', - '+1', - ], }; flush(() => { assert.strictEqual(element.$.labelSelector - .items.length, 2); + .items.length, 5); assert.strictEqual( - Polymer.dom(element.root).querySelectorAll('.placeholder').length, - 3); - - element.permittedLabels = { - 'Code-Review': [ - ' 0', - '+1', - ], - 'Verified': [ - '-2', - '-1', - ' 0', - '+1', - '+2', - ], - }; - flush(() => { - assert.strictEqual(element.$.labelSelector - .items.length, 5); - assert.strictEqual( - Polymer.dom(element.root).querySelectorAll('.placeholder').length, - 0); - done(); - }); + dom(element.root).querySelectorAll('.placeholder').length, + 0); + done(); }); }); - - test('default_value', () => { - element.permittedLabels = { - Verified: [ - '-1', - ' 0', - '+1', - ], - }; - element.labels = { - Verified: { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: -1, - }, - }; - element.label = { - name: 'Verified', - value: null, - }; - flushAsynchronousOperations(); - assert.strictEqual(element.selectedValue, '-1'); - }); - - test('default_value is null if not permitted', () => { - element.permittedLabels = { - Verified: [ - '-1', - ' 0', - '+1', - ], - }; - element.labels = { - 'Code-Review': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: -1, - }, - }; - element.label = { - name: 'Code-Review', - value: null, - }; - flushAsynchronousOperations(); - assert.isNull(element.selectedValue); - }); }); + + test('default_value', () => { + element.permittedLabels = { + Verified: [ + '-1', + ' 0', + '+1', + ], + }; + element.labels = { + Verified: { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: -1, + }, + }; + element.label = { + name: 'Verified', + value: null, + }; + flushAsynchronousOperations(); + assert.strictEqual(element.selectedValue, '-1'); + }); + + test('default_value is null if not permitted', () => { + element.permittedLabels = { + Verified: [ + '-1', + ' 0', + '+1', + ], + }; + element.labels = { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: -1, + }, + }; + element.label = { + name: 'Code-Review', + value: null, + }; + flushAsynchronousOperations(); + assert.isNull(element.selectedValue); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js index dbfdb6a..2d6825b 100644 --- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores.js
@@ -14,135 +14,143 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrLabelScores extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-label-scores'; } +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-label-score-row/gr-label-score-row.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-label-scores_html.js'; - static get properties() { - return { - _labels: { - type: Array, - computed: '_computeLabels(change.labels.*, account)', - }, - permittedLabels: { - type: Object, - observer: '_computeColumns', - }, - /** @type {?} */ - change: Object, - /** @type {?} */ - account: Object, +/** @extends Polymer.Element */ +class GrLabelScores extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _labelValues: Object, - }; - } + static get is() { return 'gr-label-scores'; } - getLabelValues() { - const labels = {}; - for (const label in this.permittedLabels) { - if (!this.permittedLabels.hasOwnProperty(label)) { continue; } + static get properties() { + return { + _labels: { + type: Array, + computed: '_computeLabels(change.labels.*, account)', + }, + permittedLabels: { + type: Object, + observer: '_computeColumns', + }, + /** @type {?} */ + change: Object, + /** @type {?} */ + account: Object, - const selectorEl = this.shadowRoot - .querySelector(`gr-label-score-row[name="${label}"]`); - if (!selectorEl) { continue; } - - // The user may have not voted on this label. - if (!selectorEl.selectedItem) { continue; } - - const selectedVal = parseInt(selectorEl.selectedValue, 10); - - // Only send the selection if the user changed it. - let prevVal = this._getVoteForAccount(this.change.labels, label, - this.account); - if (prevVal !== null) { - prevVal = parseInt(prevVal, 10); - } - if (selectedVal !== prevVal) { - labels[label] = selectedVal; - } - } - return labels; - } - - _getStringLabelValue(labels, labelName, numberValue) { - for (const k in labels[labelName].values) { - if (parseInt(k, 10) === numberValue) { - return k; - } - } - return numberValue; - } - - _getVoteForAccount(labels, labelName, account) { - const votes = labels[labelName]; - if (votes.all && votes.all.length > 0) { - for (let i = 0; i < votes.all.length; i++) { - if (votes.all[i]._account_id == account._account_id) { - return this._getStringLabelValue( - labels, labelName, votes.all[i].value); - } - } - } - return null; - } - - _computeLabels(labelRecord, account) { - // Polymer 2: check for undefined - if ([labelRecord, account].some(arg => arg === undefined)) { - return undefined; - } - - const labelsObj = labelRecord.base; - if (!labelsObj) { return []; } - return Object.keys(labelsObj).sort() - .map(key => { - return { - name: key, - value: this._getVoteForAccount(labelsObj, key, this.account), - }; - }); - } - - _computeColumns(permittedLabels) { - const labels = Object.keys(permittedLabels); - const values = {}; - for (const label of labels) { - for (const value of permittedLabels[label]) { - values[parseInt(value, 10)] = true; - } - } - - const orderedValues = Object.keys(values).sort((a, b) => a - b); - - for (let i = 0; i < orderedValues.length; i++) { - values[orderedValues[i]] = i; - } - this._labelValues = values; - } - - _changeIsMerged(changeStatus) { - return changeStatus === 'MERGED'; - } - - /** - * @param {string|undefined} label - * @param {Object|undefined} permittedLabels - * @return {string} - */ - _computeLabelAccessClass(label, permittedLabels) { - if (label == null || permittedLabels == null) { - return ''; - } - - return permittedLabels.hasOwnProperty(label) && - permittedLabels[label].length ? 'access' : 'no-access'; - } + _labelValues: Object, + }; } - customElements.define(GrLabelScores.is, GrLabelScores); -})(); + getLabelValues() { + const labels = {}; + for (const label in this.permittedLabels) { + if (!this.permittedLabels.hasOwnProperty(label)) { continue; } + + const selectorEl = this.shadowRoot + .querySelector(`gr-label-score-row[name="${label}"]`); + if (!selectorEl) { continue; } + + // The user may have not voted on this label. + if (!selectorEl.selectedItem) { continue; } + + const selectedVal = parseInt(selectorEl.selectedValue, 10); + + // Only send the selection if the user changed it. + let prevVal = this._getVoteForAccount(this.change.labels, label, + this.account); + if (prevVal !== null) { + prevVal = parseInt(prevVal, 10); + } + if (selectedVal !== prevVal) { + labels[label] = selectedVal; + } + } + return labels; + } + + _getStringLabelValue(labels, labelName, numberValue) { + for (const k in labels[labelName].values) { + if (parseInt(k, 10) === numberValue) { + return k; + } + } + return numberValue; + } + + _getVoteForAccount(labels, labelName, account) { + const votes = labels[labelName]; + if (votes.all && votes.all.length > 0) { + for (let i = 0; i < votes.all.length; i++) { + if (votes.all[i]._account_id == account._account_id) { + return this._getStringLabelValue( + labels, labelName, votes.all[i].value); + } + } + } + return null; + } + + _computeLabels(labelRecord, account) { + // Polymer 2: check for undefined + if ([labelRecord, account].some(arg => arg === undefined)) { + return undefined; + } + + const labelsObj = labelRecord.base; + if (!labelsObj) { return []; } + return Object.keys(labelsObj).sort() + .map(key => { + return { + name: key, + value: this._getVoteForAccount(labelsObj, key, this.account), + }; + }); + } + + _computeColumns(permittedLabels) { + const labels = Object.keys(permittedLabels); + const values = {}; + for (const label of labels) { + for (const value of permittedLabels[label]) { + values[parseInt(value, 10)] = true; + } + } + + const orderedValues = Object.keys(values).sort((a, b) => a - b); + + for (let i = 0; i < orderedValues.length; i++) { + values[orderedValues[i]] = i; + } + this._labelValues = values; + } + + _changeIsMerged(changeStatus) { + return changeStatus === 'MERGED'; + } + + /** + * @param {string|undefined} label + * @param {Object|undefined} permittedLabels + * @return {string} + */ + _computeLabelAccessClass(label, permittedLabels) { + if (label == null || permittedLabels == null) { + return ''; + } + + return permittedLabels.hasOwnProperty(label) && + permittedLabels[label].length ? 'access' : 'no-access'; + } +} + +customElements.define(GrLabelScores.is, GrLabelScores);
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js index 8a8a9d5..b9c53c3 100644 --- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-label-score-row/gr-label-score-row.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-label-scores"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .scoresTable { display: table; @@ -44,19 +39,10 @@ </style> <div class="scoresTable"> <template is="dom-repeat" items="[[_labels]]" as="label"> - <gr-label-score-row - class$="[[_computeLabelAccessClass(label.name, permittedLabels)]]" - label="[[label]]" - name="[[label.name]]" - labels="[[change.labels]]" - permitted-labels="[[permittedLabels]]" - label-values="[[_labelValues]]"></gr-label-score-row> + <gr-label-score-row class\$="[[_computeLabelAccessClass(label.name, permittedLabels)]]" label="[[label]]" name="[[label.name]]" labels="[[change.labels]]" permitted-labels="[[permittedLabels]]" label-values="[[_labelValues]]"></gr-label-score-row> </template> </div> - <div class="mergedMessage" - hidden$="[[!_changeIsMerged(change.status)]]"> + <div class="mergedMessage" hidden\$="[[!_changeIsMerged(change.status)]]"> Because this change has been merged, votes may not be decreased. </div> - </template> - <script src="gr-label-scores.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html index 9e93110..b6075ee 100644 --- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html +++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-label-scores</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-label-scores.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-label-scores.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-label-scores.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,166 +40,167 @@ </template> </test-fixture> -<script> - suite('gr-label-scores tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-label-scores.js'; +suite('gr-label-scores tests', () => { + let element; + let sandbox; - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - }); - element = fixture('basic'); - element.change = { - _number: '123', - labels: { - 'Code-Review': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, - value: 1, - all: [{ - _account_id: 123, - value: 1, - }], + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + element.change = { + _number: '123', + labels: { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', }, - 'Verified': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, value: 1, - all: [{ - _account_id: 123, - value: 1, - }], - }, + }], }, - }; + 'Verified': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + value: 1, + all: [{ + _account_id: 123, + value: 1, + }], + }, + }, + }; - element.account = { - _account_id: 123, - }; + element.account = { + _account_id: 123, + }; - element.permittedLabels = { - 'Code-Review': [ - '-2', - '-1', - ' 0', - '+1', - '+2', - ], - 'Verified': [ - '-1', - ' 0', - '+1', - ], - }; - flush(done); - }); + element.permittedLabels = { + 'Code-Review': [ + '-2', + '-1', + ' 0', + '+1', + '+2', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; + flush(done); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('get and set label scores', () => { - for (const label in element.permittedLabels) { - if (element.permittedLabels.hasOwnProperty(label)) { - const row = element.shadowRoot - .querySelector('gr-label-score-row[name="' + label + '"]'); - row.setSelectedValue(-1); - } + test('get and set label scores', () => { + for (const label in element.permittedLabels) { + if (element.permittedLabels.hasOwnProperty(label)) { + const row = element.shadowRoot + .querySelector('gr-label-score-row[name="' + label + '"]'); + row.setSelectedValue(-1); } - assert.deepEqual(element.getLabelValues(), { - 'Code-Review': -1, - 'Verified': -1, - }); - }); - - test('_getVoteForAccount', () => { - const labelName = 'Code-Review'; - assert.strictEqual(element._getVoteForAccount( - element.change.labels, labelName, element.account), - '+1'); - }); - - test('_computeColumns', () => { - element._computeColumns(element.permittedLabels); - assert.deepEqual(element._labelValues, { - '-2': 0, - '-1': 1, - '0': 2, - '1': 3, - '2': 4, - }); - }); - - test('_computeLabelAccessClass undefined case', () => { - assert.strictEqual( - element._computeLabelAccessClass(undefined, undefined), ''); - assert.strictEqual( - element._computeLabelAccessClass('', undefined), ''); - assert.strictEqual( - element._computeLabelAccessClass(undefined, {}), ''); - }); - - test('_computeLabelAccessClass has access', () => { - assert.strictEqual( - element._computeLabelAccessClass('foo', {foo: ['']}), 'access'); - }); - - test('_computeLabelAccessClass no access', () => { - assert.strictEqual( - element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access'); - }); - - test('changes in label score are reflected in _labels', () => { - element.change = { - _number: '123', - labels: { - 'Code-Review': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, - }, - 'Verified': { - values: { - '0': 'No score', - '+1': 'good', - '+2': 'excellent', - '-1': 'bad', - '-2': 'terrible', - }, - default_value: 0, - }, - }, - }; - assert.deepEqual(element._labels [ - {name: 'Code-Review', value: null}, - {name: 'Verified', value: null} - ]); - element.set(['change', 'labels', 'Verified', 'all'], - [{_account_id: 123, value: 1}]); - assert.deepEqual(element._labels, [ - {name: 'Code-Review', value: null}, - {name: 'Verified', value: '+1'}, - ]); + } + assert.deepEqual(element.getLabelValues(), { + 'Code-Review': -1, + 'Verified': -1, }); }); + + test('_getVoteForAccount', () => { + const labelName = 'Code-Review'; + assert.strictEqual(element._getVoteForAccount( + element.change.labels, labelName, element.account), + '+1'); + }); + + test('_computeColumns', () => { + element._computeColumns(element.permittedLabels); + assert.deepEqual(element._labelValues, { + '-2': 0, + '-1': 1, + '0': 2, + '1': 3, + '2': 4, + }); + }); + + test('_computeLabelAccessClass undefined case', () => { + assert.strictEqual( + element._computeLabelAccessClass(undefined, undefined), ''); + assert.strictEqual( + element._computeLabelAccessClass('', undefined), ''); + assert.strictEqual( + element._computeLabelAccessClass(undefined, {}), ''); + }); + + test('_computeLabelAccessClass has access', () => { + assert.strictEqual( + element._computeLabelAccessClass('foo', {foo: ['']}), 'access'); + }); + + test('_computeLabelAccessClass no access', () => { + assert.strictEqual( + element._computeLabelAccessClass('zap', {foo: ['']}), 'no-access'); + }); + + test('changes in label score are reflected in _labels', () => { + element.change = { + _number: '123', + labels: { + 'Code-Review': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + 'Verified': { + values: { + '0': 'No score', + '+1': 'good', + '+2': 'excellent', + '-1': 'bad', + '-2': 'terrible', + }, + default_value: 0, + }, + }, + }; + assert.deepEqual(element._labels [ + ({name: 'Code-Review', value: null}, {name: 'Verified', value: null}) + ]); + element.set(['change', 'labels', 'Verified', 'all'], + [{_account_id: 123, value: 1}]); + assert.deepEqual(element._labels, [ + {name: 'Code-Review', value: null}, + {name: 'Verified', value: '+1'}, + ]); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js index 13a4213..e5cbaf1 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,379 +14,396 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/; - const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/; +import '@polymer/iron-icon/iron-icon.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-account-label/gr-account-label.js'; +import '../../shared/gr-account-chip/gr-account-chip.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-formatted-text/gr-formatted-text.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import '../../../styles/gr-voting-styles.js'; +import '../gr-comment-list/gr-comment-list.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-message_html.js'; + +const PATCH_SET_PREFIX_PATTERN = /^Patch Set \d+:\s*(.*)/; +const LABEL_TITLE_SCORE_PATTERN = /^(-?)([A-Za-z0-9-]+?)([+-]\d+)?$/; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrMessage extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-message'; } + /** + * Fired when this message's reply link is tapped. + * + * @event reply + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the message's timestamp is tapped. + * + * @event message-anchor-tap */ - class GrMessage extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-message'; } - /** - * Fired when this message's reply link is tapped. - * - * @event reply - */ - /** - * Fired when the message's timestamp is tapped. - * - * @event message-anchor-tap - */ + /** + * Fired when a change message is deleted. + * + * @event change-message-deleted + */ - /** - * Fired when a change message is deleted. - * - * @event change-message-deleted - */ + static get properties() { + return { + changeNum: Number, + /** @type {?} */ + message: Object, + author: { + type: Object, + computed: '_computeAuthor(message)', + }, + comments: { + type: Object, + observer: '_commentsChanged', + }, + config: Object, + hideAutomated: { + type: Boolean, + value: false, + }, + hidden: { + type: Boolean, + computed: '_computeIsHidden(hideAutomated, isAutomated)', + reflectToAttribute: true, + }, + isAutomated: { + type: Boolean, + computed: '_computeIsAutomated(message)', + }, + showOnBehalfOf: { + type: Boolean, + computed: '_computeShowOnBehalfOf(message)', + }, + showReplyButton: { + type: Boolean, + computed: '_computeShowReplyButton(message, _loggedIn)', + }, + projectName: { + type: String, + observer: '_projectNameChanged', + }, - static get properties() { - return { - changeNum: Number, - /** @type {?} */ - message: Object, - author: { - type: Object, - computed: '_computeAuthor(message)', - }, - comments: { - type: Object, - observer: '_commentsChanged', - }, - config: Object, - hideAutomated: { - type: Boolean, - value: false, - }, - hidden: { - type: Boolean, - computed: '_computeIsHidden(hideAutomated, isAutomated)', - reflectToAttribute: true, - }, - isAutomated: { - type: Boolean, - computed: '_computeIsAutomated(message)', - }, - showOnBehalfOf: { - type: Boolean, - computed: '_computeShowOnBehalfOf(message)', - }, - showReplyButton: { - type: Boolean, - computed: '_computeShowReplyButton(message, _loggedIn)', - }, - projectName: { - type: String, - observer: '_projectNameChanged', - }, + /** + * A mapping from label names to objects representing the minimum and + * maximum possible values for that label. + */ + labelExtremes: Object, - /** - * A mapping from label names to objects representing the minimum and - * maximum possible values for that label. - */ - labelExtremes: Object, + /** + * @type {{ commentlinks: Array }} + */ + _projectConfig: Object, + // Computed property needed to trigger Polymer value observing. + _expanded: { + type: Object, + computed: '_computeExpanded(message.expanded)', + }, + _messageContentExpanded: { + type: String, + computed: + '_computeMessageContentExpanded(message.message, message.tag)', + }, + _messageContentCollapsed: { + type: String, + computed: + '_computeMessageContentCollapsed(message.message, message.tag)', + }, + _commentCountText: { + type: Number, + computed: '_computeCommentCountText(comments)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _isAdmin: { + type: Boolean, + value: false, + }, + _isDeletingChangeMsg: { + type: Boolean, + value: false, + }, + }; + } - /** - * @type {{ commentlinks: Array }} - */ - _projectConfig: Object, - // Computed property needed to trigger Polymer value observing. - _expanded: { - type: Object, - computed: '_computeExpanded(message.expanded)', - }, - _messageContentExpanded: { - type: String, - computed: - '_computeMessageContentExpanded(message.message, message.tag)', - }, - _messageContentCollapsed: { - type: String, - computed: - '_computeMessageContentCollapsed(message.message, message.tag)', - }, - _commentCountText: { - type: Number, - computed: '_computeCommentCountText(comments)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _isAdmin: { - type: Boolean, - value: false, - }, - _isDeletingChangeMsg: { - type: Boolean, - value: false, - }, - }; - } + static get observers() { + return [ + '_updateExpandedClass(message.expanded)', + ]; + } - static get observers() { - return [ - '_updateExpandedClass(message.expanded)', - ]; - } + /** @override */ + created() { + super.created(); + this.addEventListener('click', + e => this._handleClick(e)); + } - /** @override */ - created() { - super.created(); - this.addEventListener('click', - e => this._handleClick(e)); - } + /** @override */ + ready() { + super.ready(); + this.$.restAPI.getConfig().then(config => { + this.config = config; + }); + this.$.restAPI.getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + }); + this.$.restAPI.getIsAdmin().then(isAdmin => { + this._isAdmin = isAdmin; + }); + } - /** @override */ - ready() { - super.ready(); - this.$.restAPI.getConfig().then(config => { - this.config = config; - }); - this.$.restAPI.getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - }); - this.$.restAPI.getIsAdmin().then(isAdmin => { - this._isAdmin = isAdmin; - }); - } - - _updateExpandedClass(expanded) { - if (expanded) { - this.classList.add('expanded'); - } else { - this.classList.remove('expanded'); - } - } - - _computeCommentCountText(comments) { - if (!comments) return undefined; - let count = 0; - for (const file in comments) { - if (comments.hasOwnProperty(file)) { - const commentArray = comments[file] || []; - count += commentArray.length; - } - } - if (count === 0) { - return undefined; - } else if (count === 1) { - return '1 comment'; - } else { - return `${count} comments`; - } - } - - _computeMessageContentExpanded(content, tag) { - return this._computeMessageContent(content, tag, true); - } - - _computeMessageContentCollapsed(content, tag) { - return this._computeMessageContent(content, tag, false); - } - - _computeMessageContent(content, tag, isExpanded) { - content = content || ''; - tag = tag || ''; - const isNewPatchSet = tag.endsWith(':newPatchSet') || - tag.endsWith(':newWipPatchSet'); - const lines = content.split('\n'); - const filteredLines = lines.filter(line => { - if (!isExpanded && line.startsWith('>')) { - return false; - } - if (line.startsWith('(') && line.endsWith(' comment)')) { - return false; - } - if (line.startsWith('(') && line.endsWith(' comments)')) { - return false; - } - if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) { - return false; - } - return true; - }); - const mappedLines = filteredLines.map(line => { - // The change message formatting is not very consistent, so - // unfortunately we have to do a bit of tweaking here: - // Labels should be stripped from lines like this: - // Patch Set 29: Verified+1 - // Rebase messages (which have a ':newPatchSet' tag) should be kept on - // lines like this: - // Patch Set 27: Patch Set 26 was rebased - if (isNewPatchSet) { - line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1'); - } - return line; - }); - return mappedLines.join('\n').trim(); - } - - _isMessageContentEmpty() { - return !this._messageContentExpanded - || this._messageContentExpanded.length === 0; - } - - _computeAuthor(message) { - return message.author || message.updated_by; - } - - _computeShowOnBehalfOf(message) { - const author = message.author || message.updated_by; - return !!(author && message.real_author && - author._account_id != message.real_author._account_id); - } - - _computeShowReplyButton(message, loggedIn) { - return message && !!message.message && loggedIn && - !this._computeIsAutomated(message); - } - - _computeExpanded(expanded) { - return expanded; - } - - /** - * If there is no value set on the message object as to whether _expanded - * should be true or not, then _expanded is set to true if there are - * inline comments (otherwise false). - */ - _commentsChanged(value) { - if (this.message && this.message.expanded === undefined) { - this.set('message.expanded', Object.keys(value || {}).length > 0); - } - } - - _handleClick(e) { - if (this.message.expanded) { return; } - e.stopPropagation(); - this.set('message.expanded', true); - } - - _handleAuthorClick(e) { - if (!this.message.expanded) { return; } - e.stopPropagation(); - this.set('message.expanded', false); - } - - _computeIsAutomated(message) { - return !!(message.reviewer || - this._computeIsReviewerUpdate(message) || - (message.tag && message.tag.startsWith('autogenerated'))); - } - - _computeIsHidden(hideAutomated, isAutomated) { - return hideAutomated && isAutomated; - } - - _computeIsReviewerUpdate(event) { - return event.type === 'REVIEWER_UPDATE'; - } - - _getScores(message, labelExtremes) { - if (!message || !message.message || !labelExtremes) { - return []; - } - const line = message.message.split('\n', 1)[0]; - const patchSetPrefix = PATCH_SET_PREFIX_PATTERN; - if (!line.match(patchSetPrefix)) { - return []; - } - const scoresRaw = line.split(patchSetPrefix)[1]; - if (!scoresRaw) { - return []; - } - return scoresRaw.split(' ') - .map(s => s.match(LABEL_TITLE_SCORE_PATTERN)) - .filter(ms => - ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2])) - .map(ms => { - const label = ms[2]; - const value = ms[1] === '-' ? 'removed' : ms[3]; - return {label, value}; - }); - } - - _computeScoreClass(score, labelExtremes) { - // Polymer 2: check for undefined - if ([score, labelExtremes].some(arg => arg === undefined)) { - return ''; - } - if (score.value === 'removed') { - return 'removed'; - } - const classes = []; - if (score.value > 0) { - classes.push('positive'); - } else if (score.value < 0) { - classes.push('negative'); - } - const extremes = labelExtremes[score.label]; - if (extremes) { - const intScore = parseInt(score.value, 10); - if (intScore === extremes.max) { - classes.push('max'); - } else if (intScore === extremes.min) { - classes.push('min'); - } - } - return classes.join(' '); - } - - _computeClass(expanded) { - const classes = []; - classes.push(expanded ? 'expanded' : 'collapsed'); - return classes.join(' '); - } - - _handleAnchorClick(e) { - e.preventDefault(); - this.dispatchEvent(new CustomEvent('message-anchor-tap', { - bubbles: true, - composed: true, - detail: {id: this.message.id}, - })); - } - - _handleReplyTap(e) { - e.preventDefault(); - this.fire('reply', {message: this.message}); - } - - _handleDeleteMessage(e) { - e.preventDefault(); - if (!this.message || !this.message.id) return; - this._isDeletingChangeMsg = true; - this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id) - .then(() => { - this._isDeletingChangeMsg = false; - this.fire('change-message-deleted', {message: this.message}); - }); - } - - _projectNameChanged(name) { - this.$.restAPI.getProjectConfig(name).then(config => { - this._projectConfig = config; - }); - } - - _computeExpandToggleIcon(expanded) { - return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more'; - } - - _toggleExpanded(e) { - e.stopPropagation(); - this.set('message.expanded', !this.message.expanded); + _updateExpandedClass(expanded) { + if (expanded) { + this.classList.add('expanded'); + } else { + this.classList.remove('expanded'); } } - customElements.define(GrMessage.is, GrMessage); -})(); + _computeCommentCountText(comments) { + if (!comments) return undefined; + let count = 0; + for (const file in comments) { + if (comments.hasOwnProperty(file)) { + const commentArray = comments[file] || []; + count += commentArray.length; + } + } + if (count === 0) { + return undefined; + } else if (count === 1) { + return '1 comment'; + } else { + return `${count} comments`; + } + } + + _computeMessageContentExpanded(content, tag) { + return this._computeMessageContent(content, tag, true); + } + + _computeMessageContentCollapsed(content, tag) { + return this._computeMessageContent(content, tag, false); + } + + _computeMessageContent(content, tag, isExpanded) { + content = content || ''; + tag = tag || ''; + const isNewPatchSet = tag.endsWith(':newPatchSet') || + tag.endsWith(':newWipPatchSet'); + const lines = content.split('\n'); + const filteredLines = lines.filter(line => { + if (!isExpanded && line.startsWith('>')) { + return false; + } + if (line.startsWith('(') && line.endsWith(' comment)')) { + return false; + } + if (line.startsWith('(') && line.endsWith(' comments)')) { + return false; + } + if (!isNewPatchSet && line.match(PATCH_SET_PREFIX_PATTERN)) { + return false; + } + return true; + }); + const mappedLines = filteredLines.map(line => { + // The change message formatting is not very consistent, so + // unfortunately we have to do a bit of tweaking here: + // Labels should be stripped from lines like this: + // Patch Set 29: Verified+1 + // Rebase messages (which have a ':newPatchSet' tag) should be kept on + // lines like this: + // Patch Set 27: Patch Set 26 was rebased + if (isNewPatchSet) { + line = line.replace(PATCH_SET_PREFIX_PATTERN, '$1'); + } + return line; + }); + return mappedLines.join('\n').trim(); + } + + _isMessageContentEmpty() { + return !this._messageContentExpanded + || this._messageContentExpanded.length === 0; + } + + _computeAuthor(message) { + return message.author || message.updated_by; + } + + _computeShowOnBehalfOf(message) { + const author = message.author || message.updated_by; + return !!(author && message.real_author && + author._account_id != message.real_author._account_id); + } + + _computeShowReplyButton(message, loggedIn) { + return message && !!message.message && loggedIn && + !this._computeIsAutomated(message); + } + + _computeExpanded(expanded) { + return expanded; + } + + /** + * If there is no value set on the message object as to whether _expanded + * should be true or not, then _expanded is set to true if there are + * inline comments (otherwise false). + */ + _commentsChanged(value) { + if (this.message && this.message.expanded === undefined) { + this.set('message.expanded', Object.keys(value || {}).length > 0); + } + } + + _handleClick(e) { + if (this.message.expanded) { return; } + e.stopPropagation(); + this.set('message.expanded', true); + } + + _handleAuthorClick(e) { + if (!this.message.expanded) { return; } + e.stopPropagation(); + this.set('message.expanded', false); + } + + _computeIsAutomated(message) { + return !!(message.reviewer || + this._computeIsReviewerUpdate(message) || + (message.tag && message.tag.startsWith('autogenerated'))); + } + + _computeIsHidden(hideAutomated, isAutomated) { + return hideAutomated && isAutomated; + } + + _computeIsReviewerUpdate(event) { + return event.type === 'REVIEWER_UPDATE'; + } + + _getScores(message, labelExtremes) { + if (!message || !message.message || !labelExtremes) { + return []; + } + const line = message.message.split('\n', 1)[0]; + const patchSetPrefix = PATCH_SET_PREFIX_PATTERN; + if (!line.match(patchSetPrefix)) { + return []; + } + const scoresRaw = line.split(patchSetPrefix)[1]; + if (!scoresRaw) { + return []; + } + return scoresRaw.split(' ') + .map(s => s.match(LABEL_TITLE_SCORE_PATTERN)) + .filter(ms => + ms && ms.length === 4 && labelExtremes.hasOwnProperty(ms[2])) + .map(ms => { + const label = ms[2]; + const value = ms[1] === '-' ? 'removed' : ms[3]; + return {label, value}; + }); + } + + _computeScoreClass(score, labelExtremes) { + // Polymer 2: check for undefined + if ([score, labelExtremes].some(arg => arg === undefined)) { + return ''; + } + if (score.value === 'removed') { + return 'removed'; + } + const classes = []; + if (score.value > 0) { + classes.push('positive'); + } else if (score.value < 0) { + classes.push('negative'); + } + const extremes = labelExtremes[score.label]; + if (extremes) { + const intScore = parseInt(score.value, 10); + if (intScore === extremes.max) { + classes.push('max'); + } else if (intScore === extremes.min) { + classes.push('min'); + } + } + return classes.join(' '); + } + + _computeClass(expanded) { + const classes = []; + classes.push(expanded ? 'expanded' : 'collapsed'); + return classes.join(' '); + } + + _handleAnchorClick(e) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent('message-anchor-tap', { + bubbles: true, + composed: true, + detail: {id: this.message.id}, + })); + } + + _handleReplyTap(e) { + e.preventDefault(); + this.fire('reply', {message: this.message}); + } + + _handleDeleteMessage(e) { + e.preventDefault(); + if (!this.message || !this.message.id) return; + this._isDeletingChangeMsg = true; + this.$.restAPI.deleteChangeCommitMessage(this.changeNum, this.message.id) + .then(() => { + this._isDeletingChangeMsg = false; + this.fire('change-message-deleted', {message: this.message}); + }); + } + + _projectNameChanged(name) { + this.$.restAPI.getProjectConfig(name).then(config => { + this._projectConfig = config; + }); + } + + _computeExpandToggleIcon(expanded) { + return expanded ? 'gr-icons:expand-less' : 'gr-icons:expand-more'; + } + + _toggleExpanded(e) { + e.stopPropagation(); + this.set('message.expanded', !this.message.expanded); + } +} + +customElements.define(GrMessage.is, GrMessage);
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js index f7ef7e6..49ced2a 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_html.js
@@ -1,36 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-account-label/gr-account-label.html"> -<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../styles/gr-voting-styles.html"> - -<link rel="import" href="../gr-comment-list/gr-comment-list.html"> - -<dom-module id="gr-message"> - <template> +export const htmlTemplate = html` <style include="gr-voting-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -193,16 +179,16 @@ } } </style> - <div class$="[[_computeClass(_expanded)]]"> + <div class\$="[[_computeClass(_expanded)]]"> <div class="contentContainer"> <div class="author" on-click="_handleAuthorClick"> - <span hidden$="[[!showOnBehalfOf]]"> + <span hidden\$="[[!showOnBehalfOf]]"> <span class="name">[[message.real_author.name]]</span> on behalf of </span> <gr-account-label account="[[author]]" class="authorLabel"></gr-account-label> <template is="dom-repeat" items="[[_getScores(message, labelExtremes)]]" as="score"> - <span class$="score [[_computeScoreClass(score, labelExtremes)]]"> + <span class\$="score [[_computeScoreClass(score, labelExtremes)]]"> [[score.label]] [[score.value]] </span> </template> @@ -216,32 +202,18 @@ <template is="dom-if" if="[[message.message]]"> <div class="content"> <div class="message hideOnOpen">[[_messageContentCollapsed]]</div> - <gr-formatted-text - no-trailing-margin - class="message hideOnCollapsed" - content="[[_messageContentExpanded]]" - config="[[_projectConfig.commentlinks]]"></gr-formatted-text> + <gr-formatted-text no-trailing-margin="" class="message hideOnCollapsed" content="[[_messageContentExpanded]]" config="[[_projectConfig.commentlinks]]"></gr-formatted-text> <template is="dom-if" if="[[!_isMessageContentEmpty()]]"> - <div class="replyActionContainer" hidden$="[[!showReplyButton]]" hidden> - <gr-button - class="replyBtn" - link small on-click="_handleReplyTap"> + <div class="replyActionContainer" hidden\$="[[!showReplyButton]]" hidden=""> + <gr-button class="replyBtn" link="" small="" on-click="_handleReplyTap"> Reply </gr-button> - <gr-button - disabled$=[[_isDeletingChangeMsg]] - class="deleteBtn" hidden$="[[!_isAdmin]]" hidden - link small on-click="_handleDeleteMessage"> + <gr-button disabled\$="[[_isDeletingChangeMsg]]" class="deleteBtn" hidden\$="[[!_isAdmin]]" hidden="" link="" small="" on-click="_handleDeleteMessage"> Delete </gr-button> </div> </template> - <gr-comment-list - comments="[[comments]]" - change-num="[[changeNum]]" - patch-num="[[message._revision_number]]" - project-name="[[projectName]]" - project-config="[[_projectConfig]]"></gr-comment-list> + <gr-comment-list comments="[[comments]]" change-num="[[changeNum]]" patch-num="[[message._revision_number]]" project-name="[[projectName]]" project-config="[[_projectConfig]]"></gr-comment-list> </div> </template> <template is="dom-if" if="[[_computeIsReviewerUpdate(message)]]"> @@ -249,8 +221,7 @@ <template is="dom-repeat" items="[[message.updates]]" as="update"> <div class="updateCategory"> [[update.message]] - <template - is="dom-repeat" items="[[update.reviewers]]" as="reviewer"> + <template is="dom-repeat" items="[[update.reviewers]]" as="reviewer"> <gr-account-chip account="[[reviewer]]"> </gr-account-chip> </template> @@ -264,29 +235,17 @@ </template> <template is="dom-if" if="[[!message.id]]"> <span class="date"> - <gr-date-formatter - has-tooltip - show-date-and-time - date-str="[[message.date]]"></gr-date-formatter> + <gr-date-formatter has-tooltip="" show-date-and-time="" date-str="[[message.date]]"></gr-date-formatter> </span> </template> <template is="dom-if" if="[[message.id]]"> <span class="date" on-click="_handleAnchorClick"> - <gr-date-formatter - has-tooltip - show-date-and-time - date-str="[[message.date]]"></gr-date-formatter> + <gr-date-formatter has-tooltip="" show-date-and-time="" date-str="[[message.date]]"></gr-date-formatter> </span> </template> - <iron-icon - id="expandToggle" - on-click="_toggleExpanded" - title="Toggle expanded state" - icon="[[_computeExpandToggleIcon(_expanded)]]"></iron-icon> + <iron-icon id="expandToggle" on-click="_toggleExpanded" title="Toggle expanded state" icon="[[_computeExpandToggleIcon(_expanded)]]"></iron-icon> </span> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-message.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html index f22f17e..04f12c2 100644 --- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html +++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-message</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-message.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-message.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-message.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,386 +40,389 @@ </template> </test-fixture> -<script> - suite('gr-message tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-message.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-message tests', () => { + let element; - suite('when admin and logged in', () => { - setup(done => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getPreferences() { return Promise.resolve({}); }, - getConfig() { return Promise.resolve({}); }, - getIsAdmin() { return Promise.resolve(true); }, - deleteChangeCommitMessage() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - flush(done); + suite('when admin and logged in', () => { + setup(done => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getPreferences() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, + getIsAdmin() { return Promise.resolve(true); }, + deleteChangeCommitMessage() { return Promise.resolve({}); }, }); + element = fixture('basic'); + flush(done); + }); - test('reply event', done => { - element.message = { - id: '47c43261_55aa2c41', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:24:49.448000000', - message: 'Uploaded patch set 1.', - _revision_number: 1, - }; + test('reply event', done => { + element.message = { + id: '47c43261_55aa2c41', + author: { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }, + date: '2016-01-12 20:24:49.448000000', + message: 'Uploaded patch set 1.', + _revision_number: 1, + }; - element.addEventListener('reply', e => { - assert.deepEqual(e.detail.message, element.message); - done(); - }); - flushAsynchronousOperations(); - assert.isFalse( - element.shadowRoot.querySelector('.replyActionContainer').hidden - ); - MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn')); + element.addEventListener('reply', e => { + assert.deepEqual(e.detail.message, element.message); + done(); }); + flushAsynchronousOperations(); + assert.isFalse( + element.shadowRoot.querySelector('.replyActionContainer').hidden + ); + MockInteractions.tap(element.shadowRoot.querySelector('.replyBtn')); + }); - test('can see delete button', () => { - element.message = { - id: '47c43261_55aa2c41', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:24:49.448000000', - message: 'Uploaded patch set 1.', - _revision_number: 1, - }; + test('can see delete button', () => { + element.message = { + id: '47c43261_55aa2c41', + author: { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }, + date: '2016-01-12 20:24:49.448000000', + message: 'Uploaded patch set 1.', + _revision_number: 1, + }; - flushAsynchronousOperations(); - assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden); + flushAsynchronousOperations(); + assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').hidden); + }); + + test('delete change message', done => { + element.message = { + id: '47c43261_55aa2c41', + author: { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }, + date: '2016-01-12 20:24:49.448000000', + message: 'Uploaded patch set 1.', + _revision_number: 1, + }; + + element.addEventListener('change-message-deleted', e => { + assert.deepEqual(e.detail.message, element.message); + assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled); + done(); }); + flushAsynchronousOperations(); + MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn')); + assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled); + }); - test('delete change message', done => { - element.message = { - id: '47c43261_55aa2c41', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:24:49.448000000', - message: 'Uploaded patch set 1.', - _revision_number: 1, - }; + test('autogenerated prefix hiding', () => { + element.message = { + tag: 'autogenerated:gerrit:test', + updated: '2016-01-12 20:24:49.448000000', + }; - element.addEventListener('change-message-deleted', e => { - assert.deepEqual(e.detail.message, element.message); - assert.isFalse(element.shadowRoot.querySelector('.deleteBtn').disabled); - done(); - }); - flushAsynchronousOperations(); - MockInteractions.tap(element.shadowRoot.querySelector('.deleteBtn')); - assert.isTrue(element.shadowRoot.querySelector('.deleteBtn').disabled); - }); + assert.isTrue(element.isAutomated); + assert.isFalse(element.hidden); - test('autogenerated prefix hiding', () => { - element.message = { - tag: 'autogenerated:gerrit:test', - updated: '2016-01-12 20:24:49.448000000', - }; + element.hideAutomated = true; - assert.isTrue(element.isAutomated); - assert.isFalse(element.hidden); + assert.isTrue(element.hidden); + }); - element.hideAutomated = true; + test('reviewer message treated as autogenerated', () => { + element.message = { + tag: 'autogenerated:gerrit:test', + updated: '2016-01-12 20:24:49.448000000', + reviewer: {}, + }; - assert.isTrue(element.hidden); - }); + assert.isTrue(element.isAutomated); + assert.isFalse(element.hidden); - test('reviewer message treated as autogenerated', () => { - element.message = { - tag: 'autogenerated:gerrit:test', - updated: '2016-01-12 20:24:49.448000000', - reviewer: {}, - }; + element.hideAutomated = true; - assert.isTrue(element.isAutomated); - assert.isFalse(element.hidden); + assert.isTrue(element.hidden); + }); - element.hideAutomated = true; + test('batch reviewer message treated as autogenerated', () => { + element.message = { + type: 'REVIEWER_UPDATE', + updated: '2016-01-12 20:24:49.448000000', + reviewer: {}, + }; - assert.isTrue(element.hidden); - }); + assert.isTrue(element.isAutomated); + assert.isFalse(element.hidden); - test('batch reviewer message treated as autogenerated', () => { - element.message = { - type: 'REVIEWER_UPDATE', - updated: '2016-01-12 20:24:49.448000000', - reviewer: {}, - }; + element.hideAutomated = true; - assert.isTrue(element.isAutomated); - assert.isFalse(element.hidden); + assert.isTrue(element.hidden); + }); - element.hideAutomated = true; + test('tag that is not autogenerated prefix does not hide', () => { + element.message = { + tag: 'something', + updated: '2016-01-12 20:24:49.448000000', + }; - assert.isTrue(element.hidden); - }); + assert.isFalse(element.isAutomated); + assert.isFalse(element.hidden); - test('tag that is not autogenerated prefix does not hide', () => { - element.message = { - tag: 'something', - updated: '2016-01-12 20:24:49.448000000', - }; + element.hideAutomated = true; - assert.isFalse(element.isAutomated); - assert.isFalse(element.hidden); + assert.isFalse(element.hidden); + }); - element.hideAutomated = true; + test('reply button hidden unless logged in', () => { + const message = { + message: 'Uploaded patch set 1.', + }; + assert.isFalse(element._computeShowReplyButton(message, false)); + assert.isTrue(element._computeShowReplyButton(message, true)); + }); - assert.isFalse(element.hidden); - }); + test('_computeShowOnBehalfOf', () => { + const message = { + message: '...', + }; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + message.author = {_account_id: 1115495}; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + message.real_author = {_account_id: 1115495}; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + message.real_author._account_id = 123456; + assert.isOk(element._computeShowOnBehalfOf(message)); + message.updated_by = message.author; + delete message.author; + assert.isOk(element._computeShowOnBehalfOf(message)); + delete message.updated_by; + assert.isNotOk(element._computeShowOnBehalfOf(message)); + }); - test('reply button hidden unless logged in', () => { - const message = { - message: 'Uploaded patch set 1.', - }; - assert.isFalse(element._computeShowReplyButton(message, false)); - assert.isTrue(element._computeShowReplyButton(message, true)); - }); - - test('_computeShowOnBehalfOf', () => { - const message = { - message: '...', - }; - assert.isNotOk(element._computeShowOnBehalfOf(message)); - message.author = {_account_id: 1115495}; - assert.isNotOk(element._computeShowOnBehalfOf(message)); - message.real_author = {_account_id: 1115495}; - assert.isNotOk(element._computeShowOnBehalfOf(message)); - message.real_author._account_id = 123456; - assert.isOk(element._computeShowOnBehalfOf(message)); - message.updated_by = message.author; - delete message.author; - assert.isOk(element._computeShowOnBehalfOf(message)); - delete message.updated_by; - assert.isNotOk(element._computeShowOnBehalfOf(message)); - }); - - ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => { - test(`${label} ignored for color voting`, () => { - element.message = { - author: {}, - expanded: false, - message: `Patch Set 1: ${label}+1`, - }; - assert.isNotOk( - Polymer.dom(element.root).querySelector('.negativeVote')); - assert.isNotOk( - Polymer.dom(element.root).querySelector('.positiveVote')); - }); - }); - - test('clicking on date link fires event', () => { - element.message = { - type: 'REVIEWER_UPDATE', - updated: '2016-01-12 20:24:49.448000000', - reviewer: {}, - id: '47c43261_55aa2c41', - }; - flushAsynchronousOperations(); - const stub = sinon.stub(); - element.addEventListener('message-anchor-tap', stub); - const dateEl = element.shadowRoot - .querySelector('.date'); - assert.ok(dateEl); - MockInteractions.tap(dateEl); - - assert.isTrue(stub.called); - assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id}); - }); - - suite('compute messages', () => { - test('empty', () => { - assert.equal(element._computeMessageContent('', '', true), ''); - assert.equal(element._computeMessageContent('', '', false), ''); - }); - - test('new patchset', () => { - const original = 'Uploaded patch set 1.'; - const tag = 'autogenerated:gerrit:newPatchSet'; - let actual = element._computeMessageContent(original, tag, true); - assert.equal(actual, original); - actual = element._computeMessageContent(original, tag, false); - assert.equal(actual, original); - }); - - test('new patchset rebased', () => { - const original = 'Patch Set 27: Patch Set 26 was rebased'; - const tag = 'autogenerated:gerrit:newPatchSet'; - const expected = 'Patch Set 26 was rebased'; - let actual = element._computeMessageContent(original, tag, true); - assert.equal(actual, expected); - actual = element._computeMessageContent(original, tag, false); - assert.equal(actual, expected); - }); - - test('ready for review', () => { - const original = 'Patch Set 1:\n\nThis change is ready for review.'; - const tag = undefined; - const expected = 'This change is ready for review.'; - let actual = element._computeMessageContent(original, tag, true); - assert.equal(actual, expected); - actual = element._computeMessageContent(original, tag, false); - assert.equal(actual, expected); - }); - - test('vote', () => { - const original = 'Patch Set 1: Code-Style+1'; - const tag = undefined; - const expected = ''; - let actual = element._computeMessageContent(original, tag, true); - assert.equal(actual, expected); - actual = element._computeMessageContent(original, tag, false); - assert.equal(actual, expected); - }); - - test('comments', () => { - const original = 'Patch Set 1:\n\n(3 comments)'; - const tag = undefined; - const expected = ''; - let actual = element._computeMessageContent(original, tag, true); - assert.equal(actual, expected); - actual = element._computeMessageContent(original, tag, false); - assert.equal(actual, expected); - }); - }); - - test('votes', () => { + ['Trybot-Ready', 'Tryjob-Request', 'Commit-Queue'].forEach(label => { + test(`${label} ignored for color voting`, () => { element.message = { author: {}, expanded: false, - message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1', + message: `Patch Set 1: ${label}+1`, }; - element.labelExtremes = { - 'Verified': {max: 1, min: -1}, - 'Code-Review': {max: 2, min: -2}, - 'Trybot-Label3': {max: 3, min: 0}, - }; - flushAsynchronousOperations(); - const scoreChips = Polymer.dom(element.root).querySelectorAll('.score'); - assert.equal(scoreChips.length, 3); - - assert.isTrue(scoreChips[0].classList.contains('positive')); - assert.isTrue(scoreChips[0].classList.contains('max')); - - assert.isTrue(scoreChips[1].classList.contains('negative')); - assert.isTrue(scoreChips[1].classList.contains('min')); - - assert.isTrue(scoreChips[2].classList.contains('positive')); - assert.isFalse(scoreChips[2].classList.contains('min')); - }); - - test('removed votes', () => { - element.message = { - author: {}, - expanded: false, - message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue', - }; - element.labelExtremes = { - 'Verified': {max: 1, min: -1}, - 'Code-Review': {max: 2, min: -2}, - 'Commit-Queue': {max: 3, min: 0}, - }; - flushAsynchronousOperations(); - const scoreChips = Polymer.dom(element.root).querySelectorAll('.score'); - assert.equal(scoreChips.length, 3); - - assert.isTrue(scoreChips[1].classList.contains('removed')); - assert.isTrue(scoreChips[2].classList.contains('removed')); - }); - - test('false negative vote', () => { - element.message = { - author: {}, - expanded: false, - message: 'Patch Set 1: Cherry Picked from branch stable-2.14.', - }; - element.labelExtremes = {}; - const scoreChips = Polymer.dom(element.root).querySelectorAll('.score'); - assert.equal(scoreChips.length, 0); + assert.isNotOk( + dom(element.root).querySelector('.negativeVote')); + assert.isNotOk( + dom(element.root).querySelector('.positiveVote')); }); }); - suite('when not logged in', () => { - setup(done => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - getPreferences() { return Promise.resolve({}); }, - getConfig() { return Promise.resolve({}); }, - getIsAdmin() { return Promise.resolve(false); }, - deleteChangeCommitMessage() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - flush(done); + test('clicking on date link fires event', () => { + element.message = { + type: 'REVIEWER_UPDATE', + updated: '2016-01-12 20:24:49.448000000', + reviewer: {}, + id: '47c43261_55aa2c41', + }; + flushAsynchronousOperations(); + const stub = sinon.stub(); + element.addEventListener('message-anchor-tap', stub); + const dateEl = element.shadowRoot + .querySelector('.date'); + assert.ok(dateEl); + MockInteractions.tap(dateEl); + + assert.isTrue(stub.called); + assert.deepEqual(stub.lastCall.args[0].detail, {id: element.message.id}); + }); + + suite('compute messages', () => { + test('empty', () => { + assert.equal(element._computeMessageContent('', '', true), ''); + assert.equal(element._computeMessageContent('', '', false), ''); }); - test('reply and delete button should be hidden', () => { - element.message = { - id: '47c43261_55aa2c41', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:24:49.448000000', - message: 'Uploaded patch set 1.', - _revision_number: 1, - }; + test('new patchset', () => { + const original = 'Uploaded patch set 1.'; + const tag = 'autogenerated:gerrit:newPatchSet'; + let actual = element._computeMessageContent(original, tag, true); + assert.equal(actual, original); + actual = element._computeMessageContent(original, tag, false); + assert.equal(actual, original); + }); - flushAsynchronousOperations(); - assert.isTrue( - element.shadowRoot.querySelector('.replyActionContainer').hidden - ); - assert.isTrue( - element.shadowRoot.querySelector('.deleteBtn').hidden - ); + test('new patchset rebased', () => { + const original = 'Patch Set 27: Patch Set 26 was rebased'; + const tag = 'autogenerated:gerrit:newPatchSet'; + const expected = 'Patch Set 26 was rebased'; + let actual = element._computeMessageContent(original, tag, true); + assert.equal(actual, expected); + actual = element._computeMessageContent(original, tag, false); + assert.equal(actual, expected); + }); + + test('ready for review', () => { + const original = 'Patch Set 1:\n\nThis change is ready for review.'; + const tag = undefined; + const expected = 'This change is ready for review.'; + let actual = element._computeMessageContent(original, tag, true); + assert.equal(actual, expected); + actual = element._computeMessageContent(original, tag, false); + assert.equal(actual, expected); + }); + + test('vote', () => { + const original = 'Patch Set 1: Code-Style+1'; + const tag = undefined; + const expected = ''; + let actual = element._computeMessageContent(original, tag, true); + assert.equal(actual, expected); + actual = element._computeMessageContent(original, tag, false); + assert.equal(actual, expected); + }); + + test('comments', () => { + const original = 'Patch Set 1:\n\n(3 comments)'; + const tag = undefined; + const expected = ''; + let actual = element._computeMessageContent(original, tag, true); + assert.equal(actual, expected); + actual = element._computeMessageContent(original, tag, false); + assert.equal(actual, expected); }); }); - suite('when logged in but not admin', () => { - setup(done => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getConfig() { return Promise.resolve({}); }, - getIsAdmin() { return Promise.resolve(false); }, - deleteChangeCommitMessage() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - flush(done); - }); + test('votes', () => { + element.message = { + author: {}, + expanded: false, + message: 'Patch Set 1: Verified+1 Code-Review-2 Trybot-Label3+1 Blub+1', + }; + element.labelExtremes = { + 'Verified': {max: 1, min: -1}, + 'Code-Review': {max: 2, min: -2}, + 'Trybot-Label3': {max: 3, min: 0}, + }; + flushAsynchronousOperations(); + const scoreChips = dom(element.root).querySelectorAll('.score'); + assert.equal(scoreChips.length, 3); - test('can see reply but not delete button', () => { - element.message = { - id: '47c43261_55aa2c41', - author: { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }, - date: '2016-01-12 20:24:49.448000000', - message: 'Uploaded patch set 1.', - _revision_number: 1, - }; + assert.isTrue(scoreChips[0].classList.contains('positive')); + assert.isTrue(scoreChips[0].classList.contains('max')); - flushAsynchronousOperations(); - assert.isFalse( - element.shadowRoot.querySelector('.replyActionContainer').hidden - ); - assert.isTrue( - element.shadowRoot.querySelector('.deleteBtn').hidden - ); - }); + assert.isTrue(scoreChips[1].classList.contains('negative')); + assert.isTrue(scoreChips[1].classList.contains('min')); + + assert.isTrue(scoreChips[2].classList.contains('positive')); + assert.isFalse(scoreChips[2].classList.contains('min')); + }); + + test('removed votes', () => { + element.message = { + author: {}, + expanded: false, + message: 'Patch Set 1: Verified+1 -Code-Review -Commit-Queue', + }; + element.labelExtremes = { + 'Verified': {max: 1, min: -1}, + 'Code-Review': {max: 2, min: -2}, + 'Commit-Queue': {max: 3, min: 0}, + }; + flushAsynchronousOperations(); + const scoreChips = dom(element.root).querySelectorAll('.score'); + assert.equal(scoreChips.length, 3); + + assert.isTrue(scoreChips[1].classList.contains('removed')); + assert.isTrue(scoreChips[2].classList.contains('removed')); + }); + + test('false negative vote', () => { + element.message = { + author: {}, + expanded: false, + message: 'Patch Set 1: Cherry Picked from branch stable-2.14.', + }; + element.labelExtremes = {}; + const scoreChips = dom(element.root).querySelectorAll('.score'); + assert.equal(scoreChips.length, 0); }); }); + + suite('when not logged in', () => { + setup(done => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + getPreferences() { return Promise.resolve({}); }, + getConfig() { return Promise.resolve({}); }, + getIsAdmin() { return Promise.resolve(false); }, + deleteChangeCommitMessage() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + flush(done); + }); + + test('reply and delete button should be hidden', () => { + element.message = { + id: '47c43261_55aa2c41', + author: { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }, + date: '2016-01-12 20:24:49.448000000', + message: 'Uploaded patch set 1.', + _revision_number: 1, + }; + + flushAsynchronousOperations(); + assert.isTrue( + element.shadowRoot.querySelector('.replyActionContainer').hidden + ); + assert.isTrue( + element.shadowRoot.querySelector('.deleteBtn').hidden + ); + }); + }); + + suite('when logged in but not admin', () => { + setup(done => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getConfig() { return Promise.resolve({}); }, + getIsAdmin() { return Promise.resolve(false); }, + deleteChangeCommitMessage() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + flush(done); + }); + + test('can see reply but not delete button', () => { + element.message = { + id: '47c43261_55aa2c41', + author: { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }, + date: '2016-01-12 20:24:49.448000000', + message: 'Uploaded patch set 1.', + _revision_number: 1, + }; + + flushAsynchronousOperations(); + assert.isFalse( + element.shadowRoot.querySelector('.replyActionContainer').hidden + ); + assert.isTrue( + element.shadowRoot.querySelector('.deleteBtn').hidden + ); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js index 6f74e4b..eaac988 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,416 +14,429 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const MAX_INITIAL_SHOWN_MESSAGES = 20; - const MESSAGES_INCREMENT = 5; +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../shared/gr-button/gr-button.js'; +import '../gr-message/gr-message.js'; +import '../../../styles/shared-styles.js'; +import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-messages-list_html.js'; - const ReportingEvent = { - SHOW_ALL: 'show-all-messages', - SHOW_MORE: 'show-more-messages', - }; +const MAX_INITIAL_SHOWN_MESSAGES = 20; +const MESSAGES_INCREMENT = 5; - /** - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element - */ - class GrMessagesList extends Polymer.mixinBehaviors( [ - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-messages-list'; } +const ReportingEvent = { + SHOW_ALL: 'show-all-messages', + SHOW_MORE: 'show-more-messages', +}; - static get properties() { - return { - changeNum: Number, - messages: { - type: Array, - value() { return []; }, - }, - reviewerUpdates: { - type: Array, - value() { return []; }, - }, - changeComments: Object, - projectName: String, - showReplyButtons: { - type: Boolean, - value: false, - }, - labels: Object, +/** + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrMessagesList extends mixinBehaviors( [ + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _expanded: { - type: Boolean, - value: false, - observer: '_expandedChanged', - }, + static get is() { return 'gr-messages-list'; } - _expandCollapseTitle: { - type: String, - }, + static get properties() { + return { + changeNum: Number, + messages: { + type: Array, + value() { return []; }, + }, + reviewerUpdates: { + type: Array, + value() { return []; }, + }, + changeComments: Object, + projectName: String, + showReplyButtons: { + type: Boolean, + value: false, + }, + labels: Object, - _hideAutomated: { - type: Boolean, - value: false, - }, - /** - * The messages after processing and including merged reviewer updates. - */ - _processedMessages: { - type: Array, - computed: '_computeItems(messages, reviewerUpdates)', - observer: '_processedMessagesChanged', - }, - /** - * The subset of _processedMessages that is visible to the user. - */ - _visibleMessages: { - type: Array, - value() { return []; }, - }, + _expanded: { + type: Boolean, + value: false, + observer: '_expandedChanged', + }, - _labelExtremes: { - type: Object, - computed: '_computeLabelExtremes(labels.*)', - }, - }; - } + _expandCollapseTitle: { + type: String, + }, - scrollToMessage(messageID) { - let el = this.shadowRoot - .querySelector('[data-message-id="' + messageID + '"]'); - // If the message is hidden, expand the hidden messages back to that - // point. - if (!el) { - let index; - for (index = 0; index < this._processedMessages.length; index++) { - if (this._processedMessages[index].id === messageID) { - break; - } - } - if (index === this._processedMessages.length) { return; } + _hideAutomated: { + type: Boolean, + value: false, + }, + /** + * The messages after processing and including merged reviewer updates. + */ + _processedMessages: { + type: Array, + computed: '_computeItems(messages, reviewerUpdates)', + observer: '_processedMessagesChanged', + }, + /** + * The subset of _processedMessages that is visible to the user. + */ + _visibleMessages: { + type: Array, + value() { return []; }, + }, - const newMessages = this._processedMessages.slice(index, - -this._visibleMessages.length); - // Add newMessages to the beginning of _visibleMessages. - this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); - // Allow the dom-repeat to stamp. - Polymer.dom.flush(); - el = this.shadowRoot - .querySelector('[data-message-id="' + messageID + '"]'); - } + _labelExtremes: { + type: Object, + computed: '_computeLabelExtremes(labels.*)', + }, + }; + } - el.set('message.expanded', true); - let top = el.offsetTop; - for (let offsetParent = el.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - window.scrollTo(0, top); - this._highlightEl(el); - } - - _isAutomated(message) { - return !!(message.reviewer || - (message.tag && message.tag.startsWith('autogenerated'))); - } - - _computeItems(messages, reviewerUpdates) { - // Polymer 2: check for undefined - if ([messages, reviewerUpdates].some(arg => arg === undefined)) { - return []; - } - - messages = messages || []; - reviewerUpdates = reviewerUpdates || []; - let mi = 0; - let ri = 0; - let result = []; - let mDate; - let rDate; - for (let i = 0; i < messages.length; i++) { - messages[i]._index = i; - } - - while (mi < messages.length || ri < reviewerUpdates.length) { - if (mi >= messages.length) { - result = result.concat(reviewerUpdates.slice(ri)); + scrollToMessage(messageID) { + let el = this.shadowRoot + .querySelector('[data-message-id="' + messageID + '"]'); + // If the message is hidden, expand the hidden messages back to that + // point. + if (!el) { + let index; + for (index = 0; index < this._processedMessages.length; index++) { + if (this._processedMessages[index].id === messageID) { break; } - if (ri >= reviewerUpdates.length) { - result = result.concat(messages.slice(mi)); - break; - } - mDate = mDate || util.parseDate(messages[mi].date); - rDate = rDate || util.parseDate(reviewerUpdates[ri].date); - if (rDate < mDate) { - result.push(reviewerUpdates[ri++]); - rDate = null; - } else { - result.push(messages[mi++]); - mDate = null; - } } - return result; - } + if (index === this._processedMessages.length) { return; } - _expandedChanged(exp) { - if (this._processedMessages) { - for (let i = 0; i < this._processedMessages.length; i++) { - this._processedMessages[i].expanded = exp; - } - } - // _visibleMessages is a subarray of _processedMessages - // _processedMessages contains all items from _visibleMessages - // At this point all _visibleMessages.expanded values are set, - // and notifyPath must be used to notify Polymer about changes. - if (this._visibleMessages) { - for (let i = 0; i < this._visibleMessages.length; i++) { - this.notifyPath(`_visibleMessages.${i}.expanded`); - } - } - - if (this._expanded) { - this._expandCollapseTitle = this.createTitle( - this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS); - } else { - this._expandCollapseTitle = this.createTitle( - this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS); - } - } - - _highlightEl(el) { - const highlightedEls = - Polymer.dom(this.root).querySelectorAll('.highlighted'); - for (const highlighedEl of highlightedEls) { - highlighedEl.classList.remove('highlighted'); - } - function handleAnimationEnd() { - el.removeEventListener('animationend', handleAnimationEnd); - el.classList.remove('highlighted'); - } - el.addEventListener('animationend', handleAnimationEnd); - el.classList.add('highlighted'); - } - - /** - * @param {boolean} expand - */ - handleExpandCollapse(expand) { - this._expanded = expand; - } - - _handleExpandCollapseTap(e) { - e.preventDefault(); - this.handleExpandCollapse(!this._expanded); - } - - _handleAnchorClick(e) { - this.scrollToMessage(e.detail.id); - } - - _hasAutomatedMessages(messages) { - if (!messages) { return false; } - for (const message of messages) { - if (this._isAutomated(message)) { - return true; - } - } - return false; - } - - _computeExpandCollapseMessage(expanded) { - return expanded ? 'Collapse all' : 'Expand all'; - } - - /** - * Computes message author's file comments for change's message. - * Method uses this.messages to find next message and relies on messages - * to be sorted by date field descending. - * - * @param {!Object} changeComments changeComment object, which includes - * a method to get all published comments (including robot comments), - * which returns a Hash of arrays of comments, filename as key. - * @param {!Object} message - * @return {!Object} Hash of arrays of comments, filename as key. - */ - _computeCommentsForMessage(changeComments, message) { - if ([changeComments, message].some(arg => arg === undefined)) { - return []; - } - const comments = changeComments.getAllPublishedComments(); - if (message._index === undefined || !comments || !this.messages) { - return []; - } - const messages = this.messages || []; - const index = message._index; - const authorId = message.author && message.author._account_id; - const mDate = util.parseDate(message.date).getTime(); - // NB: Messages array has oldest messages first. - let nextMDate; - if (index > 0) { - for (let i = index - 1; i >= 0; i--) { - if (messages[i] && messages[i].author && - messages[i].author._account_id === authorId) { - nextMDate = util.parseDate(messages[i].date).getTime(); - break; - } - } - } - const msgComments = {}; - for (const file in comments) { - if (!comments.hasOwnProperty(file)) { continue; } - const fileComments = comments[file]; - for (let i = 0; i < fileComments.length; i++) { - if (fileComments[i].author && - fileComments[i].author._account_id !== authorId) { - continue; - } - const cDate = util.parseDate(fileComments[i].updated).getTime(); - if (cDate <= mDate) { - if (nextMDate && cDate <= nextMDate) { - continue; - } - msgComments[file] = msgComments[file] || []; - msgComments[file].push(fileComments[i]); - } - } - } - return msgComments; - } - - /** - * Returns the number of messages to splice to the beginning of - * _visibleMessages. This is the minimum of the total number of messages - * remaining in the list and the number of messages needed to display five - * more visible messages in the list. - */ - _getDelta(visibleMessages, messages, hideAutomated) { - if ([visibleMessages, messages].some(arg => arg === undefined)) { - return 0; - } - - let delta = MESSAGES_INCREMENT; - const msgsRemaining = messages.length - visibleMessages.length; - - if (hideAutomated) { - let counter = 0; - let i; - for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) { - if (!this._isAutomated(messages[i - 1])) { counter++; } - } - delta = msgsRemaining - i; - } - return Math.min(msgsRemaining, delta); - } - - /** - * Gets the number of messages that would be visible, but do not currently - * exist in _visibleMessages. - */ - _numRemaining(visibleMessages, messages, hideAutomated) { - if ([visibleMessages, messages].some(arg => arg === undefined)) { - return 0; - } - - if (hideAutomated) { - return this._getHumanMessages(messages).length - - this._getHumanMessages(visibleMessages).length; - } - return messages.length - visibleMessages.length; - } - - _computeIncrementText(visibleMessages, messages, hideAutomated) { - let delta = this._getDelta(visibleMessages, messages, hideAutomated); - delta = Math.min( - this._numRemaining(visibleMessages, messages, hideAutomated), delta); - return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more'; - } - - _getHumanMessages(messages) { - return messages.filter(msg => !this._isAutomated(msg)); - } - - _computeShowHideTextHidden(visibleMessages, messages, - hideAutomated) { - if ([visibleMessages, messages].some(arg => arg === undefined)) { - return 0; - } - - if (hideAutomated) { - messages = this._getHumanMessages(messages); - visibleMessages = this._getHumanMessages(visibleMessages); - } - return visibleMessages.length >= messages.length; - } - - _handleShowAllTap() { - this._visibleMessages = this._processedMessages; - this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL); - } - - _handleIncrementShownMessages() { - const delta = this._getDelta(this._visibleMessages, - this._processedMessages, this._hideAutomated); - const len = this._visibleMessages.length; - const newMessages = this._processedMessages.slice(-(len + delta), -len); - // Add newMessages to the beginning of _visibleMessages + const newMessages = this._processedMessages.slice(index, + -this._visibleMessages.length); + // Add newMessages to the beginning of _visibleMessages. this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); - this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE); + // Allow the dom-repeat to stamp. + flush(); + el = this.shadowRoot + .querySelector('[data-message-id="' + messageID + '"]'); } - _processedMessagesChanged(messages) { - if (messages) { - this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES); + el.set('message.expanded', true); + let top = el.offsetTop; + for (let offsetParent = el.offsetParent; + offsetParent; + offsetParent = offsetParent.offsetParent) { + top += offsetParent.offsetTop; + } + window.scrollTo(0, top); + this._highlightEl(el); + } - if (messages.length === 0) return; - const tags = messages.map(message => message.tag || message.type || - (message.comments ? 'comments' : 'none')); - const tagsCounted = tags.reduce((acc, val) => { - acc[val] = (acc[val] || 0) + 1; - return acc; - }, {all: messages.length}); - this.$.reporting.reportInteraction('messages-count', tagsCounted); + _isAutomated(message) { + return !!(message.reviewer || + (message.tag && message.tag.startsWith('autogenerated'))); + } + + _computeItems(messages, reviewerUpdates) { + // Polymer 2: check for undefined + if ([messages, reviewerUpdates].some(arg => arg === undefined)) { + return []; + } + + messages = messages || []; + reviewerUpdates = reviewerUpdates || []; + let mi = 0; + let ri = 0; + let result = []; + let mDate; + let rDate; + for (let i = 0; i < messages.length; i++) { + messages[i]._index = i; + } + + while (mi < messages.length || ri < reviewerUpdates.length) { + if (mi >= messages.length) { + result = result.concat(reviewerUpdates.slice(ri)); + break; + } + if (ri >= reviewerUpdates.length) { + result = result.concat(messages.slice(mi)); + break; + } + mDate = mDate || util.parseDate(messages[mi].date); + rDate = rDate || util.parseDate(reviewerUpdates[ri].date); + if (rDate < mDate) { + result.push(reviewerUpdates[ri++]); + rDate = null; + } else { + result.push(messages[mi++]); + mDate = null; + } + } + return result; + } + + _expandedChanged(exp) { + if (this._processedMessages) { + for (let i = 0; i < this._processedMessages.length; i++) { + this._processedMessages[i].expanded = exp; + } + } + // _visibleMessages is a subarray of _processedMessages + // _processedMessages contains all items from _visibleMessages + // At this point all _visibleMessages.expanded values are set, + // and notifyPath must be used to notify Polymer about changes. + if (this._visibleMessages) { + for (let i = 0; i < this._visibleMessages.length; i++) { + this.notifyPath(`_visibleMessages.${i}.expanded`); } } - _computeNumMessagesText(visibleMessages, messages, - hideAutomated) { - const total = - this._numRemaining(visibleMessages, messages, hideAutomated); - return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages'; - } - - _computeIncrementHidden(visibleMessages, messages, - hideAutomated) { - const total = - this._numRemaining(visibleMessages, messages, hideAutomated); - return total <= this._getDelta(visibleMessages, messages, hideAutomated); - } - - /** - * Compute a mapping from label name to objects representing the minimum and - * maximum possible values for that label. - */ - _computeLabelExtremes(labelRecord) { - const extremes = {}; - const labels = labelRecord.base; - if (!labels) { return extremes; } - for (const key of Object.keys(labels)) { - if (!labels[key] || !labels[key].values) { continue; } - const values = Object.keys(labels[key].values) - .map(v => parseInt(v, 10)); - values.sort((a, b) => a - b); - if (!values.length) { continue; } - extremes[key] = {min: values[0], max: values[values.length - 1]}; - } - return extremes; + if (this._expanded) { + this._expandCollapseTitle = this.createTitle( + this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS); + } else { + this._expandCollapseTitle = this.createTitle( + this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS); } } - customElements.define(GrMessagesList.is, GrMessagesList); -})(); + _highlightEl(el) { + const highlightedEls = + dom(this.root).querySelectorAll('.highlighted'); + for (const highlighedEl of highlightedEls) { + highlighedEl.classList.remove('highlighted'); + } + function handleAnimationEnd() { + el.removeEventListener('animationend', handleAnimationEnd); + el.classList.remove('highlighted'); + } + el.addEventListener('animationend', handleAnimationEnd); + el.classList.add('highlighted'); + } + + /** + * @param {boolean} expand + */ + handleExpandCollapse(expand) { + this._expanded = expand; + } + + _handleExpandCollapseTap(e) { + e.preventDefault(); + this.handleExpandCollapse(!this._expanded); + } + + _handleAnchorClick(e) { + this.scrollToMessage(e.detail.id); + } + + _hasAutomatedMessages(messages) { + if (!messages) { return false; } + for (const message of messages) { + if (this._isAutomated(message)) { + return true; + } + } + return false; + } + + _computeExpandCollapseMessage(expanded) { + return expanded ? 'Collapse all' : 'Expand all'; + } + + /** + * Computes message author's file comments for change's message. + * Method uses this.messages to find next message and relies on messages + * to be sorted by date field descending. + * + * @param {!Object} changeComments changeComment object, which includes + * a method to get all published comments (including robot comments), + * which returns a Hash of arrays of comments, filename as key. + * @param {!Object} message + * @return {!Object} Hash of arrays of comments, filename as key. + */ + _computeCommentsForMessage(changeComments, message) { + if ([changeComments, message].some(arg => arg === undefined)) { + return []; + } + const comments = changeComments.getAllPublishedComments(); + if (message._index === undefined || !comments || !this.messages) { + return []; + } + const messages = this.messages || []; + const index = message._index; + const authorId = message.author && message.author._account_id; + const mDate = util.parseDate(message.date).getTime(); + // NB: Messages array has oldest messages first. + let nextMDate; + if (index > 0) { + for (let i = index - 1; i >= 0; i--) { + if (messages[i] && messages[i].author && + messages[i].author._account_id === authorId) { + nextMDate = util.parseDate(messages[i].date).getTime(); + break; + } + } + } + const msgComments = {}; + for (const file in comments) { + if (!comments.hasOwnProperty(file)) { continue; } + const fileComments = comments[file]; + for (let i = 0; i < fileComments.length; i++) { + if (fileComments[i].author && + fileComments[i].author._account_id !== authorId) { + continue; + } + const cDate = util.parseDate(fileComments[i].updated).getTime(); + if (cDate <= mDate) { + if (nextMDate && cDate <= nextMDate) { + continue; + } + msgComments[file] = msgComments[file] || []; + msgComments[file].push(fileComments[i]); + } + } + } + return msgComments; + } + + /** + * Returns the number of messages to splice to the beginning of + * _visibleMessages. This is the minimum of the total number of messages + * remaining in the list and the number of messages needed to display five + * more visible messages in the list. + */ + _getDelta(visibleMessages, messages, hideAutomated) { + if ([visibleMessages, messages].some(arg => arg === undefined)) { + return 0; + } + + let delta = MESSAGES_INCREMENT; + const msgsRemaining = messages.length - visibleMessages.length; + + if (hideAutomated) { + let counter = 0; + let i; + for (i = msgsRemaining; i > 0 && counter < MESSAGES_INCREMENT; i--) { + if (!this._isAutomated(messages[i - 1])) { counter++; } + } + delta = msgsRemaining - i; + } + return Math.min(msgsRemaining, delta); + } + + /** + * Gets the number of messages that would be visible, but do not currently + * exist in _visibleMessages. + */ + _numRemaining(visibleMessages, messages, hideAutomated) { + if ([visibleMessages, messages].some(arg => arg === undefined)) { + return 0; + } + + if (hideAutomated) { + return this._getHumanMessages(messages).length - + this._getHumanMessages(visibleMessages).length; + } + return messages.length - visibleMessages.length; + } + + _computeIncrementText(visibleMessages, messages, hideAutomated) { + let delta = this._getDelta(visibleMessages, messages, hideAutomated); + delta = Math.min( + this._numRemaining(visibleMessages, messages, hideAutomated), delta); + return 'Show ' + Math.min(MESSAGES_INCREMENT, delta) + ' more'; + } + + _getHumanMessages(messages) { + return messages.filter(msg => !this._isAutomated(msg)); + } + + _computeShowHideTextHidden(visibleMessages, messages, + hideAutomated) { + if ([visibleMessages, messages].some(arg => arg === undefined)) { + return 0; + } + + if (hideAutomated) { + messages = this._getHumanMessages(messages); + visibleMessages = this._getHumanMessages(visibleMessages); + } + return visibleMessages.length >= messages.length; + } + + _handleShowAllTap() { + this._visibleMessages = this._processedMessages; + this.$.reporting.reportInteraction(ReportingEvent.SHOW_ALL); + } + + _handleIncrementShownMessages() { + const delta = this._getDelta(this._visibleMessages, + this._processedMessages, this._hideAutomated); + const len = this._visibleMessages.length; + const newMessages = this._processedMessages.slice(-(len + delta), -len); + // Add newMessages to the beginning of _visibleMessages + this.splice(...['_visibleMessages', 0, 0].concat(newMessages)); + this.$.reporting.reportInteraction(ReportingEvent.SHOW_MORE); + } + + _processedMessagesChanged(messages) { + if (messages) { + this._visibleMessages = messages.slice(-MAX_INITIAL_SHOWN_MESSAGES); + + if (messages.length === 0) return; + const tags = messages.map(message => message.tag || message.type || + (message.comments ? 'comments' : 'none')); + const tagsCounted = tags.reduce((acc, val) => { + acc[val] = (acc[val] || 0) + 1; + return acc; + }, {all: messages.length}); + this.$.reporting.reportInteraction('messages-count', tagsCounted); + } + } + + _computeNumMessagesText(visibleMessages, messages, + hideAutomated) { + const total = + this._numRemaining(visibleMessages, messages, hideAutomated); + return total === 1 ? 'Show 1 message' : 'Show all ' + total + ' messages'; + } + + _computeIncrementHidden(visibleMessages, messages, + hideAutomated) { + const total = + this._numRemaining(visibleMessages, messages, hideAutomated); + return total <= this._getDelta(visibleMessages, messages, hideAutomated); + } + + /** + * Compute a mapping from label name to objects representing the minimum and + * maximum possible values for that label. + */ + _computeLabelExtremes(labelRecord) { + const extremes = {}; + const labels = labelRecord.base; + if (!labels) { return extremes; } + for (const key of Object.keys(labels)) { + if (!labels[key] || !labels[key].values) { continue; } + const values = Object.keys(labels[key].values) + .map(v => parseInt(v, 10)); + values.sort((a, b) => a - b); + if (!values.length) { continue; } + extremes[key] = {min: values[0], max: values[values.length - 1]}; + } + return extremes; + } +} + +customElements.define(GrMessagesList.is, GrMessagesList);
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js index 60ec6b0..3e9f7b5 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../gr-message/gr-message.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-messages-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host, .messageListControls { @@ -75,55 +67,27 @@ } </style> <div class="header"> - <span - id="automatedMessageToggleContainer" - class="container" - hidden$="[[!_hasAutomatedMessages(messages)]]"> - <paper-toggle-button - id="automatedMessageToggle" - checked="{{_hideAutomated}}"></paper-toggle-button>Only comments + <span id="automatedMessageToggleContainer" class="container" hidden\$="[[!_hasAutomatedMessages(messages)]]"> + <paper-toggle-button id="automatedMessageToggle" checked="{{_hideAutomated}}"></paper-toggle-button>Only comments <span class="transparent separator"></span> </span> - <gr-button - id="collapse-messages" - link - title="[[_expandCollapseTitle]]" - on-click="_handleExpandCollapseTap"> + <gr-button id="collapse-messages" link="" title="[[_expandCollapseTitle]]" on-click="_handleExpandCollapseTap"> [[_computeExpandCollapseMessage(_expanded)]] </gr-button> </div> - <span - id="messageControlsContainer" - hidden$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]"> - <gr-button id="oldMessagesBtn" link on-click="_handleShowAllTap"> + <span id="messageControlsContainer" hidden\$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]"> + <gr-button id="oldMessagesBtn" link="" on-click="_handleShowAllTap"> [[_computeNumMessagesText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]] </gr-button> - <span - class="container" - hidden$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]"> + <span class="container" hidden\$="[[_computeIncrementHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]"> <span class="transparent separator"></span> - <gr-button id="incrementMessagesBtn" link - on-click="_handleIncrementShownMessages"> + <gr-button id="incrementMessagesBtn" link="" on-click="_handleIncrementShownMessages"> [[_computeIncrementText(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]] </gr-button> </span> </span> - <template - is="dom-repeat" - items="[[_visibleMessages]]" - as="message"> - <gr-message - change-num="[[changeNum]]" - message="[[message]]" - comments="[[_computeCommentsForMessage(changeComments, message)]]" - hide-automated="[[_hideAutomated]]" - project-name="[[projectName]]" - show-reply-button="[[showReplyButtons]]" - on-message-anchor-tap="_handleAnchorClick" - label-extremes="[[_labelExtremes]]" - data-message-id$="[[message.id]]"></gr-message> + <template is="dom-repeat" items="[[_visibleMessages]]" as="message"> + <gr-message change-num="[[changeNum]]" message="[[message]]" comments="[[_computeCommentsForMessage(changeComments, message)]]" hide-automated="[[_hideAutomated]]" project-name="[[projectName]]" show-reply-button="[[showReplyButtons]]" on-message-anchor-tap="_handleAnchorClick" label-extremes="[[_labelExtremes]]" data-message-id\$="[[message.id]]"></gr-message> </template> <gr-reporting id="reporting" category="message-list"></gr-reporting> - </template> - <script src="gr-messages-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html index 2ee2a81..961abb9 100644 --- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -19,17 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-messages-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../diff/gr-comment-api/gr-comment-api.js"></script> -<link rel="import" href="gr-messages-list.html"> +<script type="module" src="./gr-messages-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../diff/gr-comment-api/gr-comment-api.js'; +import './gr-messages-list.js'; +import '../../diff/gr-comment-api/gr-comment-api-mock_test.js'; +void(0); +</script> <dom-module id="comment-api-mock"> <template> @@ -38,7 +45,7 @@ change-comments="[[_changeComments]]"></gr-messages-list> <gr-comment-api id="commentAPI"></gr-comment-api> </template> - <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script> + <script type="module" src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script> </dom-module> <test-fixture id="basic"> @@ -49,574 +56,579 @@ </template> </test-fixture> -<script> - const randomMessage = function(opt_params) { - const params = opt_params || {}; - const author1 = { - _account_id: 1115495, - name: 'Andrew Bonventre', - email: 'andybons@chromium.org', - }; - return { - id: params.id || Math.random().toString(), - date: params.date || '2016-01-12 20:28:33.038000', - message: params.message || Math.random().toString(), - _revision_number: params._revision_number || 1, - author: params.author || author1, - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../diff/gr-comment-api/gr-comment-api.js'; +import './gr-messages-list.js'; +import '../../diff/gr-comment-api/gr-comment-api-mock_test.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +const randomMessage = function(opt_params) { + const params = opt_params || {}; + const author1 = { + _account_id: 1115495, + name: 'Andrew Bonventre', + email: 'andybons@chromium.org', + }; + return { + id: params.id || Math.random().toString(), + date: params.date || '2016-01-12 20:28:33.038000', + message: params.message || Math.random().toString(), + _revision_number: params._revision_number || 1, + author: params.author || author1, + }; +}; + +const randomAutomated = function(opt_params) { + return Object.assign({tag: 'autogenerated:gerrit:replace'}, + randomMessage(opt_params)); +}; + +suite('gr-messages-list tests', () => { + let element; + let messages; + let sandbox; + let commentApiWrapper; + + const getMessages = function() { + return dom(element.root).querySelectorAll('gr-message'); }; - const randomAutomated = function(opt_params) { - return Object.assign({tag: 'autogenerated:gerrit:replace'}, - randomMessage(opt_params)); + const author = { + _account_id: 42, + name: 'Marvin the Paranoid Android', + email: 'marvin@sirius.org', }; - suite('gr-messages-list tests', async () => { - await readyToTest(); + const comments = { + file1: [ + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: '6505d749_f0bec0aa', + line: 62, + id: '6505d749_10ed44b2', + patch_set: 2, + author: { + email: 'some@email.com', + _account_id: 123, + }, + }, + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: 'c5912363_6b820105', + line: 42, + id: '450a935e_0f1c05db', + patch_set: 2, + author, + }, + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: '6505d749_f0bec0aa', + line: 62, + id: '6505d749_10ed44b2', + patch_set: 2, + author, + }, + ], + file2: [ + { + message: 'message text', + updated: '2016-09-27 00:18:03.000000000', + in_reply_to: 'c5912363_4b7d450a', + line: 132, + id: '450a935e_4f260d25', + patch_set: 2, + author, + }, + ], + }; + + suite('basic tests', () => { + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, + getDiffComments() { return Promise.resolve(comments); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + }); + sandbox = sinon.sandbox.create(); + messages = _.times(3, randomMessage); + // Element must be wrapped in an element with direct access to the + // comment API. + commentApiWrapper = fixture('basic'); + element = commentApiWrapper.$.messagesList; + element.messages = messages; + + // Stub methods on the changeComments object after changeComments has + // been initialized. + return commentApiWrapper.loadComments(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('show some old messages', () => { + assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); + element.messages = _.times(26, randomMessage); + flushAsynchronousOperations(); + + assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); + assert.equal(getMessages().length, 20); + assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase() + .trim(), 'SHOW 5 MORE'); + MockInteractions.tap(element.$.incrementMessagesBtn); + flushAsynchronousOperations(); + + assert.equal(getMessages().length, 25); + assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase() + .trim(), 'SHOW 1 MORE'); + MockInteractions.tap(element.$.incrementMessagesBtn); + flushAsynchronousOperations(); + + assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); + assert.equal(getMessages().length, 26); + }); + + test('show all old messages', () => { + assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); + element.messages = _.times(26, randomMessage); + flushAsynchronousOperations(); + + assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); + assert.equal(getMessages().length, 20); + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW ALL 6 MESSAGES'); + MockInteractions.tap(element.$.oldMessagesBtn); + flushAsynchronousOperations(); + + assert.equal(getMessages().length, 26); + assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); + }); + + test('message count respects automated', () => { + element.messages = _.times(10, randomAutomated) + .concat(_.times(11, randomMessage)); + flushAsynchronousOperations(); + + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW 1 MESSAGE'); + assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); + MockInteractions.tap(element.$.automatedMessageToggle); + flushAsynchronousOperations(); + + assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); + }); + + test('message count still respects non-automated on toggle', () => { + element.messages = _.times(10, randomMessage) + .concat(_.times(11, randomAutomated)); + flushAsynchronousOperations(); + + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW 1 MESSAGE'); + assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); + MockInteractions.tap(element.$.automatedMessageToggle); + flushAsynchronousOperations(); + + assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), + 'SHOW 1 MESSAGE'); + assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); + }); + + test('show all messages respects expand', () => { + element.messages = _.times(10, randomAutomated) + .concat(_.times(11, randomMessage)); + flushAsynchronousOperations(); + + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); // Expand all. + flushAsynchronousOperations(); + + let messages = getMessages(); + assert.equal(messages.length, 20); + for (const message of messages) { + assert.isTrue(message._expanded); + } + + MockInteractions.tap(element.$.oldMessagesBtn); + flushAsynchronousOperations(); + + messages = getMessages(); + assert.equal(messages.length, 21); + for (const message of messages) { + assert.isTrue(message._expanded); + } + }); + + test('show all messages respects collapse', () => { + element.messages = _.times(10, randomAutomated) + .concat(_.times(11, randomMessage)); + flushAsynchronousOperations(); + + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); // Expand all. + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); // Collapse all. + flushAsynchronousOperations(); + + let messages = getMessages(); + assert.equal(messages.length, 20); + for (const message of messages) { + assert.isFalse(message._expanded); + } + + MockInteractions.tap(element.$.oldMessagesBtn); + flushAsynchronousOperations(); + + messages = getMessages(); + assert.equal(messages.length, 21); + for (const message of messages) { + assert.isFalse(message._expanded); + } + }); + + test('expand/collapse all', () => { + let allMessageEls = getMessages(); + for (const message of allMessageEls) { + message._expanded = false; + } + MockInteractions.tap(allMessageEls[1]); + assert.isTrue(allMessageEls[1]._expanded); + + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); + allMessageEls = getMessages(); + for (const message of allMessageEls) { + assert.isTrue(message._expanded); + } + + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); + allMessageEls = getMessages(); + for (const message of allMessageEls) { + assert.isFalse(message._expanded); + } + }); + + test('expand/collapse from external keypress', () => { + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); + let allMessageEls = getMessages(); + for (const message of allMessageEls) { + assert.isTrue(message._expanded); + } + + // Expand/collapse all text also changes. + assert.equal(element.shadowRoot + .querySelector('#collapse-messages').textContent.trim(), + 'Collapse all'); + + MockInteractions.tap(element.shadowRoot + .querySelector('#collapse-messages')); + allMessageEls = getMessages(); + for (const message of allMessageEls) { + assert.isFalse(message._expanded); + } + // Expand/collapse all text also changes. + assert.equal(element.shadowRoot + .querySelector('#collapse-messages').textContent.trim(), + 'Expand all'); + }); + + test('hide messages does not appear when no automated messages', () => { + assert.isOk(element.shadowRoot + .querySelector('#automatedMessageToggleContainer[hidden]')); + }); + + test('scroll to message', () => { + const allMessageEls = getMessages(); + for (const message of allMessageEls) { + message.set('message.expanded', false); + } + + const scrollToStub = sandbox.stub(window, 'scrollTo'); + const highlightStub = sandbox.stub(element, '_highlightEl'); + + element.scrollToMessage('invalid'); + + for (const message of allMessageEls) { + assert.isFalse(message._expanded, + 'expected gr-message to not be expanded'); + } + + const messageID = messages[1].id; + element.scrollToMessage(messageID); + assert.isTrue( + element.shadowRoot + .querySelector('[data-message-id="' + messageID + '"]') + ._expanded); + + assert.isTrue(scrollToStub.calledOnce); + assert.isTrue(highlightStub.calledOnce); + }); + + test('scroll to message offscreen', () => { + const scrollToStub = sandbox.stub(window, 'scrollTo'); + const highlightStub = sandbox.stub(element, '_highlightEl'); + element.messages = _.times(25, randomMessage); + flushAsynchronousOperations(); + assert.isFalse(scrollToStub.called); + assert.isFalse(highlightStub.called); + + const messageID = element.messages[1].id; + element.scrollToMessage(messageID); + assert.isTrue(scrollToStub.calledOnce); + assert.isTrue(highlightStub.calledOnce); + assert.equal(element._visibleMessages.length, 24); + assert.isTrue( + element.shadowRoot + .querySelector('[data-message-id="' + messageID + '"]') + ._expanded); + }); + + test('messages', () => { + const messages = [].concat( + randomMessage(), + { + _index: 5, + _revision_number: 4, + message: 'Uploaded patch set 4.', + date: '2016-09-28 13:36:33.000000000', + author, + id: '8c19ccc949c6d482b061be6a28e10782abf0e7af', + }, + { + _index: 6, + _revision_number: 4, + message: 'Patch Set 4:\n\n(6 comments)', + date: '2016-09-28 13:36:33.000000000', + author, + id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5', + } + ); + element.messages = messages; + const isAuthor = function(author, message) { + return message.author._account_id === author._account_id; + }; + const isMarvin = isAuthor.bind(null, author); + flushAsynchronousOperations(); + const messageElements = getMessages(); + assert.equal(messageElements.length, messages.length); + assert.deepEqual(messageElements[1].message, messages[1]); + assert.deepEqual(messageElements[2].message, messages[2]); + assert.deepEqual(messageElements[1].comments.file1, + comments.file1.filter(isMarvin)); + assert.deepEqual(messageElements[1].comments.file2, + comments.file2.filter(isMarvin)); + assert.deepEqual(messageElements[2].comments, {}); + }); + + test('messages without author do not throw', () => { + const messages = [{ + _index: 5, + _revision_number: 4, + message: 'Uploaded patch set 4.', + date: '2016-09-28 13:36:33.000000000', + id: '8c19ccc949c6d482b061be6a28e10782abf0e7af', + }]; + element.messages = messages; + flushAsynchronousOperations(); + const messageEls = getMessages(); + assert.equal(messageEls.length, 1); + assert.equal(messageEls[0].message.message, messages[0].message); + }); + + test('hide increment text if increment >= total remaining', () => { + // Test with stubbed return values, as _numRemaining and _getDelta have + // their own tests. + sandbox.stub(element, '_getDelta').returns(5); + const remainingStub = sandbox.stub(element, '_numRemaining').returns(6); + assert.isFalse(element._computeIncrementHidden(null, null, null)); + remainingStub.restore(); + + sandbox.stub(element, '_numRemaining').returns(4); + assert.isTrue(element._computeIncrementHidden(null, null, null)); + }); + }); + + suite('gr-messages-list automate tests', () => { let element; let messages; let sandbox; let commentApiWrapper; const getMessages = function() { - return Polymer.dom(element.root).querySelectorAll('gr-message'); + return dom(element.root).querySelectorAll('gr-message'); + }; + const getHiddenMessages = function() { + return dom(element.root).querySelectorAll('gr-message[hidden]'); }; - const author = { - _account_id: 42, - name: 'Marvin the Paranoid Android', - email: 'marvin@sirius.org', + const randomMessageReviewer = { + reviewer: {}, + date: '2016-01-13 20:30:33.038000', }; - const comments = { - file1: [ - { - message: 'message text', - updated: '2016-09-27 00:18:03.000000000', - in_reply_to: '6505d749_f0bec0aa', - line: 62, - id: '6505d749_10ed44b2', - patch_set: 2, - author: { - email: 'some@email.com', - _account_id: 123, - }, - }, - { - message: 'message text', - updated: '2016-09-27 00:18:03.000000000', - in_reply_to: 'c5912363_6b820105', - line: 42, - id: '450a935e_0f1c05db', - patch_set: 2, - author, - }, - { - message: 'message text', - updated: '2016-09-27 00:18:03.000000000', - in_reply_to: '6505d749_f0bec0aa', - line: 62, - id: '6505d749_10ed44b2', - patch_set: 2, - author, - }, - ], - file2: [ - { - message: 'message text', - updated: '2016-09-27 00:18:03.000000000', - in_reply_to: 'c5912363_4b7d450a', - line: 132, - id: '450a935e_4f260d25', - patch_set: 2, - author, - }, - ], - }; - - suite('basic tests', () => { - setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getLoggedIn() { return Promise.resolve(false); }, - getDiffComments() { return Promise.resolve(comments); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - }); - sandbox = sinon.sandbox.create(); - messages = _.times(3, randomMessage); - // Element must be wrapped in an element with direct access to the - // comment API. - commentApiWrapper = fixture('basic'); - element = commentApiWrapper.$.messagesList; - element.messages = messages; - - // Stub methods on the changeComments object after changeComments has - // been initialized. - return commentApiWrapper.loadComments(); + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, }); - teardown(() => { - sandbox.restore(); - }); + sandbox = sinon.sandbox.create(); + messages = _.times(2, randomAutomated); + messages.push(randomMessageReviewer); - test('show some old messages', () => { - assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); - element.messages = _.times(26, randomMessage); - flushAsynchronousOperations(); + // Element must be wrapped in an element with direct access to the + // comment API. + commentApiWrapper = fixture('basic'); + element = commentApiWrapper.$.messagesList; + sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll'); + element.messages = messages; - assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); - assert.equal(getMessages().length, 20); - assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase() - .trim(), 'SHOW 5 MORE'); - MockInteractions.tap(element.$.incrementMessagesBtn); - flushAsynchronousOperations(); - - assert.equal(getMessages().length, 25); - assert.equal(element.$.incrementMessagesBtn.innerText.toUpperCase() - .trim(), 'SHOW 1 MORE'); - MockInteractions.tap(element.$.incrementMessagesBtn); - flushAsynchronousOperations(); - - assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); - assert.equal(getMessages().length, 26); - }); - - test('show all old messages', () => { - assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); - element.messages = _.times(26, randomMessage); - flushAsynchronousOperations(); - - assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); - assert.equal(getMessages().length, 20); - assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), - 'SHOW ALL 6 MESSAGES'); - MockInteractions.tap(element.$.oldMessagesBtn); - flushAsynchronousOperations(); - - assert.equal(getMessages().length, 26); - assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); - }); - - test('message count respects automated', () => { - element.messages = _.times(10, randomAutomated) - .concat(_.times(11, randomMessage)); - flushAsynchronousOperations(); - - assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), - 'SHOW 1 MESSAGE'); - assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); - MockInteractions.tap(element.$.automatedMessageToggle); - flushAsynchronousOperations(); - - assert.isTrue(element.$.messageControlsContainer.hasAttribute('hidden')); - }); - - test('message count still respects non-automated on toggle', () => { - element.messages = _.times(10, randomMessage) - .concat(_.times(11, randomAutomated)); - flushAsynchronousOperations(); - - assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), - 'SHOW 1 MESSAGE'); - assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); - MockInteractions.tap(element.$.automatedMessageToggle); - flushAsynchronousOperations(); - - assert.equal(element.$.oldMessagesBtn.innerText.toUpperCase(), - 'SHOW 1 MESSAGE'); - assert.isFalse(element.$.messageControlsContainer.hasAttribute('hidden')); - }); - - test('show all messages respects expand', () => { - element.messages = _.times(10, randomAutomated) - .concat(_.times(11, randomMessage)); - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); // Expand all. - flushAsynchronousOperations(); - - let messages = getMessages(); - assert.equal(messages.length, 20); - for (const message of messages) { - assert.isTrue(message._expanded); - } - - MockInteractions.tap(element.$.oldMessagesBtn); - flushAsynchronousOperations(); - - messages = getMessages(); - assert.equal(messages.length, 21); - for (const message of messages) { - assert.isTrue(message._expanded); - } - }); - - test('show all messages respects collapse', () => { - element.messages = _.times(10, randomAutomated) - .concat(_.times(11, randomMessage)); - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); // Expand all. - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); // Collapse all. - flushAsynchronousOperations(); - - let messages = getMessages(); - assert.equal(messages.length, 20); - for (const message of messages) { - assert.isFalse(message._expanded); - } - - MockInteractions.tap(element.$.oldMessagesBtn); - flushAsynchronousOperations(); - - messages = getMessages(); - assert.equal(messages.length, 21); - for (const message of messages) { - assert.isFalse(message._expanded); - } - }); - - test('expand/collapse all', () => { - let allMessageEls = getMessages(); - for (const message of allMessageEls) { - message._expanded = false; - } - MockInteractions.tap(allMessageEls[1]); - assert.isTrue(allMessageEls[1]._expanded); - - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); - allMessageEls = getMessages(); - for (const message of allMessageEls) { - assert.isTrue(message._expanded); - } - - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); - allMessageEls = getMessages(); - for (const message of allMessageEls) { - assert.isFalse(message._expanded); - } - }); - - test('expand/collapse from external keypress', () => { - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); - let allMessageEls = getMessages(); - for (const message of allMessageEls) { - assert.isTrue(message._expanded); - } - - // Expand/collapse all text also changes. - assert.equal(element.shadowRoot - .querySelector('#collapse-messages').textContent.trim(), - 'Collapse all'); - - MockInteractions.tap(element.shadowRoot - .querySelector('#collapse-messages')); - allMessageEls = getMessages(); - for (const message of allMessageEls) { - assert.isFalse(message._expanded); - } - // Expand/collapse all text also changes. - assert.equal(element.shadowRoot - .querySelector('#collapse-messages').textContent.trim(), - 'Expand all'); - }); - - test('hide messages does not appear when no automated messages', () => { - assert.isOk(element.shadowRoot - .querySelector('#automatedMessageToggleContainer[hidden]')); - }); - - test('scroll to message', () => { - const allMessageEls = getMessages(); - for (const message of allMessageEls) { - message.set('message.expanded', false); - } - - const scrollToStub = sandbox.stub(window, 'scrollTo'); - const highlightStub = sandbox.stub(element, '_highlightEl'); - - element.scrollToMessage('invalid'); - - for (const message of allMessageEls) { - assert.isFalse(message._expanded, - 'expected gr-message to not be expanded'); - } - - const messageID = messages[1].id; - element.scrollToMessage(messageID); - assert.isTrue( - element.shadowRoot - .querySelector('[data-message-id="' + messageID + '"]') - ._expanded); - - assert.isTrue(scrollToStub.calledOnce); - assert.isTrue(highlightStub.calledOnce); - }); - - test('scroll to message offscreen', () => { - const scrollToStub = sandbox.stub(window, 'scrollTo'); - const highlightStub = sandbox.stub(element, '_highlightEl'); - element.messages = _.times(25, randomMessage); - flushAsynchronousOperations(); - assert.isFalse(scrollToStub.called); - assert.isFalse(highlightStub.called); - - const messageID = element.messages[1].id; - element.scrollToMessage(messageID); - assert.isTrue(scrollToStub.calledOnce); - assert.isTrue(highlightStub.calledOnce); - assert.equal(element._visibleMessages.length, 24); - assert.isTrue( - element.shadowRoot - .querySelector('[data-message-id="' + messageID + '"]') - ._expanded); - }); - - test('messages', () => { - const messages = [].concat( - randomMessage(), - { - _index: 5, - _revision_number: 4, - message: 'Uploaded patch set 4.', - date: '2016-09-28 13:36:33.000000000', - author, - id: '8c19ccc949c6d482b061be6a28e10782abf0e7af', - }, - { - _index: 6, - _revision_number: 4, - message: 'Patch Set 4:\n\n(6 comments)', - date: '2016-09-28 13:36:33.000000000', - author, - id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5', - } - ); - element.messages = messages; - const isAuthor = function(author, message) { - return message.author._account_id === author._account_id; - }; - const isMarvin = isAuthor.bind(null, author); - flushAsynchronousOperations(); - const messageElements = getMessages(); - assert.equal(messageElements.length, messages.length); - assert.deepEqual(messageElements[1].message, messages[1]); - assert.deepEqual(messageElements[2].message, messages[2]); - assert.deepEqual(messageElements[1].comments.file1, - comments.file1.filter(isMarvin)); - assert.deepEqual(messageElements[1].comments.file2, - comments.file2.filter(isMarvin)); - assert.deepEqual(messageElements[2].comments, {}); - }); - - test('messages without author do not throw', () => { - const messages = [{ - _index: 5, - _revision_number: 4, - message: 'Uploaded patch set 4.', - date: '2016-09-28 13:36:33.000000000', - id: '8c19ccc949c6d482b061be6a28e10782abf0e7af', - }]; - element.messages = messages; - flushAsynchronousOperations(); - const messageEls = getMessages(); - assert.equal(messageEls.length, 1); - assert.equal(messageEls[0].message.message, messages[0].message); - }); - - test('hide increment text if increment >= total remaining', () => { - // Test with stubbed return values, as _numRemaining and _getDelta have - // their own tests. - sandbox.stub(element, '_getDelta').returns(5); - const remainingStub = sandbox.stub(element, '_numRemaining').returns(6); - assert.isFalse(element._computeIncrementHidden(null, null, null)); - remainingStub.restore(); - - sandbox.stub(element, '_numRemaining').returns(4); - assert.isTrue(element._computeIncrementHidden(null, null, null)); - }); + // Stub methods on the changeComments object after changeComments has + // been initialized. + return commentApiWrapper.loadComments(); }); - suite('gr-messages-list automate tests', () => { - let element; - let messages; - let sandbox; - let commentApiWrapper; + teardown(() => { + sandbox.restore(); + }); - const getMessages = function() { - return Polymer.dom(element.root).querySelectorAll('gr-message'); - }; - const getHiddenMessages = function() { - return Polymer.dom(element.root).querySelectorAll('gr-message[hidden]'); - }; + test('hide autogenerated button is not hidden', () => { + assert.isNotOk(element.shadowRoot + .querySelector('#automatedMessageToggle[hidden]')); + }); - const randomMessageReviewer = { - reviewer: {}, - date: '2016-01-13 20:30:33.038000', - }; + test('autogenerated messages are not hidden initially', () => { + const allHiddenMessageEls = getHiddenMessages(); - setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getLoggedIn() { return Promise.resolve(false); }, - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, + // There are no hidden messages. + assert.isFalse(!!allHiddenMessageEls.length); + }); + + test('autogenerated messages hidden after comments only toggle', () => { + let allHiddenMessageEls = getHiddenMessages(); + + element._hideAutomated = false; + MockInteractions.tap(element.$.automatedMessageToggle); + flushAsynchronousOperations(); + const allMessageEls = getMessages(); + allHiddenMessageEls = getHiddenMessages(); + + // Autogenerated messages are now hidden. + assert.equal(allHiddenMessageEls.length, allMessageEls.length); + }); + + test('autogenerated messages not hidden after comments only toggle', + () => { + let allHiddenMessageEls = getHiddenMessages(); + + element._hideAutomated = true; + MockInteractions.tap(element.$.automatedMessageToggle); + allHiddenMessageEls = getHiddenMessages(); + + // Autogenerated messages are now hidden. + assert.isFalse(!!allHiddenMessageEls.length); }); - sandbox = sinon.sandbox.create(); - messages = _.times(2, randomAutomated); - messages.push(randomMessageReviewer); + test('_getDelta', () => { + let messages = [randomMessage()]; + assert.equal(element._getDelta([], messages, false), 1); + assert.equal(element._getDelta([], messages, true), 1); - // Element must be wrapped in an element with direct access to the - // comment API. - commentApiWrapper = fixture('basic'); - element = commentApiWrapper.$.messagesList; - sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll'); - element.messages = messages; + messages = _.times(7, randomMessage); + assert.equal(element._getDelta([], messages, false), 5); + assert.equal(element._getDelta([], messages, true), 5); - // Stub methods on the changeComments object after changeComments has - // been initialized. - return commentApiWrapper.loadComments(); - }); + messages = _.times(4, randomMessage) + .concat(_.times(2, randomAutomated)) + .concat(_.times(3, randomMessage)); - teardown(() => { - sandbox.restore(); - }); + const dummyArr = _.times(2, randomMessage); + assert.equal(element._getDelta(dummyArr, messages, false), 5); + assert.equal(element._getDelta(dummyArr, messages, true), 7); + }); - test('hide autogenerated button is not hidden', () => { - assert.isNotOk(element.shadowRoot - .querySelector('#automatedMessageToggle[hidden]')); - }); + test('_getHumanMessages', () => { + assert.equal( + element._getHumanMessages(_.times(5, randomAutomated)).length, 0); + assert.equal( + element._getHumanMessages(_.times(5, randomMessage)).length, 5); - test('autogenerated messages are not hidden initially', () => { - const allHiddenMessageEls = getHiddenMessages(); + let messages = _.shuffle(_.times(5, randomMessage) + .concat(_.times(5, randomAutomated))); + messages = element._getHumanMessages(messages); + assert.equal(messages.length, 5); + assert.isFalse(element._hasAutomatedMessages(messages)); + }); - // There are no hidden messages. - assert.isFalse(!!allHiddenMessageEls.length); - }); - - test('autogenerated messages hidden after comments only toggle', () => { - let allHiddenMessageEls = getHiddenMessages(); - - element._hideAutomated = false; - MockInteractions.tap(element.$.automatedMessageToggle); - flushAsynchronousOperations(); - const allMessageEls = getMessages(); - allHiddenMessageEls = getHiddenMessages(); - - // Autogenerated messages are now hidden. - assert.equal(allHiddenMessageEls.length, allMessageEls.length); - }); - - test('autogenerated messages not hidden after comments only toggle', - () => { - let allHiddenMessageEls = getHiddenMessages(); - - element._hideAutomated = true; - MockInteractions.tap(element.$.automatedMessageToggle); - allHiddenMessageEls = getHiddenMessages(); - - // Autogenerated messages are now hidden. - assert.isFalse(!!allHiddenMessageEls.length); + test('initially show only 20 messages', () => { + sandbox.stub(element.$.reporting, 'reportInteraction', + (eventName, details) => { + assert.equal(typeof(eventName), 'string'); + if (details) { + assert.equal(typeof(details), 'object'); + } }); + const messages = Array.from(Array(23).keys()) + .map(() => { + return {}; + }); + element._processedMessagesChanged(messages); - test('_getDelta', () => { - let messages = [randomMessage()]; - assert.equal(element._getDelta([], messages, false), 1); - assert.equal(element._getDelta([], messages, true), 1); + assert.equal(element._visibleMessages.length, 20); + }); - messages = _.times(7, randomMessage); - assert.equal(element._getDelta([], messages, false), 5); - assert.equal(element._getDelta([], messages, true), 5); + test('_computeLabelExtremes', () => { + const computeSpy = sandbox.spy(element, '_computeLabelExtremes'); - messages = _.times(4, randomMessage) - .concat(_.times(2, randomAutomated)) - .concat(_.times(3, randomMessage)); + element.labels = null; + assert.isTrue(computeSpy.calledOnce); + assert.deepEqual(computeSpy.lastCall.returnValue, {}); - const dummyArr = _.times(2, randomMessage); - assert.equal(element._getDelta(dummyArr, messages, false), 5); - assert.equal(element._getDelta(dummyArr, messages, true), 7); - }); + element.labels = {}; + assert.isTrue(computeSpy.calledTwice); + assert.deepEqual(computeSpy.lastCall.returnValue, {}); - test('_getHumanMessages', () => { - assert.equal( - element._getHumanMessages(_.times(5, randomAutomated)).length, 0); - assert.equal( - element._getHumanMessages(_.times(5, randomMessage)).length, 5); + element.labels = {'my-label': {}}; + assert.isTrue(computeSpy.calledThrice); + assert.deepEqual(computeSpy.lastCall.returnValue, {}); - let messages = _.shuffle(_.times(5, randomMessage) - .concat(_.times(5, randomAutomated))); - messages = element._getHumanMessages(messages); - assert.equal(messages.length, 5); - assert.isFalse(element._hasAutomatedMessages(messages)); - }); + element.labels = {'my-label': {values: {}}}; + assert.equal(computeSpy.callCount, 4); + assert.deepEqual(computeSpy.lastCall.returnValue, {}); - test('initially show only 20 messages', () => { - sandbox.stub(element.$.reporting, 'reportInteraction', - (eventName, details) => { - assert.equal(typeof(eventName), 'string'); - if (details) { - assert.equal(typeof(details), 'object'); - } - }); - const messages = Array.from(Array(23).keys()) - .map(() => { - return {}; - }); - element._processedMessagesChanged(messages); + element.labels = {'my-label': {values: {'-12': {}}}}; + assert.equal(computeSpy.callCount, 5); + assert.deepEqual(computeSpy.lastCall.returnValue, + {'my-label': {min: -12, max: -12}}); - assert.equal(element._visibleMessages.length, 20); - }); + element.labels = { + 'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}}, + }; + assert.equal(computeSpy.callCount, 6); + assert.deepEqual(computeSpy.lastCall.returnValue, + {'my-label': {min: -2, max: 2}}); - test('_computeLabelExtremes', () => { - const computeSpy = sandbox.spy(element, '_computeLabelExtremes'); - - element.labels = null; - assert.isTrue(computeSpy.calledOnce); - assert.deepEqual(computeSpy.lastCall.returnValue, {}); - - element.labels = {}; - assert.isTrue(computeSpy.calledTwice); - assert.deepEqual(computeSpy.lastCall.returnValue, {}); - - element.labels = {'my-label': {}}; - assert.isTrue(computeSpy.calledThrice); - assert.deepEqual(computeSpy.lastCall.returnValue, {}); - - element.labels = {'my-label': {values: {}}}; - assert.equal(computeSpy.callCount, 4); - assert.deepEqual(computeSpy.lastCall.returnValue, {}); - - element.labels = {'my-label': {values: {'-12': {}}}}; - assert.equal(computeSpy.callCount, 5); - assert.deepEqual(computeSpy.lastCall.returnValue, - {'my-label': {min: -12, max: -12}}); - - element.labels = { - 'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}}, - }; - assert.equal(computeSpy.callCount, 6); - assert.deepEqual(computeSpy.lastCall.returnValue, - {'my-label': {min: -2, max: 2}}); - - element.labels = { - 'my-label': {values: {'-12': {}}}, - 'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}}, - }; - assert.equal(computeSpy.callCount, 7); - assert.deepEqual(computeSpy.lastCall.returnValue, { - 'my-label': {min: -12, max: -12}, - 'other-label': {min: -1, max: 1}, - }); + element.labels = { + 'my-label': {values: {'-12': {}}}, + 'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}}, + }; + assert.equal(computeSpy.callCount, 7); + assert.deepEqual(computeSpy.lastCall.returnValue, { + 'my-label': {min: -12, max: -12}, + 'other-label': {min: -1, max: 1}, }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js index d4a2398..c4af481 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list.js
@@ -14,384 +14,397 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-related-changes-list_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrRelatedChangesList extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-related-changes-list'; } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when a new section is loaded so that the change view can determine + * a show more button is needed, sometimes before all the sections finish + * loading. + * + * @event new-section-loaded */ - class GrRelatedChangesList extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-related-changes-list'; } - /** - * Fired when a new section is loaded so that the change view can determine - * a show more button is needed, sometimes before all the sections finish - * loading. - * - * @event new-section-loaded - */ - static get properties() { - return { - change: Object, - hasParent: { - type: Boolean, - notify: true, - value: false, - }, - patchNum: String, - parentChange: Object, - hidden: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - loading: { - type: Boolean, - notify: true, - }, - mergeable: Boolean, - _connectedRevisions: { - type: Array, - computed: '_computeConnectedRevisions(change, patchNum, ' + - '_relatedResponse.changes)', - }, - /** @type {?} */ - _relatedResponse: { - type: Object, - value() { return {changes: []}; }, - }, - /** @type {?} */ - _submittedTogether: { - type: Object, - value() { return {changes: []}; }, - }, - _conflicts: { - type: Array, - value() { return []; }, - }, - _cherryPicks: { - type: Array, - value() { return []; }, - }, - _sameTopic: { - type: Array, - value() { return []; }, - }, - }; - } - - static get observers() { - return [ - '_resultsChanged(_relatedResponse, _submittedTogether, ' + - '_conflicts, _cherryPicks, _sameTopic)', - ]; - } - - clear() { - this.loading = true; - this.hidden = true; - - this._relatedResponse = {changes: []}; - this._submittedTogether = {changes: []}; - this._conflicts = []; - this._cherryPicks = []; - this._sameTopic = []; - } - - reload() { - if (!this.change || !this.patchNum) { - return Promise.resolve(); - } - this.loading = true; - const promises = [ - this._getRelatedChanges().then(response => { - this._relatedResponse = response; - this._fireReloadEvent(); - this.hasParent = this._calculateHasParent(this.change.change_id, - response.changes); - }), - this._getSubmittedTogether().then(response => { - this._submittedTogether = response; - this._fireReloadEvent(); - }), - this._getCherryPicks().then(response => { - this._cherryPicks = response; - this._fireReloadEvent(); - }), - ]; - - // Get conflicts if change is open and is mergeable. - if (this.changeIsOpen(this.change) && this.mergeable) { - promises.push(this._getConflicts().then(response => { - // Because the server doesn't always return a response and the - // template expects an array, always return an array. - this._conflicts = response ? response : []; - this._fireReloadEvent(); - })); - } - - promises.push(this._getServerConfig().then(config => { - if (this.change.topic && !config.change.submit_whole_topic) { - return this._getChangesWithSameTopic().then(response => { - this._sameTopic = response; - }); - } else { - this._sameTopic = []; - } - return this._sameTopic; - })); - - return Promise.all(promises).then(() => { - this.loading = false; - }); - } - - _fireReloadEvent() { - // The listener on the change computes height of the related changes - // section, so they have to be rendered first, and inside a dom-repeat, - // that requires a flush. - Polymer.dom.flush(); - this.dispatchEvent(new CustomEvent('new-section-loaded')); - } - - /** - * Determines whether or not the given change has a parent change. If there - * is a relation chain, and the change id is not the last item of the - * relation chain, there is a parent. - * - * @param {number} currentChangeId - * @param {!Array} relatedChanges - * @return {boolean} - */ - _calculateHasParent(currentChangeId, relatedChanges) { - return relatedChanges.length > 0 && - relatedChanges[relatedChanges.length - 1].change_id !== - currentChangeId; - } - - _getRelatedChanges() { - return this.$.restAPI.getRelatedChanges(this.change._number, - this.patchNum); - } - - _getSubmittedTogether() { - return this.$.restAPI.getChangesSubmittedTogether(this.change._number); - } - - _getServerConfig() { - return this.$.restAPI.getConfig(); - } - - _getConflicts() { - return this.$.restAPI.getChangeConflicts(this.change._number); - } - - _getCherryPicks() { - return this.$.restAPI.getChangeCherryPicks(this.change.project, - this.change.change_id, this.change._number); - } - - _getChangesWithSameTopic() { - return this.$.restAPI.getChangesWithSameTopic(this.change.topic, - this.change._number); - } - - /** - * @param {number} changeNum - * @param {string} project - * @param {number=} opt_patchNum - * @return {string} - */ - _computeChangeURL(changeNum, project, opt_patchNum) { - return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum); - } - - _computeChangeContainerClass(currentChange, relatedChange) { - const classes = ['changeContainer']; - if ([relatedChange, currentChange].some(arg => arg === undefined)) { - return classes; - } - if (this._changesEqual(relatedChange, currentChange)) { - classes.push('thisChange'); - } - return classes.join(' '); - } - - /** - * Do the given objects describe the same change? Compares the changes by - * their numbers. - * - * @see /Documentation/rest-api-changes.html#change-info - * @see /Documentation/rest-api-changes.html#related-change-and-commit-info - * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo - * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo - * @return {boolean} - */ - _changesEqual(a, b) { - const aNum = this._getChangeNumber(a); - const bNum = this._getChangeNumber(b); - return aNum === bNum; - } - - /** - * Get the change number from either a ChangeInfo (such as those included in - * SubmittedTogetherInfo responses) or get the change number from a - * RelatedChangeAndCommitInfo (such as those included in a - * RelatedChangesInfo response). - * - * @see /Documentation/rest-api-changes.html#change-info - * @see /Documentation/rest-api-changes.html#related-change-and-commit-info - * - * @param {!Object} change Either a ChangeInfo or a - * RelatedChangeAndCommitInfo object. - * @return {number} - */ - _getChangeNumber(change) { - // Default to 0 if change property is not defined. - if (!change) return 0; - - if (change.hasOwnProperty('_change_number')) { - return change._change_number; - } - return change._number; - } - - _computeLinkClass(change) { - const statuses = []; - if (change.status == this.ChangeStatus.ABANDONED) { - statuses.push('strikethrough'); - } - if (change.submittable) { - statuses.push('submittable'); - } - return statuses.join(' '); - } - - _computeChangeStatusClass(change) { - const classes = ['status']; - if (change._revision_number != change._current_revision_number) { - classes.push('notCurrent'); - } else if (this._isIndirectAncestor(change)) { - classes.push('indirectAncestor'); - } else if (change.submittable) { - classes.push('submittable'); - } else if (change.status == this.ChangeStatus.NEW) { - classes.push('hidden'); - } - return classes.join(' '); - } - - _computeChangeStatus(change) { - switch (change.status) { - case this.ChangeStatus.MERGED: - return 'Merged'; - case this.ChangeStatus.ABANDONED: - return 'Abandoned'; - } - if (change._revision_number != change._current_revision_number) { - return 'Not current'; - } else if (this._isIndirectAncestor(change)) { - return 'Indirect ancestor'; - } else if (change.submittable) { - return 'Submittable'; - } - return ''; - } - - _resultsChanged(related, submittedTogether, conflicts, - cherryPicks, sameTopic) { - // Polymer 2: check for undefined - if ([ - related, - submittedTogether, - conflicts, - cherryPicks, - sameTopic, - ].some(arg => arg === undefined)) { - return; - } - - const results = [ - related && related.changes, - submittedTogether && submittedTogether.changes, - conflicts, - cherryPicks, - sameTopic, - ]; - for (let i = 0; i < results.length; i++) { - if (results[i] && results[i].length > 0) { - this.hidden = false; - this.fire('update', null, {bubbles: false}); - return; - } - } - this.hidden = true; - } - - _isIndirectAncestor(change) { - return !this._connectedRevisions.includes(change.commit.commit); - } - - _computeConnectedRevisions(change, patchNum, relatedChanges) { - // Polymer 2: check for undefined - if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) { - return undefined; - } - - const connected = []; - let changeRevision; - if (!change) { return []; } - for (const rev in change.revisions) { - if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) { - changeRevision = rev; - } - } - const commits = relatedChanges.map(c => c.commit); - let pos = commits.length - 1; - - while (pos >= 0) { - const commit = commits[pos].commit; - connected.push(commit); - if (commit == changeRevision) { - break; - } - pos--; - } - while (pos >= 0) { - for (let i = 0; i < commits[pos].parents.length; i++) { - if (connected.includes(commits[pos].parents[i].commit)) { - connected.push(commits[pos].commit); - break; - } - } - --pos; - } - return connected; - } - - _computeSubmittedTogetherClass(submittedTogether) { - if (!submittedTogether || ( - submittedTogether.changes.length === 0 && - !submittedTogether.non_visible_changes)) { - return 'hidden'; - } - return ''; - } - - _computeNonVisibleChangesNote(n) { - const noun = n === 1 ? 'change' : 'changes'; - return `(+ ${n} non-visible ${noun})`; - } + static get properties() { + return { + change: Object, + hasParent: { + type: Boolean, + notify: true, + value: false, + }, + patchNum: String, + parentChange: Object, + hidden: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + loading: { + type: Boolean, + notify: true, + }, + mergeable: Boolean, + _connectedRevisions: { + type: Array, + computed: '_computeConnectedRevisions(change, patchNum, ' + + '_relatedResponse.changes)', + }, + /** @type {?} */ + _relatedResponse: { + type: Object, + value() { return {changes: []}; }, + }, + /** @type {?} */ + _submittedTogether: { + type: Object, + value() { return {changes: []}; }, + }, + _conflicts: { + type: Array, + value() { return []; }, + }, + _cherryPicks: { + type: Array, + value() { return []; }, + }, + _sameTopic: { + type: Array, + value() { return []; }, + }, + }; } - customElements.define(GrRelatedChangesList.is, GrRelatedChangesList); -})(); + static get observers() { + return [ + '_resultsChanged(_relatedResponse, _submittedTogether, ' + + '_conflicts, _cherryPicks, _sameTopic)', + ]; + } + + clear() { + this.loading = true; + this.hidden = true; + + this._relatedResponse = {changes: []}; + this._submittedTogether = {changes: []}; + this._conflicts = []; + this._cherryPicks = []; + this._sameTopic = []; + } + + reload() { + if (!this.change || !this.patchNum) { + return Promise.resolve(); + } + this.loading = true; + const promises = [ + this._getRelatedChanges().then(response => { + this._relatedResponse = response; + this._fireReloadEvent(); + this.hasParent = this._calculateHasParent(this.change.change_id, + response.changes); + }), + this._getSubmittedTogether().then(response => { + this._submittedTogether = response; + this._fireReloadEvent(); + }), + this._getCherryPicks().then(response => { + this._cherryPicks = response; + this._fireReloadEvent(); + }), + ]; + + // Get conflicts if change is open and is mergeable. + if (this.changeIsOpen(this.change) && this.mergeable) { + promises.push(this._getConflicts().then(response => { + // Because the server doesn't always return a response and the + // template expects an array, always return an array. + this._conflicts = response ? response : []; + this._fireReloadEvent(); + })); + } + + promises.push(this._getServerConfig().then(config => { + if (this.change.topic && !config.change.submit_whole_topic) { + return this._getChangesWithSameTopic().then(response => { + this._sameTopic = response; + }); + } else { + this._sameTopic = []; + } + return this._sameTopic; + })); + + return Promise.all(promises).then(() => { + this.loading = false; + }); + } + + _fireReloadEvent() { + // The listener on the change computes height of the related changes + // section, so they have to be rendered first, and inside a dom-repeat, + // that requires a flush. + flush(); + this.dispatchEvent(new CustomEvent('new-section-loaded')); + } + + /** + * Determines whether or not the given change has a parent change. If there + * is a relation chain, and the change id is not the last item of the + * relation chain, there is a parent. + * + * @param {number} currentChangeId + * @param {!Array} relatedChanges + * @return {boolean} + */ + _calculateHasParent(currentChangeId, relatedChanges) { + return relatedChanges.length > 0 && + relatedChanges[relatedChanges.length - 1].change_id !== + currentChangeId; + } + + _getRelatedChanges() { + return this.$.restAPI.getRelatedChanges(this.change._number, + this.patchNum); + } + + _getSubmittedTogether() { + return this.$.restAPI.getChangesSubmittedTogether(this.change._number); + } + + _getServerConfig() { + return this.$.restAPI.getConfig(); + } + + _getConflicts() { + return this.$.restAPI.getChangeConflicts(this.change._number); + } + + _getCherryPicks() { + return this.$.restAPI.getChangeCherryPicks(this.change.project, + this.change.change_id, this.change._number); + } + + _getChangesWithSameTopic() { + return this.$.restAPI.getChangesWithSameTopic(this.change.topic, + this.change._number); + } + + /** + * @param {number} changeNum + * @param {string} project + * @param {number=} opt_patchNum + * @return {string} + */ + _computeChangeURL(changeNum, project, opt_patchNum) { + return Gerrit.Nav.getUrlForChangeById(changeNum, project, opt_patchNum); + } + + _computeChangeContainerClass(currentChange, relatedChange) { + const classes = ['changeContainer']; + if ([relatedChange, currentChange].some(arg => arg === undefined)) { + return classes; + } + if (this._changesEqual(relatedChange, currentChange)) { + classes.push('thisChange'); + } + return classes.join(' '); + } + + /** + * Do the given objects describe the same change? Compares the changes by + * their numbers. + * + * @see /Documentation/rest-api-changes.html#change-info + * @see /Documentation/rest-api-changes.html#related-change-and-commit-info + * @param {!Object} a Either ChangeInfo or RelatedChangeAndCommitInfo + * @param {!Object} b Either ChangeInfo or RelatedChangeAndCommitInfo + * @return {boolean} + */ + _changesEqual(a, b) { + const aNum = this._getChangeNumber(a); + const bNum = this._getChangeNumber(b); + return aNum === bNum; + } + + /** + * Get the change number from either a ChangeInfo (such as those included in + * SubmittedTogetherInfo responses) or get the change number from a + * RelatedChangeAndCommitInfo (such as those included in a + * RelatedChangesInfo response). + * + * @see /Documentation/rest-api-changes.html#change-info + * @see /Documentation/rest-api-changes.html#related-change-and-commit-info + * + * @param {!Object} change Either a ChangeInfo or a + * RelatedChangeAndCommitInfo object. + * @return {number} + */ + _getChangeNumber(change) { + // Default to 0 if change property is not defined. + if (!change) return 0; + + if (change.hasOwnProperty('_change_number')) { + return change._change_number; + } + return change._number; + } + + _computeLinkClass(change) { + const statuses = []; + if (change.status == this.ChangeStatus.ABANDONED) { + statuses.push('strikethrough'); + } + if (change.submittable) { + statuses.push('submittable'); + } + return statuses.join(' '); + } + + _computeChangeStatusClass(change) { + const classes = ['status']; + if (change._revision_number != change._current_revision_number) { + classes.push('notCurrent'); + } else if (this._isIndirectAncestor(change)) { + classes.push('indirectAncestor'); + } else if (change.submittable) { + classes.push('submittable'); + } else if (change.status == this.ChangeStatus.NEW) { + classes.push('hidden'); + } + return classes.join(' '); + } + + _computeChangeStatus(change) { + switch (change.status) { + case this.ChangeStatus.MERGED: + return 'Merged'; + case this.ChangeStatus.ABANDONED: + return 'Abandoned'; + } + if (change._revision_number != change._current_revision_number) { + return 'Not current'; + } else if (this._isIndirectAncestor(change)) { + return 'Indirect ancestor'; + } else if (change.submittable) { + return 'Submittable'; + } + return ''; + } + + _resultsChanged(related, submittedTogether, conflicts, + cherryPicks, sameTopic) { + // Polymer 2: check for undefined + if ([ + related, + submittedTogether, + conflicts, + cherryPicks, + sameTopic, + ].some(arg => arg === undefined)) { + return; + } + + const results = [ + related && related.changes, + submittedTogether && submittedTogether.changes, + conflicts, + cherryPicks, + sameTopic, + ]; + for (let i = 0; i < results.length; i++) { + if (results[i] && results[i].length > 0) { + this.hidden = false; + this.fire('update', null, {bubbles: false}); + return; + } + } + this.hidden = true; + } + + _isIndirectAncestor(change) { + return !this._connectedRevisions.includes(change.commit.commit); + } + + _computeConnectedRevisions(change, patchNum, relatedChanges) { + // Polymer 2: check for undefined + if ([change, patchNum, relatedChanges].some(arg => arg === undefined)) { + return undefined; + } + + const connected = []; + let changeRevision; + if (!change) { return []; } + for (const rev in change.revisions) { + if (this.patchNumEquals(change.revisions[rev]._number, patchNum)) { + changeRevision = rev; + } + } + const commits = relatedChanges.map(c => c.commit); + let pos = commits.length - 1; + + while (pos >= 0) { + const commit = commits[pos].commit; + connected.push(commit); + if (commit == changeRevision) { + break; + } + pos--; + } + while (pos >= 0) { + for (let i = 0; i < commits[pos].parents.length; i++) { + if (connected.includes(commits[pos].parents[i].commit)) { + connected.push(commits[pos].commit); + break; + } + } + --pos; + } + return connected; + } + + _computeSubmittedTogetherClass(submittedTogether) { + if (!submittedTogether || ( + submittedTogether.changes.length === 0 && + !submittedTogether.non_visible_changes)) { + return 'hidden'; + } + return ''; + } + + _computeNonVisibleChangesNote(n) { + const noun = n === 1 ? 'change' : 'changes'; + return `(+ ${n} non-visible ${noun})`; + } +} + +customElements.define(GrRelatedChangesList.is, GrRelatedChangesList);
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js index 696ffdf..1d8551d 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-related-changes-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -104,39 +96,27 @@ } </style> <div> - <section class="relatedChanges" hidden$="[[!_relatedResponse.changes.length]]" hidden> + <section class="relatedChanges" hidden\$="[[!_relatedResponse.changes.length]]" hidden=""> <h4>Relation chain</h4> - <template - is="dom-repeat" - items="[[_relatedResponse.changes]]" - as="related"> - <div class$="rightIndent [[_computeChangeContainerClass(change, related)]]"> - <a href$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]" - class$="[[_computeLinkClass(related)]]" - title$="[[related.commit.subject]]"> + <template is="dom-repeat" items="[[_relatedResponse.changes]]" as="related"> + <div class\$="rightIndent [[_computeChangeContainerClass(change, related)]]"> + <a href\$="[[_computeChangeURL(related._change_number, related.project, related._revision_number)]]" class\$="[[_computeLinkClass(related)]]" title\$="[[related.commit.subject]]"> [[related.commit.subject]] </a> - <span class$="[[_computeChangeStatusClass(related)]]"> + <span class\$="[[_computeChangeStatusClass(related)]]"> ([[_computeChangeStatus(related)]]) </span> </div> </template> </section> - <section - id="submittedTogether" - class$="[[_computeSubmittedTogetherClass(_submittedTogether)]]"> + <section id="submittedTogether" class\$="[[_computeSubmittedTogetherClass(_submittedTogether)]]"> <h4>Submitted together</h4> <template is="dom-repeat" items="[[_submittedTogether.changes]]" as="related"> - <div class$="[[_computeChangeContainerClass(change, related)]]"> - <a href$="[[_computeChangeURL(related._number, related.project)]]" - class$="[[_computeLinkClass(related)]]" - title$="[[related.project]]: [[related.branch]]: [[related.subject]]"> + <div class\$="[[_computeChangeContainerClass(change, related)]]"> + <a href\$="[[_computeChangeURL(related._number, related.project)]]" class\$="[[_computeLinkClass(related)]]" title\$="[[related.project]]: [[related.branch]]: [[related.subject]]"> [[related.project]]: [[related.branch]]: [[related.subject]] </a> - <span - tabindex="-1" - title="Submittable" - class$="submittableCheck [[_computeLinkClass(related)]]">✓</span> + <span tabindex="-1" title="Submittable" class\$="submittableCheck [[_computeLinkClass(related)]]">✓</span> </div> </template> <template is="dom-if" if="[[_submittedTogether.non_visible_changes]]"> @@ -145,45 +125,37 @@ </div> </template> </section> - <section hidden$="[[!_sameTopic.length]]" hidden> + <section hidden\$="[[!_sameTopic.length]]" hidden=""> <h4>Same topic</h4> <template is="dom-repeat" items="[[_sameTopic]]" as="change"> <div> - <a href$="[[_computeChangeURL(change._number, change.project)]]" - class$="[[_computeLinkClass(change)]]" - title$="[[change.project]]: [[change.branch]]: [[change.subject]]"> + <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.project]]: [[change.branch]]: [[change.subject]]"> [[change.project]]: [[change.branch]]: [[change.subject]] </a> </div> </template> </section> - <section hidden$="[[!_conflicts.length]]" hidden> + <section hidden\$="[[!_conflicts.length]]" hidden=""> <h4>Merge conflicts</h4> <template is="dom-repeat" items="[[_conflicts]]" as="change"> <div> - <a href$="[[_computeChangeURL(change._number, change.project)]]" - class$="[[_computeLinkClass(change)]]" - title$="[[change.subject]]"> + <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.subject]]"> [[change.subject]] </a> </div> </template> </section> - <section hidden$="[[!_cherryPicks.length]]" hidden> + <section hidden\$="[[!_cherryPicks.length]]" hidden=""> <h4>Cherry picks</h4> <template is="dom-repeat" items="[[_cherryPicks]]" as="change"> <div> - <a href$="[[_computeChangeURL(change._number, change.project)]]" - class$="[[_computeLinkClass(change)]]" - title$="[[change.branch]]: [[change.subject]]"> + <a href\$="[[_computeChangeURL(change._number, change.project)]]" class\$="[[_computeLinkClass(change)]]" title\$="[[change.branch]]: [[change.subject]]"> [[change.branch]]: [[change.subject]] </a> </div> </template> </section> </div> - <div hidden$="[[!loading]]">Loading...</div> + <div hidden\$="[[!loading]]">Loading...</div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-related-changes-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html index 9b8ebed..43037af 100644 --- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-related-changes-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-related-changes-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-related-changes-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-related-changes-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,238 +40,260 @@ </template> </test-fixture> -<script> - suite('gr-related-changes-list tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-related-changes-list.js'; +suite('gr-related-changes-list tests', () => { + let element; + let sandbox; + + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('connected revisions', () => { + const change = { + revisions: { + 'e3c6d60783bfdec9ebae7dcfec4662360433449e': { + _number: 1, + }, + '26e5e4c9c7ae31cbd876271cca281ce22b413997': { + _number: 2, + }, + 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': { + _number: 7, + }, + 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': { + _number: 5, + }, + 'd6bcee67570859ccb684873a85cf50b1f0e96fda': { + _number: 6, + }, + 'cc960918a7f90388f4a9e05753d0f7b90ad44546': { + _number: 3, + }, + '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': { + _number: 4, + }, + }, + }; + let patchNum = 7; + let relatedChanges = [ + { + commit: { + commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', + parents: [ + { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + }, + ], + }, + }, + { + commit: { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + parents: [ + { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + }, + ], + }, + }, + { + commit: { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + parents: [ + { + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + }, + ], + }, + }, + { + commit: { + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + parents: [ + { + commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + }, + ], + }, + }, + { + commit: { + commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + parents: [ + { + commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', + }, + ], + }, + }, + { + commit: { + commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', + parents: [ + { + commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75', + }, + ], + }, + }, + ]; + + let connectedChanges = + element._computeConnectedRevisions(change, patchNum, relatedChanges); + assert.deepEqual(connectedChanges, [ + '613bc4f81741a559c6667ac08d71dcc3348f73ce', + 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', + 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', + ]); + + patchNum = 4; + relatedChanges = [ + { + commit: { + commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', + parents: [ + { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + }, + ], + }, + }, + { + commit: { + commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', + parents: [ + { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + }, + ], + }, + }, + { + commit: { + commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', + parents: [ + { + commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', + }, + ], + }, + }, + { + commit: { + commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', + parents: [ + { + commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + }, + ], + }, + }, + { + commit: { + commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + parents: [ + { + commit: 'af815dac54318826b7f1fa468acc76349ffc588e', + }, + ], + }, + }, + { + commit: { + commit: 'af815dac54318826b7f1fa468acc76349ffc588e', + parents: [ + { + commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c', + }, + ], + }, + }, + ]; + + connectedChanges = + element._computeConnectedRevisions(change, patchNum, relatedChanges); + assert.deepEqual(connectedChanges, [ + 'af815dac54318826b7f1fa468acc76349ffc588e', + '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', + 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', + ]); + }); + + test('_computeChangeContainerClass', () => { + const change1 = {change_id: 123, _number: 0}; + const change2 = {change_id: 456, _change_number: 1}; + const change3 = {change_id: 123, _number: 2}; + + assert.notEqual(element._computeChangeContainerClass( + change1, change1).indexOf('thisChange'), -1); + assert.equal(element._computeChangeContainerClass( + change1, change2).indexOf('thisChange'), -1); + assert.equal(element._computeChangeContainerClass( + change1, change3).indexOf('thisChange'), -1); + }); + + test('_changesEqual', () => { + const change1 = {change_id: 123, _number: 0}; + const change2 = {change_id: 456, _number: 1}; + const change3 = {change_id: 123, _number: 2}; + const change4 = {change_id: 123, _change_number: 1}; + + assert.isTrue(element._changesEqual(change1, change1)); + assert.isFalse(element._changesEqual(change1, change2)); + assert.isFalse(element._changesEqual(change1, change3)); + assert.isTrue(element._changesEqual(change2, change4)); + }); + + test('_getChangeNumber', () => { + const change1 = {change_id: 123, _number: 0}; + const change2 = {change_id: 456, _change_number: 1}; + assert.equal(element._getChangeNumber(change1), 0); + assert.equal(element._getChangeNumber(change2), 1); + }); + + test('event for section loaded fires for each section ', () => { + const loadedStub = sandbox.stub(); + element.patchNum = 7; + element.change = { + change_id: 123, + status: 'NEW', + }; + element.mergeable = true; + element.addEventListener('new-section-loaded', loadedStub); + sandbox.stub(element, '_getRelatedChanges') + .returns(Promise.resolve({changes: []})); + sandbox.stub(element, '_getSubmittedTogether') + .returns(Promise.resolve()); + sandbox.stub(element, '_getCherryPicks') + .returns(Promise.resolve()); + sandbox.stub(element, '_getConflicts') + .returns(Promise.resolve()); + + return element.reload().then(() => { + assert.equal(loadedStub.callCount, 4); + }); + }); + + suite('_getConflicts resolves undefined', () => { let element; - let sandbox; setup(() => { element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - teardown(() => { - sandbox.restore(); - }); - - test('connected revisions', () => { - const change = { - revisions: { - 'e3c6d60783bfdec9ebae7dcfec4662360433449e': { - _number: 1, - }, - '26e5e4c9c7ae31cbd876271cca281ce22b413997': { - _number: 2, - }, - 'bf7884d695296ca0c91702ba3e2bc8df0f69a907': { - _number: 7, - }, - 'b5fc49f2e67d1889d5275cac04ad3648f2ec7fe3': { - _number: 5, - }, - 'd6bcee67570859ccb684873a85cf50b1f0e96fda': { - _number: 6, - }, - 'cc960918a7f90388f4a9e05753d0f7b90ad44546': { - _number: 3, - }, - '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6': { - _number: 4, - }, - }, - }; - let patchNum = 7; - let relatedChanges = [ - { - commit: { - commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', - parents: [ - { - commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', - }, - ], - }, - }, - { - commit: { - commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', - parents: [ - { - commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', - }, - ], - }, - }, - { - commit: { - commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', - parents: [ - { - commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', - }, - ], - }, - }, - { - commit: { - commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', - parents: [ - { - commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', - }, - ], - }, - }, - { - commit: { - commit: 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', - parents: [ - { - commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', - }, - ], - }, - }, - { - commit: { - commit: '613bc4f81741a559c6667ac08d71dcc3348f73ce', - parents: [ - { - commit: '455ed9cd27a16bf6991f04dcc57ef575dc4d5e75', - }, - ], - }, - }, - ]; - - let connectedChanges = - element._computeConnectedRevisions(change, patchNum, relatedChanges); - assert.deepEqual(connectedChanges, [ - '613bc4f81741a559c6667ac08d71dcc3348f73ce', - 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', - 'bf7884d695296ca0c91702ba3e2bc8df0f69a907', - 'b0ccb183494a8e340b8725a2dc553967d61e6dae', - '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', - '87ed20b241576b620bbaa3dfd47715ce6782b7dd', - '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', - ]); - - patchNum = 4; - relatedChanges = [ - { - commit: { - commit: '2cebeedfb1e80f4b872d0a13ade529e70652c0c8', - parents: [ - { - commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', - }, - ], - }, - }, - { - commit: { - commit: '87ed20b241576b620bbaa3dfd47715ce6782b7dd', - parents: [ - { - commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', - }, - ], - }, - }, - { - commit: { - commit: '6c71f9e86ba955a7e01e2088bce0050a90eb9fbb', - parents: [ - { - commit: 'b0ccb183494a8e340b8725a2dc553967d61e6dae', - }, - ], - }, - }, - { - commit: { - commit: 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', - parents: [ - { - commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', - }, - ], - }, - }, - { - commit: { - commit: '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', - parents: [ - { - commit: 'af815dac54318826b7f1fa468acc76349ffc588e', - }, - ], - }, - }, - { - commit: { - commit: 'af815dac54318826b7f1fa468acc76349ffc588e', - parents: [ - { - commit: '58f76e406e24cb8b0f5d64c7f5ac1e8616d0a22c', - }, - ], - }, - }, - ]; - - connectedChanges = - element._computeConnectedRevisions(change, patchNum, relatedChanges); - assert.deepEqual(connectedChanges, [ - 'af815dac54318826b7f1fa468acc76349ffc588e', - '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', - '9e593f6dcc2c0785a2ad2c895a34ad2aa9a0d8b6', - 'a3e5d9d4902b915a39e2efba5577211b9b3ebe7b', - ]); - }); - - test('_computeChangeContainerClass', () => { - const change1 = {change_id: 123, _number: 0}; - const change2 = {change_id: 456, _change_number: 1}; - const change3 = {change_id: 123, _number: 2}; - - assert.notEqual(element._computeChangeContainerClass( - change1, change1).indexOf('thisChange'), -1); - assert.equal(element._computeChangeContainerClass( - change1, change2).indexOf('thisChange'), -1); - assert.equal(element._computeChangeContainerClass( - change1, change3).indexOf('thisChange'), -1); - }); - - test('_changesEqual', () => { - const change1 = {change_id: 123, _number: 0}; - const change2 = {change_id: 456, _number: 1}; - const change3 = {change_id: 123, _number: 2}; - const change4 = {change_id: 123, _change_number: 1}; - - assert.isTrue(element._changesEqual(change1, change1)); - assert.isFalse(element._changesEqual(change1, change2)); - assert.isFalse(element._changesEqual(change1, change3)); - assert.isTrue(element._changesEqual(change2, change4)); - }); - - test('_getChangeNumber', () => { - const change1 = {change_id: 123, _number: 0}; - const change2 = {change_id: 456, _change_number: 1}; - assert.equal(element._getChangeNumber(change1), 0); - assert.equal(element._getChangeNumber(change2), 1); - }); - - test('event for section loaded fires for each section ', () => { - const loadedStub = sandbox.stub(); - element.patchNum = 7; - element.change = { - change_id: 123, - status: 'NEW', - }; - element.mergeable = true; - element.addEventListener('new-section-loaded', loadedStub); sandbox.stub(element, '_getRelatedChanges') .returns(Promise.resolve({changes: []})); sandbox.stub(element, '_getSubmittedTogether') @@ -275,283 +302,263 @@ .returns(Promise.resolve()); sandbox.stub(element, '_getConflicts') .returns(Promise.resolve()); - - return element.reload().then(() => { - assert.equal(loadedStub.callCount, 4); - }); }); - suite('_getConflicts resolves undefined', () => { - let element; - - setup(() => { - element = fixture('basic'); - - sandbox.stub(element, '_getRelatedChanges') - .returns(Promise.resolve({changes: []})); - sandbox.stub(element, '_getSubmittedTogether') - .returns(Promise.resolve()); - sandbox.stub(element, '_getCherryPicks') - .returns(Promise.resolve()); - sandbox.stub(element, '_getConflicts') - .returns(Promise.resolve()); - }); - - test('_conflicts are an empty array', () => { - element.patchNum = 7; - element.change = { - change_id: 123, - status: 'NEW', - }; - element.mergeable = true; - element.reload(); - assert.equal(element._conflicts.length, 0); - }); - }); - - suite('get conflicts tests', () => { - let element; - let conflictsStub; - - setup(() => { - element = fixture('basic'); - - sandbox.stub(element, '_getRelatedChanges') - .returns(Promise.resolve({changes: []})); - sandbox.stub(element, '_getSubmittedTogether') - .returns(Promise.resolve()); - sandbox.stub(element, '_getCherryPicks') - .returns(Promise.resolve()); - conflictsStub = sandbox.stub(element, '_getConflicts') - .returns(Promise.resolve()); - }); - - test('request conflicts if open and mergeable', () => { - element.patchNum = 7; - element.change = { - change_id: 123, - status: 'NEW', - }; - element.mergeable = true; - element.reload(); - assert.isTrue(conflictsStub.called); - }); - - test('does not request conflicts if closed and mergeable', () => { - element.patchNum = 7; - element.change = { - change_id: 123, - status: 'MERGED', - }; - element.reload(); - assert.isFalse(conflictsStub.called); - }); - - test('does not request conflicts if open and not mergeable', () => { - element.patchNum = 7; - element.change = { - change_id: 123, - status: 'NEW', - }; - element.mergeable = false; - element.reload(); - assert.isFalse(conflictsStub.called); - }); - - test('doesnt request conflicts if closed and not mergeable', () => { - element.patchNum = 7; - element.change = { - change_id: 123, - status: 'MERGED', - }; - element.mergeable = false; - element.reload(); - assert.isFalse(conflictsStub.called); - }); - }); - - test('_calculateHasParent', () => { - const changeId = 123; - const relatedChanges = []; - - assert.equal(element._calculateHasParent(changeId, relatedChanges), - false); - - relatedChanges.push({change_id: 123}); - assert.equal(element._calculateHasParent(changeId, relatedChanges), - false); - - relatedChanges.push({change_id: 234}); - assert.equal(element._calculateHasParent(changeId, relatedChanges), - true); - }); - - suite('hidden attribute and update event', () => { - const changes = [{ - project: 'foo/bar', - change_id: 'Ideadbeef', - commit: { - commit: 'deadbeef', - parents: [{commit: 'abc123'}], - author: {}, - subject: 'do that thing', - }, - _change_number: 12345, - _revision_number: 1, - _current_revision_number: 1, - status: 'NEW', - }]; - - test('clear and empties', () => { - element._relatedResponse = {changes}; - element._submittedTogether = {changes}; - element._conflicts = changes; - element._cherryPicks = changes; - element._sameTopic = changes; - - element.hidden = false; - element.clear(); - assert.isTrue(element.hidden); - assert.equal(element._relatedResponse.changes.length, 0); - assert.equal(element._submittedTogether.changes.length, 0); - assert.equal(element._conflicts.length, 0); - assert.equal(element._cherryPicks.length, 0); - assert.equal(element._sameTopic.length, 0); - }); - - test('update fires', () => { - const updateHandler = sandbox.stub(); - element.addEventListener('update', updateHandler); - - element._resultsChanged({}, {}, [], [], []); - assert.isTrue(element.hidden); - assert.isFalse(updateHandler.called); - - element._resultsChanged({}, {}, [], [], ['test']); - assert.isFalse(element.hidden); - assert.isTrue(updateHandler.called); - }); - - suite('hiding and unhiding', () => { - test('related response', () => { - assert.isTrue(element.hidden); - element._resultsChanged({changes}, {}, [], [], []); - assert.isFalse(element.hidden); - }); - - test('submitted together', () => { - assert.isTrue(element.hidden); - element._resultsChanged({}, {changes}, [], [], []); - assert.isFalse(element.hidden); - }); - - test('conflicts', () => { - assert.isTrue(element.hidden); - element._resultsChanged({}, {}, changes, [], []); - assert.isFalse(element.hidden); - }); - - test('cherrypicks', () => { - assert.isTrue(element.hidden); - element._resultsChanged({}, {}, [], changes, []); - assert.isFalse(element.hidden); - }); - - test('same topic', () => { - assert.isTrue(element.hidden); - element._resultsChanged({}, {}, [], [], changes); - assert.isFalse(element.hidden); - }); - }); - }); - - test('_computeChangeURL uses Gerrit.Nav', () => { - const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChangeById'); - element._computeChangeURL(123, 'abc/def', 12); - assert.isTrue(getUrlStub.called); - }); - - suite('submitted together changes', () => { - const change = { - project: 'foo/bar', - change_id: 'Ideadbeef', - commit: { - commit: 'deadbeef', - parents: [{commit: 'abc123'}], - author: {}, - subject: 'do that thing', - }, - _change_number: 12345, - _revision_number: 1, - _current_revision_number: 1, + test('_conflicts are an empty array', () => { + element.patchNum = 7; + element.change = { + change_id: 123, status: 'NEW', }; + element.mergeable = true; + element.reload(); + assert.equal(element._conflicts.length, 0); + }); + }); - test('_computeSubmittedTogetherClass', () => { - assert.strictEqual( - element._computeSubmittedTogetherClass(undefined), - 'hidden'); - assert.strictEqual( - element._computeSubmittedTogetherClass({changes: []}), - 'hidden'); - assert.strictEqual( - element._computeSubmittedTogetherClass({changes: [{}]}), - ''); - assert.strictEqual( - element._computeSubmittedTogetherClass({ - changes: [], - non_visible_changes: 0, - }), - 'hidden'); - assert.strictEqual( - element._computeSubmittedTogetherClass({ - changes: [], - non_visible_changes: 1, - }), - ''); - assert.strictEqual( - element._computeSubmittedTogetherClass({ - changes: [{}], - non_visible_changes: 1, - }), - ''); + suite('get conflicts tests', () => { + let element; + let conflictsStub; + + setup(() => { + element = fixture('basic'); + + sandbox.stub(element, '_getRelatedChanges') + .returns(Promise.resolve({changes: []})); + sandbox.stub(element, '_getSubmittedTogether') + .returns(Promise.resolve()); + sandbox.stub(element, '_getCherryPicks') + .returns(Promise.resolve()); + conflictsStub = sandbox.stub(element, '_getConflicts') + .returns(Promise.resolve()); + }); + + test('request conflicts if open and mergeable', () => { + element.patchNum = 7; + element.change = { + change_id: 123, + status: 'NEW', + }; + element.mergeable = true; + element.reload(); + assert.isTrue(conflictsStub.called); + }); + + test('does not request conflicts if closed and mergeable', () => { + element.patchNum = 7; + element.change = { + change_id: 123, + status: 'MERGED', + }; + element.reload(); + assert.isFalse(conflictsStub.called); + }); + + test('does not request conflicts if open and not mergeable', () => { + element.patchNum = 7; + element.change = { + change_id: 123, + status: 'NEW', + }; + element.mergeable = false; + element.reload(); + assert.isFalse(conflictsStub.called); + }); + + test('doesnt request conflicts if closed and not mergeable', () => { + element.patchNum = 7; + element.change = { + change_id: 123, + status: 'MERGED', + }; + element.mergeable = false; + element.reload(); + assert.isFalse(conflictsStub.called); + }); + }); + + test('_calculateHasParent', () => { + const changeId = 123; + const relatedChanges = []; + + assert.equal(element._calculateHasParent(changeId, relatedChanges), + false); + + relatedChanges.push({change_id: 123}); + assert.equal(element._calculateHasParent(changeId, relatedChanges), + false); + + relatedChanges.push({change_id: 234}); + assert.equal(element._calculateHasParent(changeId, relatedChanges), + true); + }); + + suite('hidden attribute and update event', () => { + const changes = [{ + project: 'foo/bar', + change_id: 'Ideadbeef', + commit: { + commit: 'deadbeef', + parents: [{commit: 'abc123'}], + author: {}, + subject: 'do that thing', + }, + _change_number: 12345, + _revision_number: 1, + _current_revision_number: 1, + status: 'NEW', + }]; + + test('clear and empties', () => { + element._relatedResponse = {changes}; + element._submittedTogether = {changes}; + element._conflicts = changes; + element._cherryPicks = changes; + element._sameTopic = changes; + + element.hidden = false; + element.clear(); + assert.isTrue(element.hidden); + assert.equal(element._relatedResponse.changes.length, 0); + assert.equal(element._submittedTogether.changes.length, 0); + assert.equal(element._conflicts.length, 0); + assert.equal(element._cherryPicks.length, 0); + assert.equal(element._sameTopic.length, 0); + }); + + test('update fires', () => { + const updateHandler = sandbox.stub(); + element.addEventListener('update', updateHandler); + + element._resultsChanged({}, {}, [], [], []); + assert.isTrue(element.hidden); + assert.isFalse(updateHandler.called); + + element._resultsChanged({}, {}, [], [], ['test']); + assert.isFalse(element.hidden); + assert.isTrue(updateHandler.called); + }); + + suite('hiding and unhiding', () => { + test('related response', () => { + assert.isTrue(element.hidden); + element._resultsChanged({changes}, {}, [], [], []); + assert.isFalse(element.hidden); }); - test('no submitted together changes', () => { - flushAsynchronousOperations(); - assert.include(element.$.submittedTogether.className, 'hidden'); + test('submitted together', () => { + assert.isTrue(element.hidden); + element._resultsChanged({}, {changes}, [], [], []); + assert.isFalse(element.hidden); }); - test('no non-visible submitted together changes', () => { - element._submittedTogether = {changes: [change]}; - flushAsynchronousOperations(); - assert.notInclude(element.$.submittedTogether.className, 'hidden'); - assert.isNull(element.shadowRoot - .querySelector('.note')); + test('conflicts', () => { + assert.isTrue(element.hidden); + element._resultsChanged({}, {}, changes, [], []); + assert.isFalse(element.hidden); }); - test('no visible submitted together changes', () => { - // Technically this should never happen, but worth asserting the logic. - element._submittedTogether = {changes: [], non_visible_changes: 1}; - flushAsynchronousOperations(); - assert.notInclude(element.$.submittedTogether.className, 'hidden'); - assert.isNotNull(element.shadowRoot - .querySelector('.note')); - assert.strictEqual( - element.shadowRoot - .querySelector('.note').innerText, '(+ 1 non-visible change)'); + test('cherrypicks', () => { + assert.isTrue(element.hidden); + element._resultsChanged({}, {}, [], changes, []); + assert.isFalse(element.hidden); }); - test('visible and non-visible submitted together changes', () => { - element._submittedTogether = {changes: [change], non_visible_changes: 2}; - flushAsynchronousOperations(); - assert.notInclude(element.$.submittedTogether.className, 'hidden'); - assert.isNotNull(element.shadowRoot - .querySelector('.note')); - assert.strictEqual( - element.shadowRoot - .querySelector('.note').innerText, '(+ 2 non-visible changes)'); + test('same topic', () => { + assert.isTrue(element.hidden); + element._resultsChanged({}, {}, [], [], changes); + assert.isFalse(element.hidden); }); }); }); + + test('_computeChangeURL uses Gerrit.Nav', () => { + const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForChangeById'); + element._computeChangeURL(123, 'abc/def', 12); + assert.isTrue(getUrlStub.called); + }); + + suite('submitted together changes', () => { + const change = { + project: 'foo/bar', + change_id: 'Ideadbeef', + commit: { + commit: 'deadbeef', + parents: [{commit: 'abc123'}], + author: {}, + subject: 'do that thing', + }, + _change_number: 12345, + _revision_number: 1, + _current_revision_number: 1, + status: 'NEW', + }; + + test('_computeSubmittedTogetherClass', () => { + assert.strictEqual( + element._computeSubmittedTogetherClass(undefined), + 'hidden'); + assert.strictEqual( + element._computeSubmittedTogetherClass({changes: []}), + 'hidden'); + assert.strictEqual( + element._computeSubmittedTogetherClass({changes: [{}]}), + ''); + assert.strictEqual( + element._computeSubmittedTogetherClass({ + changes: [], + non_visible_changes: 0, + }), + 'hidden'); + assert.strictEqual( + element._computeSubmittedTogetherClass({ + changes: [], + non_visible_changes: 1, + }), + ''); + assert.strictEqual( + element._computeSubmittedTogetherClass({ + changes: [{}], + non_visible_changes: 1, + }), + ''); + }); + + test('no submitted together changes', () => { + flushAsynchronousOperations(); + assert.include(element.$.submittedTogether.className, 'hidden'); + }); + + test('no non-visible submitted together changes', () => { + element._submittedTogether = {changes: [change]}; + flushAsynchronousOperations(); + assert.notInclude(element.$.submittedTogether.className, 'hidden'); + assert.isNull(element.shadowRoot + .querySelector('.note')); + }); + + test('no visible submitted together changes', () => { + // Technically this should never happen, but worth asserting the logic. + element._submittedTogether = {changes: [], non_visible_changes: 1}; + flushAsynchronousOperations(); + assert.notInclude(element.$.submittedTogether.className, 'hidden'); + assert.isNotNull(element.shadowRoot + .querySelector('.note')); + assert.strictEqual( + element.shadowRoot + .querySelector('.note').innerText, '(+ 1 non-visible change)'); + }); + + test('visible and non-visible submitted together changes', () => { + element._submittedTogether = {changes: [change], non_visible_changes: 2}; + flushAsynchronousOperations(); + assert.notInclude(element.$.submittedTogether.className, 'hidden'); + assert.isNotNull(element.shadowRoot + .querySelector('.note')); + assert.strictEqual( + element.shadowRoot + .querySelector('.note').innerText, '(+ 2 non-visible changes)'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html index 5fd3795..7e651c5 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reply-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../plugins/gr-plugin-host/gr-plugin-host.html"> -<link rel="import" href="gr-reply-dialog.html"> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../plugins/gr-plugin-host/gr-plugin-host.js"></script> +<script type="module" src="./gr-reply-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../plugins/gr-plugin-host/gr-plugin-host.js'; +import './gr-reply-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -43,130 +49,134 @@ </template> </test-fixture> -<script> - suite('gr-reply-dialog tests', async () => { - await readyToTest(); - let element; - let changeNum; - let patchNum; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../plugins/gr-plugin-host/gr-plugin-host.js'; +import './gr-reply-dialog.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-reply-dialog tests', () => { + let element; + let changeNum; + let patchNum; - let sandbox; + let sandbox; - const setupElement = element => { - element.change = { - _number: changeNum, - labels: { - 'Verified': { - values: { - '-1': 'Fails', - ' 0': 'No score', - '+1': 'Verified', - }, - default_value: 0, + const setupElement = element => { + element.change = { + _number: changeNum, + labels: { + 'Verified': { + values: { + '-1': 'Fails', + ' 0': 'No score', + '+1': 'Verified', }, - 'Code-Review': { - values: { - '-2': 'Do not submit', - '-1': 'I would prefer that you didn\'t submit this', - ' 0': 'No score', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', - }, - all: [{_account_id: 42, value: 0}], - default_value: 0, - }, + default_value: 0, }, - }; - element.patchNum = patchNum; - element.permittedLabels = { - 'Code-Review': [ - '-1', - ' 0', - '+1', - ], - 'Verified': [ - '-1', - ' 0', - '+1', - ], - }; - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({isLatest: true})); + 'Code-Review': { + values: { + '-2': 'Do not submit', + '-1': 'I would prefer that you didn\'t submit this', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + all: [{_account_id: 42, value: 0}], + default_value: 0, + }, + }, }; + element.patchNum = patchNum; + element.permittedLabels = { + 'Code-Review': [ + '-1', + ' 0', + '+1', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({isLatest: true})); + }; - setup(() => { - sandbox = sinon.sandbox.create(); + setup(() => { + sandbox = sinon.sandbox.create(); - changeNum = 42; - patchNum = 1; + changeNum = 42; + patchNum = 1; - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getAccount() { return Promise.resolve({_account_id: 42}); }, - }); - - element = fixture('basic'); - setupElement(element); - - // Allow the elements created by dom-repeat to be stamped. - flushAsynchronousOperations(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getAccount() { return Promise.resolve({_account_id: 42}); }, }); - teardown(() => { - sandbox.restore(); - }); + element = fixture('basic'); + setupElement(element); - test('_submit blocked when invalid email is supplied to ccs', () => { - const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve()); - // Stub the below function to avoid side effects from the send promise - // resolving. - sandbox.stub(element, '_purgeReviewersPendingRemove'); + // Allow the elements created by dom-repeat to be stamped. + flushAsynchronousOperations(); + }); - element.$.ccs.$.entry.setText('test'); - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button.send')); - assert.isFalse(sendStub.called); - flushAsynchronousOperations(); + teardown(() => { + sandbox.restore(); + }); - element.$.ccs.$.entry.setText('test@test.test'); - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button.send')); - assert.isTrue(sendStub.called); - }); + test('_submit blocked when invalid email is supplied to ccs', () => { + const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve()); + // Stub the below function to avoid side effects from the send promise + // resolving. + sandbox.stub(element, '_purgeReviewersPendingRemove'); - test('lgtm plugin', done => { - Gerrit._testOnly_resetPlugins(); - const pluginHost = fixture('plugin-host'); - pluginHost.config = { - plugin: { - js_resource_paths: [], - html_resource_paths: [ - new URL('test/plugin.html?' + Math.random(), - window.location.href).toString(), - ], - }, - }; - element = fixture('basic'); - setupElement(element); - const importSpy = - sandbox.spy(element.shadowRoot - .querySelector('gr-endpoint-decorator'), '_import'); - Gerrit.awaitPluginsLoaded().then(() => { - Promise.all(importSpy.returnValues).then(() => { - flush(() => { - const textarea = element.$.textarea.getNativeTextarea(); - textarea.value = 'LGTM'; - textarea.dispatchEvent(new CustomEvent( - 'input', {bubbles: true, composed: true})); - const labelScoreRows = Polymer.dom(element.$.labelScores.root) - .querySelector('gr-label-score-row[name="Code-Review"]'); - const selectedBtn = Polymer.dom(labelScoreRows.root) - .querySelector('gr-button[data-value="+1"].iron-selected'); - assert.isOk(selectedBtn); - done(); - }); + element.$.ccs.$.entry.setText('test'); + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button.send')); + assert.isFalse(sendStub.called); + flushAsynchronousOperations(); + + element.$.ccs.$.entry.setText('test@test.test'); + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button.send')); + assert.isTrue(sendStub.called); + }); + + test('lgtm plugin', done => { + Gerrit._testOnly_resetPlugins(); + const pluginHost = fixture('plugin-host'); + pluginHost.config = { + plugin: { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html?' + Math.random(), + window.location.href).toString(), + ], + }, + }; + element = fixture('basic'); + setupElement(element); + const importSpy = + sandbox.spy(element.shadowRoot + .querySelector('gr-endpoint-decorator'), '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues).then(() => { + flush(() => { + const textarea = element.$.textarea.getNativeTextarea(); + textarea.value = 'LGTM'; + textarea.dispatchEvent(new CustomEvent( + 'input', {bubbles: true, composed: true})); + const labelScoreRows = dom(element.$.labelScores.root) + .querySelector('gr-label-score-row[name="Code-Review"]'); + const selectedBtn = dom(labelScoreRows.root) + .querySelector('gr-button[data-value="+1"].iron-selected'); + assert.isOk(selectedBtn); + done(); }); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js index b1a05f5..305505d 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,887 +14,916 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const STORAGE_DEBOUNCE_INTERVAL_MS = 400; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../shared/gr-account-chip/gr-account-chip.js'; +import '../../shared/gr-textarea/gr-textarea.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-formatted-text/gr-formatted-text.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-storage/gr-storage.js'; +import '../../shared/gr-account-list/gr-account-list.js'; +import '../gr-label-scores/gr-label-scores.js'; +import '../gr-thread-list/gr-thread-list.js'; +import '../../../styles/shared-styles.js'; +import '../gr-comment-list/gr-comment-list.js'; +import '../../../scripts/gr-display-name-utils/gr-display-name-utils.js'; +import '../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-reply-dialog_html.js'; - const FocusTarget = { - ANY: 'any', - BODY: 'body', - CCS: 'cc', - REVIEWERS: 'reviewers', - }; +const STORAGE_DEBOUNCE_INTERVAL_MS = 400; - const ReviewerTypes = { - REVIEWER: 'REVIEWER', - CC: 'CC', - }; +const FocusTarget = { + ANY: 'any', + BODY: 'body', + CCS: 'cc', + REVIEWERS: 'reviewers', +}; - const LatestPatchState = { - LATEST: 'latest', - CHECKING: 'checking', - NOT_LATEST: 'not-latest', - }; +const ReviewerTypes = { + REVIEWER: 'REVIEWER', + CC: 'CC', +}; - const ButtonLabels = { - START_REVIEW: 'Start review', - SEND: 'Send', - }; +const LatestPatchState = { + LATEST: 'latest', + CHECKING: 'checking', + NOT_LATEST: 'not-latest', +}; - const ButtonTooltips = { - SAVE: 'Save but do not send notification or change review state', - START_REVIEW: 'Mark as ready for review and send reply', - SEND: 'Send reply', - }; +const ButtonLabels = { + START_REVIEW: 'Start review', + SEND: 'Send', +}; - const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.'; +const ButtonTooltips = { + SAVE: 'Save but do not send notification or change review state', + START_REVIEW: 'Mark as ready for review and send reply', + SEND: 'Send reply', +}; - const SEND_REPLY_TIMING_LABEL = 'SendReply'; +const EMPTY_REPLY_MESSAGE = 'Cannot send an empty reply.'; + +const SEND_REPLY_TIMING_LABEL = 'SendReply'; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrReplyDialog extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-reply-dialog'; } + /** + * Fired when a reply is successfully sent. + * + * @event send + */ /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when the user presses the cancel button. + * + * @event cancel */ - class GrReplyDialog extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-reply-dialog'; } + + /** + * Fired when the main textarea's value changes, which may have triggered + * a change in size for the dialog. + * + * @event autogrow + */ + + /** + * Fires to show an alert when a send is attempted on the non-latest patch. + * + * @event show-alert + */ + + /** + * Fires when the reply dialog believes that the server side diff drafts + * have been updated and need to be refreshed. + * + * @event comment-refresh + */ + + /** + * Fires when the state of the send button (enabled/disabled) changes. + * + * @event send-disabled-changed + */ + + constructor() { + super(); + this.FocusTarget = FocusTarget; + } + + static get properties() { + return { /** - * Fired when a reply is successfully sent. - * - * @event send + * @type {{ _number: number, removable_reviewers: Array }} */ - - /** - * Fired when the user presses the cancel button. - * - * @event cancel - */ - - /** - * Fired when the main textarea's value changes, which may have triggered - * a change in size for the dialog. - * - * @event autogrow - */ - - /** - * Fires to show an alert when a send is attempted on the non-latest patch. - * - * @event show-alert - */ - - /** - * Fires when the reply dialog believes that the server side diff drafts - * have been updated and need to be refreshed. - * - * @event comment-refresh - */ - - /** - * Fires when the state of the send button (enabled/disabled) changes. - * - * @event send-disabled-changed - */ - - constructor() { - super(); - this.FocusTarget = FocusTarget; - } - - static get properties() { - return { + change: Object, + patchNum: String, + canBeStarted: { + type: Boolean, + value: false, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + draft: { + type: String, + value: '', + observer: '_draftChanged', + }, + quote: { + type: String, + value: '', + }, + /** @type {!Function} */ + filterReviewerSuggestion: { + type: Function, + value() { + return this._filterReviewerSuggestionGenerator(false); + }, + }, + /** @type {!Function} */ + filterCCSuggestion: { + type: Function, + value() { + return this._filterReviewerSuggestionGenerator(true); + }, + }, + permittedLabels: Object, /** - * @type {{ _number: number, removable_reviewers: Array }} + * @type {{ commentlinks: Array }} */ - change: Object, - patchNum: String, - canBeStarted: { - type: Boolean, - value: false, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - draft: { - type: String, - value: '', - observer: '_draftChanged', - }, - quote: { - type: String, - value: '', - }, - /** @type {!Function} */ - filterReviewerSuggestion: { - type: Function, - value() { - return this._filterReviewerSuggestionGenerator(false); - }, - }, - /** @type {!Function} */ - filterCCSuggestion: { - type: Function, - value() { - return this._filterReviewerSuggestionGenerator(true); - }, - }, - permittedLabels: Object, - /** - * @type {{ commentlinks: Array }} - */ - projectConfig: Object, - knownLatestState: String, - underReview: { - type: Boolean, - value: true, - }, + projectConfig: Object, + knownLatestState: String, + underReview: { + type: Boolean, + value: true, + }, - _account: Object, - _ccs: Array, - /** @type {?Object} */ - _ccPendingConfirmation: { - type: Object, - observer: '_reviewerPendingConfirmationUpdated', + _account: Object, + _ccs: Array, + /** @type {?Object} */ + _ccPendingConfirmation: { + type: Object, + observer: '_reviewerPendingConfirmationUpdated', + }, + _messagePlaceholder: { + type: String, + computed: '_computeMessagePlaceholder(canBeStarted)', + }, + _owner: Object, + /** @type {?} */ + _pendingConfirmationDetails: Object, + _includeComments: { + type: Boolean, + value: true, + }, + _reviewers: Array, + /** @type {?Object} */ + _reviewerPendingConfirmation: { + type: Object, + observer: '_reviewerPendingConfirmationUpdated', + }, + _previewFormatting: { + type: Boolean, + value: false, + observer: '_handleHeightChanged', + }, + _reviewersPendingRemove: { + type: Object, + value: { + CC: [], + REVIEWER: [], }, - _messagePlaceholder: { - type: String, - computed: '_computeMessagePlaceholder(canBeStarted)', - }, - _owner: Object, - /** @type {?} */ - _pendingConfirmationDetails: Object, - _includeComments: { - type: Boolean, - value: true, - }, - _reviewers: Array, - /** @type {?Object} */ - _reviewerPendingConfirmation: { - type: Object, - observer: '_reviewerPendingConfirmationUpdated', - }, - _previewFormatting: { - type: Boolean, - value: false, - observer: '_handleHeightChanged', - }, - _reviewersPendingRemove: { - type: Object, - value: { - CC: [], - REVIEWER: [], - }, - }, - _sendButtonLabel: { - type: String, - computed: '_computeSendButtonLabel(canBeStarted)', - }, - _savingComments: Boolean, - _reviewersMutated: { - type: Boolean, - value: false, - }, - _labelsChanged: { - type: Boolean, - value: false, - }, - _saveTooltip: { - type: String, - value: ButtonTooltips.SAVE, - readOnly: true, - }, - _pluginMessage: { - type: String, - value: '', - }, - _sendDisabled: { - type: Boolean, - computed: '_computeSendButtonDisabled(_sendButtonLabel, ' + - 'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' + - '_includeComments, disabled)', - observer: '_sendDisabledChanged', - }, - draftCommentThreads: { - type: Array, - observer: '_handleHeightChanged', - }, - }; - } + }, + _sendButtonLabel: { + type: String, + computed: '_computeSendButtonLabel(canBeStarted)', + }, + _savingComments: Boolean, + _reviewersMutated: { + type: Boolean, + value: false, + }, + _labelsChanged: { + type: Boolean, + value: false, + }, + _saveTooltip: { + type: String, + value: ButtonTooltips.SAVE, + readOnly: true, + }, + _pluginMessage: { + type: String, + value: '', + }, + _sendDisabled: { + type: Boolean, + computed: '_computeSendButtonDisabled(_sendButtonLabel, ' + + 'draftCommentThreads, draft, _reviewersMutated, _labelsChanged, ' + + '_includeComments, disabled)', + observer: '_sendDisabledChanged', + }, + draftCommentThreads: { + type: Array, + observer: '_handleHeightChanged', + }, + }; + } - get keyBindings() { - return { - 'esc': '_handleEscKey', - 'ctrl+enter meta+enter': '_handleEnterKey', - }; - } + get keyBindings() { + return { + 'esc': '_handleEscKey', + 'ctrl+enter meta+enter': '_handleEnterKey', + }; + } - static get observers() { - return [ - '_changeUpdated(change.reviewers.*, change.owner)', - '_ccsChanged(_ccs.splices)', - '_reviewersChanged(_reviewers.splices)', - ]; - } + static get observers() { + return [ + '_changeUpdated(change.reviewers.*, change.owner)', + '_ccsChanged(_ccs.splices)', + '_reviewersChanged(_reviewers.splices)', + ]; + } - /** @override */ - attached() { - super.attached(); - this._getAccount().then(account => { - this._account = account || {}; - }); - } + /** @override */ + attached() { + super.attached(); + this._getAccount().then(account => { + this._account = account || {}; + }); + } - /** @override */ - ready() { - super.ready(); - this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this); - } + /** @override */ + ready() { + super.ready(); + this.$.jsAPI.addElement(this.$.jsAPI.Element.REPLY_DIALOG, this); + } - open(opt_focusTarget) { - this.knownLatestState = LatestPatchState.CHECKING; - this.fetchChangeUpdates(this.change, this.$.restAPI) - .then(result => { - this.knownLatestState = result.isLatest ? - LatestPatchState.LATEST : LatestPatchState.NOT_LATEST; - }); - - this._focusOn(opt_focusTarget); - if (this.quote && this.quote.length) { - // If a reply quote has been provided, use it and clear the property. - this.draft = this.quote; - this.quote = ''; - } else { - // Otherwise, check for an unsaved draft in localstorage. - this.draft = this._loadStoredDraft(); - } - if (this.$.restAPI.hasPendingDiffDrafts()) { - this._savingComments = true; - this.$.restAPI.awaitPendingDiffDrafts().then(() => { - this.fire('comment-refresh'); - this._savingComments = false; + open(opt_focusTarget) { + this.knownLatestState = LatestPatchState.CHECKING; + this.fetchChangeUpdates(this.change, this.$.restAPI) + .then(result => { + this.knownLatestState = result.isLatest ? + LatestPatchState.LATEST : LatestPatchState.NOT_LATEST; }); - } + + this._focusOn(opt_focusTarget); + if (this.quote && this.quote.length) { + // If a reply quote has been provided, use it and clear the property. + this.draft = this.quote; + this.quote = ''; + } else { + // Otherwise, check for an unsaved draft in localstorage. + this.draft = this._loadStoredDraft(); } - - focus() { - this._focusOn(FocusTarget.ANY); - } - - getFocusStops() { - const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton; - return { - start: this.$.reviewers.focusStart, - end, - }; - } - - setLabelValue(label, value) { - const selectorEl = - this.$.labelScores.shadowRoot - .querySelector(`gr-label-score-row[name="${label}"]`); - if (!selectorEl) { return; } - selectorEl.setSelectedValue(value); - } - - getLabelValue(label) { - const selectorEl = - this.$.labelScores.shadowRoot - .querySelector(`gr-label-score-row[name="${label}"]`); - if (!selectorEl) { return null; } - - return selectorEl.selectedValue; - } - - _handleEscKey(e) { - this.cancel(); - } - - _handleEnterKey(e) { - this._submit(); - } - - _ccsChanged(splices) { - this._reviewerTypeChanged(splices, ReviewerTypes.CC); - } - - _reviewersChanged(splices) { - this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER); - } - - _reviewerTypeChanged(splices, reviewerType) { - if (splices && splices.indexSplices) { - this._reviewersMutated = true; - this._processReviewerChange(splices.indexSplices, - reviewerType); - let key; - let index; - let account; - // Remove any accounts that already exist as a CC for reviewer - // or vice versa. - const isReviewer = ReviewerTypes.REVIEWER === reviewerType; - for (const splice of splices.indexSplices) { - for (let i = 0; i < splice.addedCount; i++) { - account = splice.object[splice.index + i]; - key = this._accountOrGroupKey(account); - const array = isReviewer ? this._ccs : this._reviewers; - index = array.findIndex( - account => this._accountOrGroupKey(account) === key); - if (index >= 0) { - this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1); - const moveFrom = isReviewer ? 'CC' : 'reviewer'; - const moveTo = isReviewer ? 'reviewer' : 'CC'; - const message = (account.name || account.email || key) + - ` moved from ${moveFrom} to ${moveTo}.`; - this.fire('show-alert', {message}); - } - } - } - } - } - - _processReviewerChange(indexSplices, type) { - for (const splice of indexSplices) { - for (const account of splice.removed) { - if (!this._reviewersPendingRemove[type]) { - console.err('Invalid type ' + type + ' for reviewer.'); - return; - } - this._reviewersPendingRemove[type].push(account); - } - } - } - - /** - * Resets the state of the _reviewersPendingRemove object, and removes - * accounts if necessary. - * - * @param {boolean} isCancel true if the action is a cancel. - * @param {Object=} opt_accountIdsTransferred map of account IDs that must - * not be removed, because they have been readded in another state. - */ - _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) { - let reviewerArr; - const keep = opt_accountIdsTransferred || {}; - for (const type in this._reviewersPendingRemove) { - if (this._reviewersPendingRemove.hasOwnProperty(type)) { - if (!isCancel) { - reviewerArr = this._reviewersPendingRemove[type]; - for (let i = 0; i < reviewerArr.length; i++) { - if (!keep[reviewerArr[i]._account_id]) { - this._removeAccount(reviewerArr[i], type); - } - } - } - this._reviewersPendingRemove[type] = []; - } - } - } - - /** - * Removes an account from the change, both on the backend and the client. - * Does nothing if the account is a pending addition. - * - * @param {!Object} account - * @param {string} type - */ - _removeAccount(account, type) { - if (account._pendingAdd) { return; } - - return this.$.restAPI.removeChangeReviewer(this.change._number, - account._account_id).then(response => { - if (!response.ok) { return response; } - - const reviewers = this.change.reviewers[type] || []; - for (let i = 0; i < reviewers.length; i++) { - if (reviewers[i]._account_id == account._account_id) { - this.splice(`change.reviewers.${type}`, i, 1); - break; - } - } + if (this.$.restAPI.hasPendingDiffDrafts()) { + this._savingComments = true; + this.$.restAPI.awaitPendingDiffDrafts().then(() => { + this.fire('comment-refresh'); + this._savingComments = false; }); } - - _mapReviewer(reviewer) { - let reviewerId; - let confirmed; - if (reviewer.account) { - reviewerId = reviewer.account._account_id || reviewer.account.email; - } else if (reviewer.group) { - reviewerId = reviewer.group.id; - confirmed = reviewer.group.confirmed; - } - return {reviewer: reviewerId, confirmed}; - } - - send(includeComments, startReview) { - this.$.reporting.time(SEND_REPLY_TIMING_LABEL); - const labels = this.$.labelScores.getLabelValues(); - - const obj = { - drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP', - labels, - }; - - if (startReview) { - obj.ready = true; - } - - if (this.draft != null) { - obj.message = this.draft; - } - - const accountAdditions = {}; - obj.reviewers = this.$.reviewers.additions().map(reviewer => { - if (reviewer.account) { - accountAdditions[reviewer.account._account_id] = true; - } - return this._mapReviewer(reviewer); - }); - const ccsEl = this.$.ccs; - if (ccsEl) { - for (let reviewer of ccsEl.additions()) { - if (reviewer.account) { - accountAdditions[reviewer.account._account_id] = true; - } - reviewer = this._mapReviewer(reviewer); - reviewer.state = 'CC'; - obj.reviewers.push(reviewer); - } - } - - this.disabled = true; - - const errFn = this._handle400Error.bind(this); - return this._saveReview(obj, errFn) - .then(response => { - if (!response) { - // Null or undefined response indicates that an error handler - // took responsibility, so just return. - return {}; - } - if (!response.ok) { - this.fire('server-error', {response}); - return {}; - } - - this.draft = ''; - this._includeComments = true; - this.fire('send', null, {bubbles: false}); - return accountAdditions; - }) - .then(result => { - this.disabled = false; - return result; - }) - .catch(err => { - this.disabled = false; - throw err; - }); - } - - _focusOn(section) { - // Safeguard- always want to focus on something. - if (!section || section === FocusTarget.ANY) { - section = this._chooseFocusTarget(); - } - if (section === FocusTarget.BODY) { - const textarea = this.$.textarea; - textarea.async(textarea.getNativeTextarea() - .focus.bind(textarea.getNativeTextarea())); - } else if (section === FocusTarget.REVIEWERS) { - const reviewerEntry = this.$.reviewers.focusStart; - reviewerEntry.async(reviewerEntry.focus); - } else if (section === FocusTarget.CCS) { - const ccEntry = this.$.ccs.focusStart; - ccEntry.async(ccEntry.focus); - } - } - - _chooseFocusTarget() { - // If we are the owner and the reviewers field is empty, focus on that. - if (this._account && this.change && this.change.owner && - this._account._account_id === this.change.owner._account_id && - (!this._reviewers || this._reviewers.length === 0)) { - return FocusTarget.REVIEWERS; - } - - // Default to BODY. - return FocusTarget.BODY; - } - - _handle400Error(response) { - // A call to _saveReview could fail with a server error if erroneous - // reviewers were requested. This is signalled with a 400 Bad Request - // status. The default gr-rest-api-interface error handling would - // result in a large JSON response body being displayed to the user in - // the gr-error-manager toast. - // - // We can modify the error handling behavior by passing this function - // through to restAPI as a custom error handling function. Since we're - // short-circuiting restAPI we can do our own response parsing and fire - // the server-error ourselves. - // - this.disabled = false; - - // Using response.clone() here, because getResponseObject() and - // potentially the generic error handler will want to call text() on the - // response object, which can only be done once per object. - const jsonPromise = this.$.restAPI.getResponseObject(response.clone()); - return jsonPromise.then(result => { - // Only perform custom error handling for 400s and a parseable - // ReviewResult response. - if (response.status === 400 && result) { - const errors = []; - for (const state of ['reviewers', 'ccs']) { - if (!result.hasOwnProperty(state)) { continue; } - for (const reviewer of Object.values(result[state])) { - if (reviewer.error) { - errors.push(reviewer.error); - } - } - } - response = { - ok: false, - status: response.status, - text() { return Promise.resolve(errors.join(', ')); }, - }; - } - this.fire('server-error', {response}); - return null; // Means that the error has been handled. - }); - } - - _computeHideDraftList(draftCommentThreads) { - return draftCommentThreads.length === 0; - } - - _computeDraftsTitle(draftCommentThreads) { - const total = draftCommentThreads.length; - if (total == 0) { return ''; } - if (total == 1) { return '1 Draft'; } - if (total > 1) { return total + ' Drafts'; } - } - - _computeMessagePlaceholder(canBeStarted) { - return canBeStarted ? - 'Add a note for your reviewers...' : - 'Say something nice...'; - } - - _changeUpdated(changeRecord, owner) { - // Polymer 2: check for undefined - if ([changeRecord, owner].some(arg => arg === undefined)) { - return; - } - - this._rebuildReviewerArrays(changeRecord.base, owner); - } - - _rebuildReviewerArrays(change, owner) { - this._owner = owner; - - const reviewers = []; - const ccs = []; - - for (const key in change) { - if (change.hasOwnProperty(key)) { - if (key !== 'REVIEWER' && key !== 'CC') { - console.warn('unexpected reviewer state:', key); - continue; - } - for (const entry of change[key]) { - if (entry._account_id === owner._account_id) { - continue; - } - switch (key) { - case 'REVIEWER': - reviewers.push(entry); - break; - case 'CC': - ccs.push(entry); - break; - } - } - } - } - - this._ccs = ccs; - this._reviewers = reviewers; - } - - _accountOrGroupKey(entry) { - return entry.id || entry._account_id; - } - - /** - * Generates a function to filter out reviewer/CC entries. When isCCs is - * truthy, the function filters out entries that already exist in this._ccs. - * When falsy, the function filters entries that exist in this._reviewers. - * - * @param {boolean} isCCs - * @return {!Function} - */ - _filterReviewerSuggestionGenerator(isCCs) { - return suggestion => { - let entry; - if (suggestion.account) { - entry = suggestion.account; - } else if (suggestion.group) { - entry = suggestion.group; - } else { - console.warn( - 'received suggestion that was neither account nor group:', - suggestion); - } - if (entry._account_id === this._owner._account_id) { - return false; - } - - const key = this._accountOrGroupKey(entry); - const finder = entry => this._accountOrGroupKey(entry) === key; - if (isCCs) { - return this._ccs.find(finder) === undefined; - } - return this._reviewers.find(finder) === undefined; - }; - } - - _getAccount() { - return this.$.restAPI.getAccount(); - } - - _cancelTapHandler(e) { - e.preventDefault(); - this.cancel(); - } - - cancel() { - this.fire('cancel', null, {bubbles: false}); - this.$.textarea.closeDropdown(); - this._purgeReviewersPendingRemove(true); - this._rebuildReviewerArrays(this.change.reviewers, this._owner); - } - - _saveClickHandler(e) { - e.preventDefault(); - if (!this.$.ccs.submitEntryText()) { - // Do not proceed with the save if there is an invalid email entry in - // the text field of the CC entry. - return; - } - this.send(this._includeComments, false).then(keepReviewers => { - this._purgeReviewersPendingRemove(false, keepReviewers); - }); - } - - _sendTapHandler(e) { - e.preventDefault(); - this._submit(); - } - - _submit() { - if (!this.$.ccs.submitEntryText()) { - // Do not proceed with the send if there is an invalid email entry in - // the text field of the CC entry. - return; - } - if (this._sendDisabled) { - this.dispatchEvent(new CustomEvent('show-alert', { - bubbles: true, - composed: true, - detail: {message: EMPTY_REPLY_MESSAGE}, - })); - return; - } - return this.send(this._includeComments, this.canBeStarted) - .then(keepReviewers => { - this._purgeReviewersPendingRemove(false, keepReviewers); - }) - .catch(err => { - this.dispatchEvent(new CustomEvent('show-error', { - bubbles: true, - composed: true, - detail: {message: `Error submitting review ${err}`}, - })); - }); - } - - _saveReview(review, opt_errFn) { - return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum, - review, opt_errFn); - } - - _reviewerPendingConfirmationUpdated(reviewer) { - if (reviewer === null) { - this.$.reviewerConfirmationOverlay.close(); - } else { - this._pendingConfirmationDetails = - this._ccPendingConfirmation || this._reviewerPendingConfirmation; - this.$.reviewerConfirmationOverlay.open(); - } - } - - _confirmPendingReviewer() { - if (this._ccPendingConfirmation) { - this.$.ccs.confirmGroup(this._ccPendingConfirmation.group); - this._focusOn(FocusTarget.CCS); - } else { - this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group); - this._focusOn(FocusTarget.REVIEWERS); - } - } - - _cancelPendingReviewer() { - this._ccPendingConfirmation = null; - this._reviewerPendingConfirmation = null; - - const target = - this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS; - this._focusOn(target); - } - - _getStorageLocation() { - // Tests trigger this method without setting change. - if (!this.change) { return {}; } - return { - changeNum: this.change._number, - patchNum: '@change', - path: '@change', - }; - } - - _loadStoredDraft() { - const draft = this.$.storage.getDraftComment(this._getStorageLocation()); - return draft ? draft.message : ''; - } - - _handleAccountTextEntry() { - // When either of the account entries has input added to the autocomplete, - // it should trigger the save button to enable/ - // - // Note: if the text is removed, the save button will not get disabled. - this._reviewersMutated = true; - } - - _draftChanged(newDraft, oldDraft) { - this.debounce('store', () => { - if (!newDraft.length && oldDraft) { - // If the draft has been modified to be empty, then erase the storage - // entry. - this.$.storage.eraseDraftComment(this._getStorageLocation()); - } else if (newDraft.length) { - this.$.storage.setDraftComment(this._getStorageLocation(), - this.draft); - } - }, STORAGE_DEBOUNCE_INTERVAL_MS); - } - - _handleHeightChanged(e) { - this.fire('autogrow'); - } - - _handleLabelsChanged() { - this._labelsChanged = Object.keys( - this.$.labelScores.getLabelValues()).length !== 0; - } - - _isState(knownLatestState, value) { - return knownLatestState === value; - } - - _reload() { - // Load the current change without any patch range. - Gerrit.Nav.navigateToChange(this.change); - this.cancel(); - } - - _computeSendButtonLabel(canBeStarted) { - return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND; - } - - _computeSendButtonTooltip(canBeStarted) { - return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND; - } - - _computeSavingLabelClass(savingComments) { - return savingComments ? 'saving' : ''; - } - - _computeSendButtonDisabled( - buttonLabel, draftCommentThreads, text, reviewersMutated, - labelsChanged, includeComments, disabled) { - // Polymer 2: check for undefined - if ([ - buttonLabel, - draftCommentThreads, - text, - reviewersMutated, - labelsChanged, - includeComments, - disabled, - ].some(arg => arg === undefined)) { - return undefined; - } - - if (disabled) { return true; } - if (buttonLabel === ButtonLabels.START_REVIEW) { return false; } - const hasDrafts = includeComments && draftCommentThreads.length; - return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged; - } - - _computePatchSetWarning(patchNum, labelsChanged) { - let str = `Patch ${patchNum} is not latest.`; - if (labelsChanged) { - str += ' Voting on a non-latest patch will have no effect.'; - } - return str; - } - - setPluginMessage(message) { - this._pluginMessage = message; - } - - _sendDisabledChanged(sendDisabled) { - this.dispatchEvent(new CustomEvent('send-disabled-changed')); - } - - _getReviewerSuggestionsProvider(change) { - const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, - change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER); - provider.init(); - return provider; - } - - _getCcSuggestionsProvider(change) { - const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, - change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC); - provider.init(); - return provider; - } - - _onThreadListModified() { - // TODO(taoalpha): this won't propogate the changes to the files - // should consider replacing this with either top level events - // or gerrit level events - - // emit the event so change-view can also get updated with latest changes - this.fire('comment-refresh'); - } } - customElements.define(GrReplyDialog.is, GrReplyDialog); -})(); + focus() { + this._focusOn(FocusTarget.ANY); + } + + getFocusStops() { + const end = this._sendDisabled ? this.$.cancelButton : this.$.sendButton; + return { + start: this.$.reviewers.focusStart, + end, + }; + } + + setLabelValue(label, value) { + const selectorEl = + this.$.labelScores.shadowRoot + .querySelector(`gr-label-score-row[name="${label}"]`); + if (!selectorEl) { return; } + selectorEl.setSelectedValue(value); + } + + getLabelValue(label) { + const selectorEl = + this.$.labelScores.shadowRoot + .querySelector(`gr-label-score-row[name="${label}"]`); + if (!selectorEl) { return null; } + + return selectorEl.selectedValue; + } + + _handleEscKey(e) { + this.cancel(); + } + + _handleEnterKey(e) { + this._submit(); + } + + _ccsChanged(splices) { + this._reviewerTypeChanged(splices, ReviewerTypes.CC); + } + + _reviewersChanged(splices) { + this._reviewerTypeChanged(splices, ReviewerTypes.REVIEWER); + } + + _reviewerTypeChanged(splices, reviewerType) { + if (splices && splices.indexSplices) { + this._reviewersMutated = true; + this._processReviewerChange(splices.indexSplices, + reviewerType); + let key; + let index; + let account; + // Remove any accounts that already exist as a CC for reviewer + // or vice versa. + const isReviewer = ReviewerTypes.REVIEWER === reviewerType; + for (const splice of splices.indexSplices) { + for (let i = 0; i < splice.addedCount; i++) { + account = splice.object[splice.index + i]; + key = this._accountOrGroupKey(account); + const array = isReviewer ? this._ccs : this._reviewers; + index = array.findIndex( + account => this._accountOrGroupKey(account) === key); + if (index >= 0) { + this.splice(isReviewer ? '_ccs' : '_reviewers', index, 1); + const moveFrom = isReviewer ? 'CC' : 'reviewer'; + const moveTo = isReviewer ? 'reviewer' : 'CC'; + const message = (account.name || account.email || key) + + ` moved from ${moveFrom} to ${moveTo}.`; + this.fire('show-alert', {message}); + } + } + } + } + } + + _processReviewerChange(indexSplices, type) { + for (const splice of indexSplices) { + for (const account of splice.removed) { + if (!this._reviewersPendingRemove[type]) { + console.err('Invalid type ' + type + ' for reviewer.'); + return; + } + this._reviewersPendingRemove[type].push(account); + } + } + } + + /** + * Resets the state of the _reviewersPendingRemove object, and removes + * accounts if necessary. + * + * @param {boolean} isCancel true if the action is a cancel. + * @param {Object=} opt_accountIdsTransferred map of account IDs that must + * not be removed, because they have been readded in another state. + */ + _purgeReviewersPendingRemove(isCancel, opt_accountIdsTransferred) { + let reviewerArr; + const keep = opt_accountIdsTransferred || {}; + for (const type in this._reviewersPendingRemove) { + if (this._reviewersPendingRemove.hasOwnProperty(type)) { + if (!isCancel) { + reviewerArr = this._reviewersPendingRemove[type]; + for (let i = 0; i < reviewerArr.length; i++) { + if (!keep[reviewerArr[i]._account_id]) { + this._removeAccount(reviewerArr[i], type); + } + } + } + this._reviewersPendingRemove[type] = []; + } + } + } + + /** + * Removes an account from the change, both on the backend and the client. + * Does nothing if the account is a pending addition. + * + * @param {!Object} account + * @param {string} type + */ + _removeAccount(account, type) { + if (account._pendingAdd) { return; } + + return this.$.restAPI.removeChangeReviewer(this.change._number, + account._account_id).then(response => { + if (!response.ok) { return response; } + + const reviewers = this.change.reviewers[type] || []; + for (let i = 0; i < reviewers.length; i++) { + if (reviewers[i]._account_id == account._account_id) { + this.splice(`change.reviewers.${type}`, i, 1); + break; + } + } + }); + } + + _mapReviewer(reviewer) { + let reviewerId; + let confirmed; + if (reviewer.account) { + reviewerId = reviewer.account._account_id || reviewer.account.email; + } else if (reviewer.group) { + reviewerId = reviewer.group.id; + confirmed = reviewer.group.confirmed; + } + return {reviewer: reviewerId, confirmed}; + } + + send(includeComments, startReview) { + this.$.reporting.time(SEND_REPLY_TIMING_LABEL); + const labels = this.$.labelScores.getLabelValues(); + + const obj = { + drafts: includeComments ? 'PUBLISH_ALL_REVISIONS' : 'KEEP', + labels, + }; + + if (startReview) { + obj.ready = true; + } + + if (this.draft != null) { + obj.message = this.draft; + } + + const accountAdditions = {}; + obj.reviewers = this.$.reviewers.additions().map(reviewer => { + if (reviewer.account) { + accountAdditions[reviewer.account._account_id] = true; + } + return this._mapReviewer(reviewer); + }); + const ccsEl = this.$.ccs; + if (ccsEl) { + for (let reviewer of ccsEl.additions()) { + if (reviewer.account) { + accountAdditions[reviewer.account._account_id] = true; + } + reviewer = this._mapReviewer(reviewer); + reviewer.state = 'CC'; + obj.reviewers.push(reviewer); + } + } + + this.disabled = true; + + const errFn = this._handle400Error.bind(this); + return this._saveReview(obj, errFn) + .then(response => { + if (!response) { + // Null or undefined response indicates that an error handler + // took responsibility, so just return. + return {}; + } + if (!response.ok) { + this.fire('server-error', {response}); + return {}; + } + + this.draft = ''; + this._includeComments = true; + this.fire('send', null, {bubbles: false}); + return accountAdditions; + }) + .then(result => { + this.disabled = false; + return result; + }) + .catch(err => { + this.disabled = false; + throw err; + }); + } + + _focusOn(section) { + // Safeguard- always want to focus on something. + if (!section || section === FocusTarget.ANY) { + section = this._chooseFocusTarget(); + } + if (section === FocusTarget.BODY) { + const textarea = this.$.textarea; + textarea.async(textarea.getNativeTextarea() + .focus.bind(textarea.getNativeTextarea())); + } else if (section === FocusTarget.REVIEWERS) { + const reviewerEntry = this.$.reviewers.focusStart; + reviewerEntry.async(reviewerEntry.focus); + } else if (section === FocusTarget.CCS) { + const ccEntry = this.$.ccs.focusStart; + ccEntry.async(ccEntry.focus); + } + } + + _chooseFocusTarget() { + // If we are the owner and the reviewers field is empty, focus on that. + if (this._account && this.change && this.change.owner && + this._account._account_id === this.change.owner._account_id && + (!this._reviewers || this._reviewers.length === 0)) { + return FocusTarget.REVIEWERS; + } + + // Default to BODY. + return FocusTarget.BODY; + } + + _handle400Error(response) { + // A call to _saveReview could fail with a server error if erroneous + // reviewers were requested. This is signalled with a 400 Bad Request + // status. The default gr-rest-api-interface error handling would + // result in a large JSON response body being displayed to the user in + // the gr-error-manager toast. + // + // We can modify the error handling behavior by passing this function + // through to restAPI as a custom error handling function. Since we're + // short-circuiting restAPI we can do our own response parsing and fire + // the server-error ourselves. + // + this.disabled = false; + + // Using response.clone() here, because getResponseObject() and + // potentially the generic error handler will want to call text() on the + // response object, which can only be done once per object. + const jsonPromise = this.$.restAPI.getResponseObject(response.clone()); + return jsonPromise.then(result => { + // Only perform custom error handling for 400s and a parseable + // ReviewResult response. + if (response.status === 400 && result) { + const errors = []; + for (const state of ['reviewers', 'ccs']) { + if (!result.hasOwnProperty(state)) { continue; } + for (const reviewer of Object.values(result[state])) { + if (reviewer.error) { + errors.push(reviewer.error); + } + } + } + response = { + ok: false, + status: response.status, + text() { return Promise.resolve(errors.join(', ')); }, + }; + } + this.fire('server-error', {response}); + return null; // Means that the error has been handled. + }); + } + + _computeHideDraftList(draftCommentThreads) { + return draftCommentThreads.length === 0; + } + + _computeDraftsTitle(draftCommentThreads) { + const total = draftCommentThreads.length; + if (total == 0) { return ''; } + if (total == 1) { return '1 Draft'; } + if (total > 1) { return total + ' Drafts'; } + } + + _computeMessagePlaceholder(canBeStarted) { + return canBeStarted ? + 'Add a note for your reviewers...' : + 'Say something nice...'; + } + + _changeUpdated(changeRecord, owner) { + // Polymer 2: check for undefined + if ([changeRecord, owner].some(arg => arg === undefined)) { + return; + } + + this._rebuildReviewerArrays(changeRecord.base, owner); + } + + _rebuildReviewerArrays(change, owner) { + this._owner = owner; + + const reviewers = []; + const ccs = []; + + for (const key in change) { + if (change.hasOwnProperty(key)) { + if (key !== 'REVIEWER' && key !== 'CC') { + console.warn('unexpected reviewer state:', key); + continue; + } + for (const entry of change[key]) { + if (entry._account_id === owner._account_id) { + continue; + } + switch (key) { + case 'REVIEWER': + reviewers.push(entry); + break; + case 'CC': + ccs.push(entry); + break; + } + } + } + } + + this._ccs = ccs; + this._reviewers = reviewers; + } + + _accountOrGroupKey(entry) { + return entry.id || entry._account_id; + } + + /** + * Generates a function to filter out reviewer/CC entries. When isCCs is + * truthy, the function filters out entries that already exist in this._ccs. + * When falsy, the function filters entries that exist in this._reviewers. + * + * @param {boolean} isCCs + * @return {!Function} + */ + _filterReviewerSuggestionGenerator(isCCs) { + return suggestion => { + let entry; + if (suggestion.account) { + entry = suggestion.account; + } else if (suggestion.group) { + entry = suggestion.group; + } else { + console.warn( + 'received suggestion that was neither account nor group:', + suggestion); + } + if (entry._account_id === this._owner._account_id) { + return false; + } + + const key = this._accountOrGroupKey(entry); + const finder = entry => this._accountOrGroupKey(entry) === key; + if (isCCs) { + return this._ccs.find(finder) === undefined; + } + return this._reviewers.find(finder) === undefined; + }; + } + + _getAccount() { + return this.$.restAPI.getAccount(); + } + + _cancelTapHandler(e) { + e.preventDefault(); + this.cancel(); + } + + cancel() { + this.fire('cancel', null, {bubbles: false}); + this.$.textarea.closeDropdown(); + this._purgeReviewersPendingRemove(true); + this._rebuildReviewerArrays(this.change.reviewers, this._owner); + } + + _saveClickHandler(e) { + e.preventDefault(); + if (!this.$.ccs.submitEntryText()) { + // Do not proceed with the save if there is an invalid email entry in + // the text field of the CC entry. + return; + } + this.send(this._includeComments, false).then(keepReviewers => { + this._purgeReviewersPendingRemove(false, keepReviewers); + }); + } + + _sendTapHandler(e) { + e.preventDefault(); + this._submit(); + } + + _submit() { + if (!this.$.ccs.submitEntryText()) { + // Do not proceed with the send if there is an invalid email entry in + // the text field of the CC entry. + return; + } + if (this._sendDisabled) { + this.dispatchEvent(new CustomEvent('show-alert', { + bubbles: true, + composed: true, + detail: {message: EMPTY_REPLY_MESSAGE}, + })); + return; + } + return this.send(this._includeComments, this.canBeStarted) + .then(keepReviewers => { + this._purgeReviewersPendingRemove(false, keepReviewers); + }) + .catch(err => { + this.dispatchEvent(new CustomEvent('show-error', { + bubbles: true, + composed: true, + detail: {message: `Error submitting review ${err}`}, + })); + }); + } + + _saveReview(review, opt_errFn) { + return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum, + review, opt_errFn); + } + + _reviewerPendingConfirmationUpdated(reviewer) { + if (reviewer === null) { + this.$.reviewerConfirmationOverlay.close(); + } else { + this._pendingConfirmationDetails = + this._ccPendingConfirmation || this._reviewerPendingConfirmation; + this.$.reviewerConfirmationOverlay.open(); + } + } + + _confirmPendingReviewer() { + if (this._ccPendingConfirmation) { + this.$.ccs.confirmGroup(this._ccPendingConfirmation.group); + this._focusOn(FocusTarget.CCS); + } else { + this.$.reviewers.confirmGroup(this._reviewerPendingConfirmation.group); + this._focusOn(FocusTarget.REVIEWERS); + } + } + + _cancelPendingReviewer() { + this._ccPendingConfirmation = null; + this._reviewerPendingConfirmation = null; + + const target = + this._ccPendingConfirmation ? FocusTarget.CCS : FocusTarget.REVIEWERS; + this._focusOn(target); + } + + _getStorageLocation() { + // Tests trigger this method without setting change. + if (!this.change) { return {}; } + return { + changeNum: this.change._number, + patchNum: '@change', + path: '@change', + }; + } + + _loadStoredDraft() { + const draft = this.$.storage.getDraftComment(this._getStorageLocation()); + return draft ? draft.message : ''; + } + + _handleAccountTextEntry() { + // When either of the account entries has input added to the autocomplete, + // it should trigger the save button to enable/ + // + // Note: if the text is removed, the save button will not get disabled. + this._reviewersMutated = true; + } + + _draftChanged(newDraft, oldDraft) { + this.debounce('store', () => { + if (!newDraft.length && oldDraft) { + // If the draft has been modified to be empty, then erase the storage + // entry. + this.$.storage.eraseDraftComment(this._getStorageLocation()); + } else if (newDraft.length) { + this.$.storage.setDraftComment(this._getStorageLocation(), + this.draft); + } + }, STORAGE_DEBOUNCE_INTERVAL_MS); + } + + _handleHeightChanged(e) { + this.fire('autogrow'); + } + + _handleLabelsChanged() { + this._labelsChanged = Object.keys( + this.$.labelScores.getLabelValues()).length !== 0; + } + + _isState(knownLatestState, value) { + return knownLatestState === value; + } + + _reload() { + // Load the current change without any patch range. + Gerrit.Nav.navigateToChange(this.change); + this.cancel(); + } + + _computeSendButtonLabel(canBeStarted) { + return canBeStarted ? ButtonLabels.START_REVIEW : ButtonLabels.SEND; + } + + _computeSendButtonTooltip(canBeStarted) { + return canBeStarted ? ButtonTooltips.START_REVIEW : ButtonTooltips.SEND; + } + + _computeSavingLabelClass(savingComments) { + return savingComments ? 'saving' : ''; + } + + _computeSendButtonDisabled( + buttonLabel, draftCommentThreads, text, reviewersMutated, + labelsChanged, includeComments, disabled) { + // Polymer 2: check for undefined + if ([ + buttonLabel, + draftCommentThreads, + text, + reviewersMutated, + labelsChanged, + includeComments, + disabled, + ].some(arg => arg === undefined)) { + return undefined; + } + + if (disabled) { return true; } + if (buttonLabel === ButtonLabels.START_REVIEW) { return false; } + const hasDrafts = includeComments && draftCommentThreads.length; + return !hasDrafts && !text.length && !reviewersMutated && !labelsChanged; + } + + _computePatchSetWarning(patchNum, labelsChanged) { + let str = `Patch ${patchNum} is not latest.`; + if (labelsChanged) { + str += ' Voting on a non-latest patch will have no effect.'; + } + return str; + } + + setPluginMessage(message) { + this._pluginMessage = message; + } + + _sendDisabledChanged(sendDisabled) { + this.dispatchEvent(new CustomEvent('send-disabled-changed')); + } + + _getReviewerSuggestionsProvider(change) { + const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, + change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER); + provider.init(); + return provider; + } + + _getCcSuggestionsProvider(change) { + const provider = GrReviewerSuggestionsProvider.create(this.$.restAPI, + change._number, Gerrit.SUGGESTIONS_PROVIDERS_USERS_TYPES.CC); + provider.init(); + return provider; + } + + _onThreadListModified() { + // TODO(taoalpha): this won't propogate the changes to the files + // should consider replacing this with either top level events + // or gerrit level events + + // emit the event so change-view can also get updated with latest changes + this.fire('comment-refresh'); + } +} + +customElements.define(GrReplyDialog.is, GrReplyDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js index 8424b5d..0c98834 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_html.js
@@ -1,47 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../../shared/gr-textarea/gr-textarea.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-storage/gr-storage.html"> -<link rel="import" href="../../shared/gr-account-list/gr-account-list.html"> -<link rel="import" href="../gr-label-scores/gr-label-scores.html"> -<link rel="import" href="../gr-thread-list/gr-thread-list.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../change/gr-comment-list/gr-comment-list.html"> -<script src="../../../scripts/gr-display-name-utils/gr-display-name-utils.js"></script> -<script src="../../../scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider.js"></script> - -<dom-module id="gr-reply-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background-color: var(--dialog-background-color); @@ -167,33 +142,15 @@ <section class="peopleContainer"> <div class="peopleList"> <div class="peopleListLabel">Reviewers</div> - <gr-account-list - id="reviewers" - accounts="{{_reviewers}}" - removable-values="[[change.removable_reviewers]]" - filter="[[filterReviewerSuggestion]]" - pending-confirmation="{{_reviewerPendingConfirmation}}" - placeholder="Add reviewer..." - on-account-text-changed="_handleAccountTextEntry" - suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"> + <gr-account-list id="reviewers" accounts="{{_reviewers}}" removable-values="[[change.removable_reviewers]]" filter="[[filterReviewerSuggestion]]" pending-confirmation="{{_reviewerPendingConfirmation}}" placeholder="Add reviewer..." on-account-text-changed="_handleAccountTextEntry" suggestions-provider="[[_getReviewerSuggestionsProvider(change)]]"> </gr-account-list> </div> <div class="peopleList"> <div class="peopleListLabel">CC</div> - <gr-account-list - id="ccs" - accounts="{{_ccs}}" - filter="[[filterCCSuggestion]]" - pending-confirmation="{{_ccPendingConfirmation}}" - allow-any-input - placeholder="Add CC..." - on-account-text-changed="_handleAccountTextEntry" - suggestions-provider="[[_getCcSuggestionsProvider(change)]]"> + <gr-account-list id="ccs" accounts="{{_ccs}}" filter="[[filterCCSuggestion]]" pending-confirmation="{{_ccPendingConfirmation}}" allow-any-input="" placeholder="Add CC..." on-account-text-changed="_handleAccountTextEntry" suggestions-provider="[[_getCcSuggestionsProvider(change)]]"> </gr-account-list> </div> - <gr-overlay - id="reviewerConfirmationOverlay" - on-iron-overlay-canceled="_cancelPendingReviewer"> + <gr-overlay id="reviewerConfirmationOverlay" on-iron-overlay-canceled="_cancelPendingReviewer"> <div class="reviewerConfirmation"> Group <span class="groupName"> @@ -215,18 +172,7 @@ </section> <section class="textareaContainer"> <gr-endpoint-decorator name="reply-text"> - <gr-textarea - id="textarea" - class="message" - autocomplete="on" - placeholder=[[_messagePlaceholder]] - fixed-position-dropdown - hide-border="true" - monospace="true" - disabled="{{disabled}}" - rows="4" - text="{{draft}}" - on-bind-value-changed="_handleHeightChanged"> + <gr-textarea id="textarea" class="message" autocomplete="on" placeholder="[[_messagePlaceholder]]" fixed-position-dropdown="" hide-border="true" monospace="true" disabled="{{disabled}}" rows="4" text="{{draft}}" on-bind-value-changed="_handleHeightChanged"> </gr-textarea> </gr-endpoint-decorator> </section> @@ -235,84 +181,44 @@ <input type="checkbox" checked="{{_previewFormatting::change}}"> Preview formatting </label> - <gr-formatted-text - content="[[draft]]" - hidden$="[[!_previewFormatting]]" - config="[[projectConfig.commentlinks]]"></gr-formatted-text> + <gr-formatted-text content="[[draft]]" hidden\$="[[!_previewFormatting]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text> </section> <section class="labelsContainer"> <gr-endpoint-decorator name="reply-label-scores"> - <gr-label-scores - id="labelScores" - account="[[_account]]" - change="[[change]]" - on-labels-changed="_handleLabelsChanged" - permitted-labels=[[permittedLabels]]></gr-label-scores> + <gr-label-scores id="labelScores" account="[[_account]]" change="[[change]]" on-labels-changed="_handleLabelsChanged" permitted-labels="[[permittedLabels]]"></gr-label-scores> </gr-endpoint-decorator> <div id="pluginMessage">[[_pluginMessage]]</div> </section> - <section class="draftsContainer" hidden$="[[_computeHideDraftList(draftCommentThreads)]]"> + <section class="draftsContainer" hidden\$="[[_computeHideDraftList(draftCommentThreads)]]"> <div class="includeComments"> - <input type="checkbox" id="includeComments" - checked="{{_includeComments::change}}"> + <input type="checkbox" id="includeComments" checked="{{_includeComments::change}}"> <label for="includeComments">Publish [[_computeDraftsTitle(draftCommentThreads)]]</label> </div> - <gr-thread-list - id="commentList" - hidden$="[[!_includeComments]]" - threads="[[draftCommentThreads]]" - change="[[change]]" - change-num="[[change._number]]" - logged-in="true" - hide-toggle-buttons - on-thread-list-modified="_onThreadListModified"> + <gr-thread-list id="commentList" hidden\$="[[!_includeComments]]" threads="[[draftCommentThreads]]" change="[[change]]" change-num="[[change._number]]" logged-in="true" hide-toggle-buttons="" on-thread-list-modified="_onThreadListModified"> </gr-thread-list> - <span - id="savingLabel" - class$="[[_computeSavingLabelClass(_savingComments)]]"> + <span id="savingLabel" class\$="[[_computeSavingLabelClass(_savingComments)]]"> Saving comments... </span> </section> <section class="actions"> <div class="left"> - <span - id="checkingStatusLabel" - hidden$="[[!_isState(knownLatestState, 'checking')]]"> + <span id="checkingStatusLabel" hidden\$="[[!_isState(knownLatestState, 'checking')]]"> Checking whether patch [[patchNum]] is latest... </span> - <span - id="notLatestLabel" - hidden$="[[!_isState(knownLatestState, 'not-latest')]]"> + <span id="notLatestLabel" hidden\$="[[!_isState(knownLatestState, 'not-latest')]]"> [[_computePatchSetWarning(patchNum, _labelsChanged)]] - <gr-button link on-click="_reload">Reload</gr-button> + <gr-button link="" on-click="_reload">Reload</gr-button> </span> </div> <div class="right"> - <gr-button - link - id="cancelButton" - class="action cancel" - on-click="_cancelTapHandler">Cancel</gr-button> + <gr-button link="" id="cancelButton" class="action cancel" on-click="_cancelTapHandler">Cancel</gr-button> <template is="dom-if" if="[[canBeStarted]]"> <!-- Use 'Send' here as the change may only about reviewers / ccs and when this button is visible, the next button will always be 'Start review' --> - <gr-button - link - disabled="[[_isState(knownLatestState, 'not-latest')]]" - class="action save" - has-tooltip - title="[[_saveTooltip]]" - on-click="_saveClickHandler">Save</gr-button> + <gr-button link="" disabled="[[_isState(knownLatestState, 'not-latest')]]" class="action save" has-tooltip="" title="[[_saveTooltip]]" on-click="_saveClickHandler">Save</gr-button> </template> - <gr-button - id="sendButton" - primary - disabled="[[_sendDisabled]]" - class="action send" - has-tooltip - title$="[[_computeSendButtonTooltip(canBeStarted)]]" - on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button> + <gr-button id="sendButton" primary="" disabled="[[_sendDisabled]]" class="action send" has-tooltip="" title\$="[[_computeSendButtonTooltip(canBeStarted)]]" on-click="_sendTapHandler">[[_sendButtonLabel]]</gr-button> </div> </section> </div> @@ -320,6 +226,4 @@ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-reply-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html index b7c2330..5257ef46 100644 --- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -19,16 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reply-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-manager.html"> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-reply-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-reply-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reply-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,1196 +40,1199 @@ </template> </test-fixture> -<script> - function cloneableResponse(status, text) { - return { - ok: false, - status, - text() { - return Promise.resolve(text); - }, - clone() { - return { - ok: false, - status, - text() { - return Promise.resolve(text); - }, - }; - }, - }; - } - - suite('gr-reply-dialog tests', async () => { - await readyToTest(); - let element; - let changeNum; - let patchNum; - - let sandbox; - let getDraftCommentStub; - let setDraftCommentStub; - let eraseDraftCommentStub; - - let lastId = 0; - const makeAccount = function() { return {_account_id: lastId++}; }; - const makeGroup = function() { return {id: lastId++}; }; - - setup(() => { - sandbox = sinon.sandbox.create(); - - changeNum = 42; - patchNum = 1; - - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getAccount() { return Promise.resolve({}); }, - getChange() { return Promise.resolve({}); }, - getChangeSuggestedReviewers() { return Promise.resolve([]); }, - }); - - element = fixture('basic'); - element.change = { - _number: changeNum, - labels: { - 'Verified': { - values: { - '-1': 'Fails', - ' 0': 'No score', - '+1': 'Verified', - }, - default_value: 0, - }, - 'Code-Review': { - values: { - '-2': 'Do not submit', - '-1': 'I would prefer that you didn\'t submit this', - ' 0': 'No score', - '+1': 'Looks good to me, but someone else must approve', - '+2': 'Looks good to me, approved', - }, - default_value: 0, - }, +<script type="module"> +import '../../../test/test-pre-setup.js'; +import {IronOverlayManager} from '@polymer/iron-overlay-behavior/iron-overlay-manager.js'; +import '../../../test/common-test-setup.js'; +import './gr-reply-dialog.js'; +function cloneableResponse(status, text) { + return { + ok: false, + status, + text() { + return Promise.resolve(text); + }, + clone() { + return { + ok: false, + status, + text() { + return Promise.resolve(text); }, }; - element.patchNum = patchNum; - element.permittedLabels = { - 'Code-Review': [ - '-1', - ' 0', - '+1', - ], - 'Verified': [ - '-1', - ' 0', - '+1', - ], - }; + }, + }; +} - getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment'); - setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment'); - eraseDraftCommentStub = sandbox.stub(element.$.storage, - 'eraseDraftComment'); +suite('gr-reply-dialog tests', () => { + let element; + let changeNum; + let patchNum; - sandbox.stub(element, 'fetchChangeUpdates') - .returns(Promise.resolve({isLatest: true})); + let sandbox; + let getDraftCommentStub; + let setDraftCommentStub; + let eraseDraftCommentStub; - // Allow the elements created by dom-repeat to be stamped. - flushAsynchronousOperations(); + let lastId = 0; + const makeAccount = function() { return {_account_id: lastId++}; }; + const makeGroup = function() { return {id: lastId++}; }; + + setup(() => { + sandbox = sinon.sandbox.create(); + + changeNum = 42; + patchNum = 1; + + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getAccount() { return Promise.resolve({}); }, + getChange() { return Promise.resolve({}); }, + getChangeSuggestedReviewers() { return Promise.resolve([]); }, }); - teardown(() => { - sandbox.restore(); - }); - - function stubSaveReview(jsonResponseProducer) { - return sandbox.stub( - element, - '_saveReview', - review => new Promise((resolve, reject) => { - try { - const result = jsonResponseProducer(review) || {}; - const resultStr = - element.$.restAPI.JSON_PREFIX + JSON.stringify(result); - resolve({ - ok: true, - text() { - return Promise.resolve(resultStr); - }, - }); - } catch (err) { - reject(err); - } - })); - } - - test('default to publishing draft comments with reply', done => { - // Async tick is needed because iron-selector content is distributed and - // distributed content requires an observer to be set up. - // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. - flush(() => { - flush(() => { - element.draft = 'I wholeheartedly disapprove'; - - stubSaveReview(review => { - assert.deepEqual(review, { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: { - 'Code-Review': 0, - 'Verified': 0, - }, - message: 'I wholeheartedly disapprove', - reviewers: [], - }); - assert.isFalse(element.$.commentList.hidden); - done(); - }); - - // This is needed on non-Blink engines most likely due to the ways in - // which the dom-repeat elements are stamped. - flush(() => { - MockInteractions.tap(element.shadowRoot - .querySelector('.send')); - }); - }); - }); - }); - - test('keep draft comments with reply', done => { - MockInteractions.tap(element.shadowRoot.querySelector('#includeComments')); - assert.equal(element._includeComments, false); - - // Async tick is needed because iron-selector content is distributed and - // distributed content requires an observer to be set up. - // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. - flush(() => { - flush(() => { - element.draft = 'I wholeheartedly disapprove'; - - stubSaveReview(review => { - assert.deepEqual(review, { - drafts: 'KEEP', - labels: { - 'Code-Review': 0, - 'Verified': 0, - }, - message: 'I wholeheartedly disapprove', - reviewers: [], - }); - assert.isTrue(element.$.commentList.hidden); - done(); - }); - - // This is needed on non-Blink engines most likely due to the ways in - // which the dom-repeat elements are stamped. - flush(() => { - MockInteractions.tap(element.shadowRoot - .querySelector('.send')); - }); - }); - }); - }); - - test('label picker', done => { - element.draft = 'I wholeheartedly disapprove'; - stubSaveReview(review => { - assert.deepEqual(review, { - drafts: 'PUBLISH_ALL_REVISIONS', - labels: { - 'Code-Review': -1, - 'Verified': -1, + element = fixture('basic'); + element.change = { + _number: changeNum, + labels: { + 'Verified': { + values: { + '-1': 'Fails', + ' 0': 'No score', + '+1': 'Verified', }, - message: 'I wholeheartedly disapprove', - reviewers: [], - }); - }); + default_value: 0, + }, + 'Code-Review': { + values: { + '-2': 'Do not submit', + '-1': 'I would prefer that you didn\'t submit this', + ' 0': 'No score', + '+1': 'Looks good to me, but someone else must approve', + '+2': 'Looks good to me, approved', + }, + default_value: 0, + }, + }, + }; + element.patchNum = patchNum; + element.permittedLabels = { + 'Code-Review': [ + '-1', + ' 0', + '+1', + ], + 'Verified': [ + '-1', + ' 0', + '+1', + ], + }; - sandbox.stub(element.$.labelScores, 'getLabelValues', () => { - return { - 'Code-Review': -1, - 'Verified': -1, - }; - }); + getDraftCommentStub = sandbox.stub(element.$.storage, 'getDraftComment'); + setDraftCommentStub = sandbox.stub(element.$.storage, 'setDraftComment'); + eraseDraftCommentStub = sandbox.stub(element.$.storage, + 'eraseDraftComment'); - element.addEventListener('send', () => { - // Flush to ensure properties are updated. - flush(() => { - assert.isFalse(element.disabled, - 'Element should be enabled when done sending reply.'); - assert.equal(element.draft.length, 0); + sandbox.stub(element, 'fetchChangeUpdates') + .returns(Promise.resolve({isLatest: true})); + + // Allow the elements created by dom-repeat to be stamped. + flushAsynchronousOperations(); + }); + + teardown(() => { + sandbox.restore(); + }); + + function stubSaveReview(jsonResponseProducer) { + return sandbox.stub( + element, + '_saveReview', + review => new Promise((resolve, reject) => { + try { + const result = jsonResponseProducer(review) || {}; + const resultStr = + element.$.restAPI.JSON_PREFIX + JSON.stringify(result); + resolve({ + ok: true, + text() { + return Promise.resolve(resultStr); + }, + }); + } catch (err) { + reject(err); + } + })); + } + + test('default to publishing draft comments with reply', done => { + // Async tick is needed because iron-selector content is distributed and + // distributed content requires an observer to be set up. + // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. + flush(() => { + flush(() => { + element.draft = 'I wholeheartedly disapprove'; + + stubSaveReview(review => { + assert.deepEqual(review, { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: { + 'Code-Review': 0, + 'Verified': 0, + }, + message: 'I wholeheartedly disapprove', + reviewers: [], + }); + assert.isFalse(element.$.commentList.hidden); done(); }); - }); - // This is needed on non-Blink engines most likely due to the ways in - // which the dom-repeat elements are stamped. - flush(() => { - MockInteractions.tap(element.shadowRoot - .querySelector('.send')); - assert.isTrue(element.disabled); - }); - }); - - test('getlabelValue returns value', done => { - flush(() => { - element.shadowRoot - .querySelector('gr-label-scores') - .shadowRoot - .querySelector(`gr-label-score-row[name="Verified"]`) - .setSelectedValue(-1); - assert.equal('-1', element.getLabelValue('Verified')); - done(); - }); - }); - - test('getlabelValue when no score is selected', done => { - flush(() => { - element.shadowRoot - .querySelector('gr-label-scores') - .shadowRoot - .querySelector(`gr-label-score-row[name="Code-Review"]`) - .setSelectedValue(-1); - assert.strictEqual(element.getLabelValue('Verified'), ' 0'); - done(); - }); - }); - - test('setlabelValue', done => { - element._account = {_account_id: 1}; - flush(() => { - const label = 'Verified'; - const value = '+1'; - element.setLabelValue(label, value); - - const labels = element.$.labelScores.getLabelValues(); - assert.deepEqual(labels, { - 'Code-Review': 0, - 'Verified': 1, + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + flush(() => { + MockInteractions.tap(element.shadowRoot + .querySelector('.send')); }); + }); + }); + }); + + test('keep draft comments with reply', done => { + MockInteractions.tap(element.shadowRoot.querySelector('#includeComments')); + assert.equal(element._includeComments, false); + + // Async tick is needed because iron-selector content is distributed and + // distributed content requires an observer to be set up. + // Note: Double flush seems to be needed in Safari. {@see Issue 4963}. + flush(() => { + flush(() => { + element.draft = 'I wholeheartedly disapprove'; + + stubSaveReview(review => { + assert.deepEqual(review, { + drafts: 'KEEP', + labels: { + 'Code-Review': 0, + 'Verified': 0, + }, + message: 'I wholeheartedly disapprove', + reviewers: [], + }); + assert.isTrue(element.$.commentList.hidden); + done(); + }); + + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + flush(() => { + MockInteractions.tap(element.shadowRoot + .querySelector('.send')); + }); + }); + }); + }); + + test('label picker', done => { + element.draft = 'I wholeheartedly disapprove'; + stubSaveReview(review => { + assert.deepEqual(review, { + drafts: 'PUBLISH_ALL_REVISIONS', + labels: { + 'Code-Review': -1, + 'Verified': -1, + }, + message: 'I wholeheartedly disapprove', + reviewers: [], + }); + }); + + sandbox.stub(element.$.labelScores, 'getLabelValues', () => { + return { + 'Code-Review': -1, + 'Verified': -1, + }; + }); + + element.addEventListener('send', () => { + // Flush to ensure properties are updated. + flush(() => { + assert.isFalse(element.disabled, + 'Element should be enabled when done sending reply.'); + assert.equal(element.draft.length, 0); done(); }); }); - function getActiveElement() { - return Polymer.IronOverlayManager.deepActiveElement; - } + // This is needed on non-Blink engines most likely due to the ways in + // which the dom-repeat elements are stamped. + flush(() => { + MockInteractions.tap(element.shadowRoot + .querySelector('.send')); + assert.isTrue(element.disabled); + }); + }); - function isVisible(el) { - assert.ok(el); - return getComputedStyle(el).getPropertyValue('display') != 'none'; - } + test('getlabelValue returns value', done => { + flush(() => { + element.shadowRoot + .querySelector('gr-label-scores') + .shadowRoot + .querySelector(`gr-label-score-row[name="Verified"]`) + .setSelectedValue(-1); + assert.equal('-1', element.getLabelValue('Verified')); + done(); + }); + }); - function overlayObserver(mode) { - return new Promise(resolve => { - function listener() { - element.removeEventListener('iron-overlay-' + mode, listener); - resolve(); - } - element.addEventListener('iron-overlay-' + mode, listener); + test('getlabelValue when no score is selected', done => { + flush(() => { + element.shadowRoot + .querySelector('gr-label-scores') + .shadowRoot + .querySelector(`gr-label-score-row[name="Code-Review"]`) + .setSelectedValue(-1); + assert.strictEqual(element.getLabelValue('Verified'), ' 0'); + done(); + }); + }); + + test('setlabelValue', done => { + element._account = {_account_id: 1}; + flush(() => { + const label = 'Verified'; + const value = '+1'; + element.setLabelValue(label, value); + + const labels = element.$.labelScores.getLabelValues(); + assert.deepEqual(labels, { + 'Code-Review': 0, + 'Verified': 1, }); - } + done(); + }); + }); - function isFocusInsideElement(element) { - // In Polymer 2 focused element either <paper-input> or nested - // native input <input> element depending on the current focus - // in browser window. - // For example, the focus is changed if the developer console - // get a focus. - let activeElement = getActiveElement(); - while (activeElement) { - if (activeElement === element) { - return true; - } - if (activeElement.parentElement) { - activeElement = activeElement.parentElement; - } else { - activeElement = activeElement.getRootNode().host; - } + function getActiveElement() { + return IronOverlayManager.deepActiveElement; + } + + function isVisible(el) { + assert.ok(el); + return getComputedStyle(el).getPropertyValue('display') != 'none'; + } + + function overlayObserver(mode) { + return new Promise(resolve => { + function listener() { + element.removeEventListener('iron-overlay-' + mode, listener); + resolve(); } - return false; + element.addEventListener('iron-overlay-' + mode, listener); + }); + } + + function isFocusInsideElement(element) { + // In Polymer 2 focused element either <paper-input> or nested + // native input <input> element depending on the current focus + // in browser window. + // For example, the focus is changed if the developer console + // get a focus. + let activeElement = getActiveElement(); + while (activeElement) { + if (activeElement === element) { + return true; + } + if (activeElement.parentElement) { + activeElement = activeElement.parentElement; + } else { + activeElement = activeElement.getRootNode().host; + } } + return false; + } - function testConfirmationDialog(done, cc) { - const yesButton = element - .shadowRoot - .querySelector('.reviewerConfirmationButtons gr-button:first-child'); - const noButton = element - .shadowRoot - .querySelector('.reviewerConfirmationButtons gr-button:last-child'); + function testConfirmationDialog(done, cc) { + const yesButton = element + .shadowRoot + .querySelector('.reviewerConfirmationButtons gr-button:first-child'); + const noButton = element + .shadowRoot + .querySelector('.reviewerConfirmationButtons gr-button:last-child'); - element._ccPendingConfirmation = null; - element._reviewerPendingConfirmation = null; - flushAsynchronousOperations(); - assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); + element._ccPendingConfirmation = null; + element._reviewerPendingConfirmation = null; + flushAsynchronousOperations(); + assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); - // Cause the confirmation dialog to display. - let observer = overlayObserver('opened'); - const group = { - id: 'id', - name: 'name', + // Cause the confirmation dialog to display. + let observer = overlayObserver('opened'); + const group = { + id: 'id', + name: 'name', + }; + if (cc) { + element._ccPendingConfirmation = { + group, + count: 10, }; - if (cc) { - element._ccPendingConfirmation = { - group, - count: 10, - }; - } else { - element._reviewerPendingConfirmation = { - group, - count: 10, - }; - } - flushAsynchronousOperations(); + } else { + element._reviewerPendingConfirmation = { + group, + count: 10, + }; + } + flushAsynchronousOperations(); - if (cc) { - assert.deepEqual( - element._ccPendingConfirmation, - element._pendingConfirmationDetails); - } else { - assert.deepEqual( - element._reviewerPendingConfirmation, - element._pendingConfirmationDetails); - } + if (cc) { + assert.deepEqual( + element._ccPendingConfirmation, + element._pendingConfirmationDetails); + } else { + assert.deepEqual( + element._reviewerPendingConfirmation, + element._pendingConfirmationDetails); + } - observer - .then(() => { - assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); - observer = overlayObserver('closed'); - const expected = 'Group name has 10 members'; - assert.notEqual( - element.$.reviewerConfirmationOverlay.innerText - .indexOf(expected), - -1); - MockInteractions.tap(noButton); // close the overlay - return observer; - }).then(() => { - assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); + observer + .then(() => { + assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); + observer = overlayObserver('closed'); + const expected = 'Group name has 10 members'; + assert.notEqual( + element.$.reviewerConfirmationOverlay.innerText + .indexOf(expected), + -1); + MockInteractions.tap(noButton); // close the overlay + return observer; + }).then(() => { + assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); - // We should be focused on account entry input. + // We should be focused on account entry input. + assert.isTrue( + isFocusInsideElement( + element.$.reviewers.$.entry.$.input.$.input + ) + ); + + // No reviewer/CC should have been added. + assert.equal(element.$.ccs.additions().length, 0); + assert.equal(element.$.reviewers.additions().length, 0); + + // Reopen confirmation dialog. + observer = overlayObserver('opened'); + if (cc) { + element._ccPendingConfirmation = { + group, + count: 10, + }; + } else { + element._reviewerPendingConfirmation = { + group, + count: 10, + }; + } + return observer; + }) + .then(() => { + assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); + observer = overlayObserver('closed'); + MockInteractions.tap(yesButton); // Confirm the group. + return observer; + }) + .then(() => { + assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); + const additions = cc ? + element.$.ccs.additions() : + element.$.reviewers.additions(); + assert.deepEqual( + additions, + [ + { + group: { + id: 'id', + name: 'name', + confirmed: true, + _group: true, + _pendingAdd: true, + }, + }, + ]); + + // We should be focused on account entry input. + if (cc) { + assert.isTrue( + isFocusInsideElement( + element.$.ccs.$.entry.$.input.$.input + ) + ); + } else { assert.isTrue( isFocusInsideElement( element.$.reviewers.$.entry.$.input.$.input ) ); + } + }) + .then(done); + } - // No reviewer/CC should have been added. - assert.equal(element.$.ccs.additions().length, 0); - assert.equal(element.$.reviewers.additions().length, 0); + test('cc confirmation', done => { + testConfirmationDialog(done, true); + }); - // Reopen confirmation dialog. - observer = overlayObserver('opened'); - if (cc) { - element._ccPendingConfirmation = { - group, - count: 10, - }; - } else { - element._reviewerPendingConfirmation = { - group, - count: 10, - }; - } - return observer; - }) - .then(() => { - assert.isTrue(isVisible(element.$.reviewerConfirmationOverlay)); - observer = overlayObserver('closed'); - MockInteractions.tap(yesButton); // Confirm the group. - return observer; - }) - .then(() => { - assert.isFalse(isVisible(element.$.reviewerConfirmationOverlay)); - const additions = cc ? - element.$.ccs.additions() : - element.$.reviewers.additions(); - assert.deepEqual( - additions, - [ - { - group: { - id: 'id', - name: 'name', - confirmed: true, - _group: true, - _pendingAdd: true, - }, - }, - ]); + test('reviewer confirmation', done => { + testConfirmationDialog(done, false); + }); - // We should be focused on account entry input. - if (cc) { - assert.isTrue( - isFocusInsideElement( - element.$.ccs.$.entry.$.input.$.input - ) - ); - } else { - assert.isTrue( - isFocusInsideElement( - element.$.reviewers.$.entry.$.input.$.input - ) - ); - } - }) - .then(done); - } + test('_getStorageLocation', () => { + const actual = element._getStorageLocation(); + assert.equal(actual.changeNum, changeNum); + assert.equal(actual.patchNum, '@change'); + assert.equal(actual.path, '@change'); + }); - test('cc confirmation', done => { - testConfirmationDialog(done, true); + test('_reviewersMutated when account-text-change is fired from ccs', () => { + flushAsynchronousOperations(); + assert.isFalse(element._reviewersMutated); + assert.isTrue(element.$.ccs.allowAnyInput); + assert.isFalse(element.shadowRoot + .querySelector('#reviewers').allowAnyInput); + element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed', + {bubbles: true, composed: true})); + assert.isTrue(element._reviewersMutated); + }); + + test('gets draft from storage on open', () => { + const storedDraft = 'hello world'; + getDraftCommentStub.returns({message: storedDraft}); + element.open(); + assert.isTrue(getDraftCommentStub.called); + assert.equal(element.draft, storedDraft); + }); + + test('gets draft from storage even when text is already present', () => { + const storedDraft = 'hello world'; + getDraftCommentStub.returns({message: storedDraft}); + element.draft = 'foo bar'; + element.open(); + assert.isTrue(getDraftCommentStub.called); + assert.equal(element.draft, storedDraft); + }); + + test('blank if no stored draft', () => { + getDraftCommentStub.returns(null); + element.draft = 'foo bar'; + element.open(); + assert.isTrue(getDraftCommentStub.called); + assert.equal(element.draft, ''); + }); + + test('does not check stored draft when quote is present', () => { + const storedDraft = 'hello world'; + const quote = '> foo bar'; + getDraftCommentStub.returns({message: storedDraft}); + element.quote = quote; + element.open(); + assert.isFalse(getDraftCommentStub.called); + assert.equal(element.draft, quote); + assert.isNotOk(element.quote); + }); + + test('updates stored draft on edits', () => { + const firstEdit = 'hello'; + const location = element._getStorageLocation(); + + element.draft = firstEdit; + element.flushDebouncer('store'); + + assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit)); + + element.draft = ''; + element.flushDebouncer('store'); + + assert.isTrue(eraseDraftCommentStub.calledWith(location)); + }); + + test('400 converts to human-readable server-error', done => { + sandbox.stub(window, 'fetch', () => { + const text = '....{"reviewers":{"id1":{"error":"first error"}},' + + '"ccs":{"id2":{"error":"second error"}}}'; + return Promise.resolve(cloneableResponse(400, text)); }); - test('reviewer confirmation', done => { - testConfirmationDialog(done, false); - }); - - test('_getStorageLocation', () => { - const actual = element._getStorageLocation(); - assert.equal(actual.changeNum, changeNum); - assert.equal(actual.patchNum, '@change'); - assert.equal(actual.path, '@change'); - }); - - test('_reviewersMutated when account-text-change is fired from ccs', () => { - flushAsynchronousOperations(); - assert.isFalse(element._reviewersMutated); - assert.isTrue(element.$.ccs.allowAnyInput); - assert.isFalse(element.shadowRoot - .querySelector('#reviewers').allowAnyInput); - element.$.ccs.dispatchEvent(new CustomEvent('account-text-changed', - {bubbles: true, composed: true})); - assert.isTrue(element._reviewersMutated); - }); - - test('gets draft from storage on open', () => { - const storedDraft = 'hello world'; - getDraftCommentStub.returns({message: storedDraft}); - element.open(); - assert.isTrue(getDraftCommentStub.called); - assert.equal(element.draft, storedDraft); - }); - - test('gets draft from storage even when text is already present', () => { - const storedDraft = 'hello world'; - getDraftCommentStub.returns({message: storedDraft}); - element.draft = 'foo bar'; - element.open(); - assert.isTrue(getDraftCommentStub.called); - assert.equal(element.draft, storedDraft); - }); - - test('blank if no stored draft', () => { - getDraftCommentStub.returns(null); - element.draft = 'foo bar'; - element.open(); - assert.isTrue(getDraftCommentStub.called); - assert.equal(element.draft, ''); - }); - - test('does not check stored draft when quote is present', () => { - const storedDraft = 'hello world'; - const quote = '> foo bar'; - getDraftCommentStub.returns({message: storedDraft}); - element.quote = quote; - element.open(); - assert.isFalse(getDraftCommentStub.called); - assert.equal(element.draft, quote); - assert.isNotOk(element.quote); - }); - - test('updates stored draft on edits', () => { - const firstEdit = 'hello'; - const location = element._getStorageLocation(); - - element.draft = firstEdit; - element.flushDebouncer('store'); - - assert.isTrue(setDraftCommentStub.calledWith(location, firstEdit)); - - element.draft = ''; - element.flushDebouncer('store'); - - assert.isTrue(eraseDraftCommentStub.calledWith(location)); - }); - - test('400 converts to human-readable server-error', done => { - sandbox.stub(window, 'fetch', () => { - const text = '....{"reviewers":{"id1":{"error":"first error"}},' + - '"ccs":{"id2":{"error":"second error"}}}'; - return Promise.resolve(cloneableResponse(400, text)); - }); - - element.addEventListener('server-error', event => { - if (event.target !== element) { - return; - } - event.detail.response.text().then(body => { - assert.equal(body, 'first error, second error'); - done(); - }); - }); - - // Async tick is needed because iron-selector content is distributed and - // distributed content requires an observer to be set up. - flush(() => { element.send(); }); - }); - - test('non-json 400 is treated as a normal server-error', done => { - sandbox.stub(window, 'fetch', () => { - const text = 'Comment validation error!'; - return Promise.resolve(cloneableResponse(400, text)); - }); - - element.addEventListener('server-error', event => { - if (event.target !== element) { - return; - } - event.detail.response.text().then(body => { - assert.equal(body, 'Comment validation error!'); - done(); - }); - }); - - // Async tick is needed because iron-selector content is distributed and - // distributed content requires an observer to be set up. - flush(() => { element.send(); }); - }); - - test('filterReviewerSuggestion', () => { - const owner = makeAccount(); - const reviewer1 = makeAccount(); - const reviewer2 = makeGroup(); - const cc1 = makeAccount(); - const cc2 = makeGroup(); - let filter = element._filterReviewerSuggestionGenerator(false); - - element._owner = owner; - element._reviewers = [reviewer1, reviewer2]; - element._ccs = [cc1, cc2]; - - assert.isTrue(filter({account: makeAccount()})); - assert.isTrue(filter({group: makeGroup()})); - - // Owner should be excluded. - assert.isFalse(filter({account: owner})); - - // Existing and pending reviewers should be excluded when isCC = false. - assert.isFalse(filter({account: reviewer1})); - assert.isFalse(filter({group: reviewer2})); - - filter = element._filterReviewerSuggestionGenerator(true); - - // Existing and pending CCs should be excluded when isCC = true;. - assert.isFalse(filter({account: cc1})); - assert.isFalse(filter({group: cc2})); - }); - - test('_focusOn', () => { - sandbox.spy(element, '_chooseFocusTarget'); - flushAsynchronousOperations(); - const textareaStub = sandbox.stub(element.$.textarea, 'async'); - const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart, - 'async'); - const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async'); - element._focusOn(); - assert.equal(element._chooseFocusTarget.callCount, 1); - assert.deepEqual(textareaStub.callCount, 1); - assert.deepEqual(reviewerEntryStub.callCount, 0); - assert.deepEqual(ccStub.callCount, 0); - - element._focusOn(element.FocusTarget.ANY); - assert.equal(element._chooseFocusTarget.callCount, 2); - assert.deepEqual(textareaStub.callCount, 2); - assert.deepEqual(reviewerEntryStub.callCount, 0); - assert.deepEqual(ccStub.callCount, 0); - - element._focusOn(element.FocusTarget.BODY); - assert.equal(element._chooseFocusTarget.callCount, 2); - assert.deepEqual(textareaStub.callCount, 3); - assert.deepEqual(reviewerEntryStub.callCount, 0); - assert.deepEqual(ccStub.callCount, 0); - - element._focusOn(element.FocusTarget.REVIEWERS); - assert.equal(element._chooseFocusTarget.callCount, 2); - assert.deepEqual(textareaStub.callCount, 3); - assert.deepEqual(reviewerEntryStub.callCount, 1); - assert.deepEqual(ccStub.callCount, 0); - - element._focusOn(element.FocusTarget.CCS); - assert.equal(element._chooseFocusTarget.callCount, 2); - assert.deepEqual(textareaStub.callCount, 3); - assert.deepEqual(reviewerEntryStub.callCount, 1); - assert.deepEqual(ccStub.callCount, 1); - }); - - test('_chooseFocusTarget', () => { - element._account = null; - assert.strictEqual( - element._chooseFocusTarget(), element.FocusTarget.BODY); - - element._account = {_account_id: 1}; - assert.strictEqual( - element._chooseFocusTarget(), element.FocusTarget.BODY); - - element.change.owner = {_account_id: 2}; - assert.strictEqual( - element._chooseFocusTarget(), element.FocusTarget.BODY); - - element.change.owner._account_id = 1; - element.change._reviewers = null; - assert.strictEqual( - element._chooseFocusTarget(), element.FocusTarget.REVIEWERS); - - element._reviewers = []; - assert.strictEqual( - element._chooseFocusTarget(), element.FocusTarget.REVIEWERS); - - element._reviewers.push({}); - assert.strictEqual( - element._chooseFocusTarget(), element.FocusTarget.BODY); - }); - - test('only send labels that have changed', done => { - flush(() => { - stubSaveReview(review => { - assert.deepEqual(review.labels, { - 'Code-Review': 0, - 'Verified': -1, - }); - }); - - element.addEventListener('send', () => { - done(); - }); - // Without wrapping this test in flush(), the below two calls to - // MockInteractions.tap() cause a race in some situations in shadow DOM. - // The send button can be tapped before the others, causing the test to - // fail. - - element.shadowRoot - .querySelector('gr-label-scores').shadowRoot - .querySelector( - 'gr-label-score-row[name="Verified"]') - .setSelectedValue(-1); - MockInteractions.tap(element.shadowRoot - .querySelector('.send')); - }); - }); - - test('_processReviewerChange', () => { - const mockIndexSplices = function(toRemove) { - return [{ - removed: [toRemove], - }]; - }; - - element._processReviewerChange( - mockIndexSplices(makeAccount()), 'REVIEWER'); - assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1); - }); - - test('_purgeReviewersPendingRemove', () => { - const removeStub = sandbox.stub(element, '_removeAccount'); - const mock = function() { - element._reviewersPendingRemove = { - test: [makeAccount()], - test2: [makeAccount(), makeAccount()], - }; - }; - const checkObjEmpty = function(obj) { - for (const prop in obj) { - if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; } - } - return true; - }; - mock(); - element._purgeReviewersPendingRemove(true); // Cancel - assert.isFalse(removeStub.called); - assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); - - mock(); - element._purgeReviewersPendingRemove(false); // Submit - assert.isTrue(removeStub.called); - assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); - }); - - test('_removeAccount', done => { - sandbox.stub(element.$.restAPI, 'removeChangeReviewer') - .returns(Promise.resolve({ok: true})); - const arr = [makeAccount(), makeAccount()]; - element.change.reviewers = { - REVIEWER: arr.slice(), - }; - - element._removeAccount(arr[1], 'REVIEWER').then(() => { - assert.equal(element.change.reviewers.REVIEWER.length, 1); - assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1)); + element.addEventListener('server-error', event => { + if (event.target !== element) { + return; + } + event.detail.response.text().then(body => { + assert.equal(body, 'first error, second error'); done(); }); }); - test('moving from cc to reviewer', () => { - element._reviewersPendingRemove = { - CC: [], - REVIEWER: [], - }; - flushAsynchronousOperations(); + // Async tick is needed because iron-selector content is distributed and + // distributed content requires an observer to be set up. + flush(() => { element.send(); }); + }); - const reviewer1 = makeAccount(); - const reviewer2 = makeAccount(); - const reviewer3 = makeAccount(); - const cc1 = makeAccount(); - const cc2 = makeAccount(); - const cc3 = makeAccount(); - const cc4 = makeAccount(); - element._reviewers = [reviewer1, reviewer2, reviewer3]; - element._ccs = [cc1, cc2, cc3, cc4]; - element.push('_reviewers', cc1); - flushAsynchronousOperations(); - - assert.deepEqual(element._reviewers, - [reviewer1, reviewer2, reviewer3, cc1]); - assert.deepEqual(element._ccs, [cc2, cc3, cc4]); - assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]); - - element.push('_reviewers', cc4, cc3); - flushAsynchronousOperations(); - - assert.deepEqual(element._reviewers, - [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]); - assert.deepEqual(element._ccs, [cc2]); - assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]); + test('non-json 400 is treated as a normal server-error', done => { + sandbox.stub(window, 'fetch', () => { + const text = 'Comment validation error!'; + return Promise.resolve(cloneableResponse(400, text)); }); - test('moving from reviewer to cc', () => { - element._reviewersPendingRemove = { - CC: [], - REVIEWER: [], - }; - flushAsynchronousOperations(); - - const reviewer1 = makeAccount(); - const reviewer2 = makeAccount(); - const reviewer3 = makeAccount(); - const cc1 = makeAccount(); - const cc2 = makeAccount(); - const cc3 = makeAccount(); - const cc4 = makeAccount(); - element._reviewers = [reviewer1, reviewer2, reviewer3]; - element._ccs = [cc1, cc2, cc3, cc4]; - element.push('_ccs', reviewer1); - flushAsynchronousOperations(); - - assert.deepEqual(element._reviewers, - [reviewer2, reviewer3]); - assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]); - assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]); - - element.push('_ccs', reviewer3, reviewer2); - flushAsynchronousOperations(); - - assert.deepEqual(element._reviewers, []); - assert.deepEqual(element._ccs, - [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]); - assert.deepEqual(element._reviewersPendingRemove.REVIEWER, - [reviewer1, reviewer3, reviewer2]); - }); - - test('migrate reviewers between states', done => { - element._reviewersPendingRemove = { - CC: [], - REVIEWER: [], - }; - flushAsynchronousOperations(); - const reviewers = element.$.reviewers; - const ccs = element.$.ccs; - const reviewer1 = makeAccount(); - const reviewer2 = makeAccount(); - const cc1 = makeAccount(); - const cc2 = makeAccount(); - const cc3 = makeAccount(); - element._reviewers = [reviewer1, reviewer2]; - element._ccs = [cc1, cc2, cc3]; - - const mutations = []; - - stubSaveReview(review => mutations.push(...review.reviewers)); - - sandbox.stub(element, '_removeAccount', (account, type) => { - mutations.push({state: 'REMOVED', account}); - return Promise.resolve(); - }); - - // Remove and add to other field. - reviewers.fire('remove', {account: reviewer1}); - ccs.$.entry.fire('add', {value: {account: reviewer1}}); - ccs.fire('remove', {account: cc1}); - ccs.fire('remove', {account: cc3}); - reviewers.$.entry.fire('add', {value: {account: cc1}}); - - // Add to other field without removing from former field. - // (Currently not possible in UI, but this is a good consistency check). - reviewers.$.entry.fire('add', {value: {account: cc2}}); - ccs.$.entry.fire('add', {value: {account: reviewer2}}); - const mapReviewer = function(reviewer, opt_state) { - const result = {reviewer: reviewer._account_id, confirmed: undefined}; - if (opt_state) { - result.state = opt_state; - } - return result; - }; - - // Send and purge and verify moves, delete cc3. - element.send() - .then(keepReviewers => - element._purgeReviewersPendingRemove(false, keepReviewers)) - .then(() => { - assert.deepEqual( - mutations, [ - mapReviewer(cc1), - mapReviewer(cc2), - mapReviewer(reviewer1, 'CC'), - mapReviewer(reviewer2, 'CC'), - {account: cc3, state: 'REMOVED'}, - ]); - done(); - }); - }); - - test('emits cancel on esc key', () => { - const cancelHandler = sandbox.spy(); - element.addEventListener('cancel', cancelHandler); - MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc'); - flushAsynchronousOperations(); - - assert.isTrue(cancelHandler.called); - }); - - test('should not send on enter key', () => { - stubSaveReview(() => undefined); - element.addEventListener('send', () => assert.fail('wrongly called')); - MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); - flushAsynchronousOperations(); - }); - - test('emit send on ctrl+enter key', done => { - stubSaveReview(() => undefined); - element.addEventListener('send', () => done()); - MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter'); - flushAsynchronousOperations(); - }); - - test('_computeMessagePlaceholder', () => { - assert.equal( - element._computeMessagePlaceholder(false), - 'Say something nice...'); - assert.equal( - element._computeMessagePlaceholder(true), - 'Add a note for your reviewers...'); - }); - - test('_computeSendButtonLabel', () => { - assert.equal( - element._computeSendButtonLabel(false), - 'Send'); - assert.equal( - element._computeSendButtonLabel(true), - 'Start review'); - }); - - test('_handle400Error reviewrs and CCs', done => { - const error1 = 'error 1'; - const error2 = 'error 2'; - const error3 = 'error 3'; - const text = ')]}\'' + JSON.stringify({ - reviewers: { - username1: { - input: 'user 1', - error: error1, - }, - username2: { - input: 'user 2', - error: error2, - }, - }, - ccs: { - username3: { - input: 'user 3', - error: error3, - }, - }, - }); - element.addEventListener('server-error', e => { - e.detail.response.text().then(text => { - assert.equal(text, [error1, error2, error3].join(', ')); - done(); - }); - }); - element._handle400Error(cloneableResponse(400, text)); - }); - - test('_handle400Error CCs only', done => { - const error1 = 'error 1'; - const text = ')]}\'' + JSON.stringify({ - ccs: { - username1: { - input: 'user 1', - error: error1, - }, - }, - }); - element.addEventListener('server-error', e => { - e.detail.response.text().then(text => { - assert.equal(text, error1); - done(); - }); - }); - element._handle400Error(cloneableResponse(400, text)); - }); - - test('fires height change when the drafts comments load', done => { - // Flush DOM operations before binding to the autogrow event so we don't - // catch the events fired from the initial layout. - flush(() => { - const autoGrowHandler = sinon.stub(); - element.addEventListener('autogrow', autoGrowHandler); - element.draftCommentThreads = []; - flush(() => { - assert.isTrue(autoGrowHandler.called); - done(); - }); + element.addEventListener('server-error', event => { + if (event.target !== element) { + return; + } + event.detail.response.text().then(body => { + assert.equal(body, 'Comment validation error!'); + done(); }); }); - suite('post review API', () => { - let startReviewStub; + // Async tick is needed because iron-selector content is distributed and + // distributed content requires an observer to be set up. + flush(() => { element.send(); }); + }); - setup(() => { - startReviewStub = sandbox.stub( - element.$.restAPI, - 'startReview', - () => Promise.resolve()); - }); + test('filterReviewerSuggestion', () => { + const owner = makeAccount(); + const reviewer1 = makeAccount(); + const reviewer2 = makeGroup(); + const cc1 = makeAccount(); + const cc2 = makeGroup(); + let filter = element._filterReviewerSuggestionGenerator(false); - test('ready property in review input on start review', () => { - stubSaveReview(review => { - assert.isTrue(review.ready); - return {ready: true}; - }); - return element.send(true, true).then(() => { - assert.isFalse(startReviewStub.called); - }); - }); + element._owner = owner; + element._reviewers = [reviewer1, reviewer2]; + element._ccs = [cc1, cc2]; - test('no ready property in review input on save review', () => { - stubSaveReview(review => { - assert.isUndefined(review.ready); - }); - return element.send(true, false).then(() => { - assert.isFalse(startReviewStub.called); - }); - }); - }); + assert.isTrue(filter({account: makeAccount()})); + assert.isTrue(filter({group: makeGroup()})); - suite('start review and save buttons', () => { - let sendStub; + // Owner should be excluded. + assert.isFalse(filter({account: owner})); - setup(() => { - sendStub = sandbox.stub(element, 'send', () => Promise.resolve()); - element.canBeStarted = true; - // Flush to make both Start/Save buttons appear in DOM. - flushAsynchronousOperations(); - }); + // Existing and pending reviewers should be excluded when isCC = false. + assert.isFalse(filter({account: reviewer1})); + assert.isFalse(filter({group: reviewer2})); - test('start review sets ready', () => { - MockInteractions.tap(element.shadowRoot - .querySelector('.send')); - flushAsynchronousOperations(); - assert.isTrue(sendStub.calledWith(true, true)); - }); + filter = element._filterReviewerSuggestionGenerator(true); - test('save review doesn\'t set ready', () => { - MockInteractions.tap(element.shadowRoot - .querySelector('.save')); - flushAsynchronousOperations(); - assert.isTrue(sendStub.calledWith(true, false)); - }); - }); + // Existing and pending CCs should be excluded when isCC = true;. + assert.isFalse(filter({account: cc1})); + assert.isFalse(filter({group: cc2})); + }); - test('buttons disabled until all API calls are resolved', () => { + test('_focusOn', () => { + sandbox.spy(element, '_chooseFocusTarget'); + flushAsynchronousOperations(); + const textareaStub = sandbox.stub(element.$.textarea, 'async'); + const reviewerEntryStub = sandbox.stub(element.$.reviewers.focusStart, + 'async'); + const ccStub = sandbox.stub(element.$.ccs.focusStart, 'async'); + element._focusOn(); + assert.equal(element._chooseFocusTarget.callCount, 1); + assert.deepEqual(textareaStub.callCount, 1); + assert.deepEqual(reviewerEntryStub.callCount, 0); + assert.deepEqual(ccStub.callCount, 0); + + element._focusOn(element.FocusTarget.ANY); + assert.equal(element._chooseFocusTarget.callCount, 2); + assert.deepEqual(textareaStub.callCount, 2); + assert.deepEqual(reviewerEntryStub.callCount, 0); + assert.deepEqual(ccStub.callCount, 0); + + element._focusOn(element.FocusTarget.BODY); + assert.equal(element._chooseFocusTarget.callCount, 2); + assert.deepEqual(textareaStub.callCount, 3); + assert.deepEqual(reviewerEntryStub.callCount, 0); + assert.deepEqual(ccStub.callCount, 0); + + element._focusOn(element.FocusTarget.REVIEWERS); + assert.equal(element._chooseFocusTarget.callCount, 2); + assert.deepEqual(textareaStub.callCount, 3); + assert.deepEqual(reviewerEntryStub.callCount, 1); + assert.deepEqual(ccStub.callCount, 0); + + element._focusOn(element.FocusTarget.CCS); + assert.equal(element._chooseFocusTarget.callCount, 2); + assert.deepEqual(textareaStub.callCount, 3); + assert.deepEqual(reviewerEntryStub.callCount, 1); + assert.deepEqual(ccStub.callCount, 1); + }); + + test('_chooseFocusTarget', () => { + element._account = null; + assert.strictEqual( + element._chooseFocusTarget(), element.FocusTarget.BODY); + + element._account = {_account_id: 1}; + assert.strictEqual( + element._chooseFocusTarget(), element.FocusTarget.BODY); + + element.change.owner = {_account_id: 2}; + assert.strictEqual( + element._chooseFocusTarget(), element.FocusTarget.BODY); + + element.change.owner._account_id = 1; + element.change._reviewers = null; + assert.strictEqual( + element._chooseFocusTarget(), element.FocusTarget.REVIEWERS); + + element._reviewers = []; + assert.strictEqual( + element._chooseFocusTarget(), element.FocusTarget.REVIEWERS); + + element._reviewers.push({}); + assert.strictEqual( + element._chooseFocusTarget(), element.FocusTarget.BODY); + }); + + test('only send labels that have changed', done => { + flush(() => { stubSaveReview(review => { + assert.deepEqual(review.labels, { + 'Code-Review': 0, + 'Verified': -1, + }); + }); + + element.addEventListener('send', () => { + done(); + }); + // Without wrapping this test in flush(), the below two calls to + // MockInteractions.tap() cause a race in some situations in shadow DOM. + // The send button can be tapped before the others, causing the test to + // fail. + + element.shadowRoot + .querySelector('gr-label-scores').shadowRoot + .querySelector( + 'gr-label-score-row[name="Verified"]') + .setSelectedValue(-1); + MockInteractions.tap(element.shadowRoot + .querySelector('.send')); + }); + }); + + test('_processReviewerChange', () => { + const mockIndexSplices = function(toRemove) { + return [{ + removed: [toRemove], + }]; + }; + + element._processReviewerChange( + mockIndexSplices(makeAccount()), 'REVIEWER'); + assert.equal(element._reviewersPendingRemove.REVIEWER.length, 1); + }); + + test('_purgeReviewersPendingRemove', () => { + const removeStub = sandbox.stub(element, '_removeAccount'); + const mock = function() { + element._reviewersPendingRemove = { + test: [makeAccount()], + test2: [makeAccount(), makeAccount()], + }; + }; + const checkObjEmpty = function(obj) { + for (const prop in obj) { + if (obj.hasOwnProperty(prop) && obj[prop].length) { return false; } + } + return true; + }; + mock(); + element._purgeReviewersPendingRemove(true); // Cancel + assert.isFalse(removeStub.called); + assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); + + mock(); + element._purgeReviewersPendingRemove(false); // Submit + assert.isTrue(removeStub.called); + assert.isTrue(checkObjEmpty(element._reviewersPendingRemove)); + }); + + test('_removeAccount', done => { + sandbox.stub(element.$.restAPI, 'removeChangeReviewer') + .returns(Promise.resolve({ok: true})); + const arr = [makeAccount(), makeAccount()]; + element.change.reviewers = { + REVIEWER: arr.slice(), + }; + + element._removeAccount(arr[1], 'REVIEWER').then(() => { + assert.equal(element.change.reviewers.REVIEWER.length, 1); + assert.deepEqual(element.change.reviewers.REVIEWER, arr.slice(0, 1)); + done(); + }); + }); + + test('moving from cc to reviewer', () => { + element._reviewersPendingRemove = { + CC: [], + REVIEWER: [], + }; + flushAsynchronousOperations(); + + const reviewer1 = makeAccount(); + const reviewer2 = makeAccount(); + const reviewer3 = makeAccount(); + const cc1 = makeAccount(); + const cc2 = makeAccount(); + const cc3 = makeAccount(); + const cc4 = makeAccount(); + element._reviewers = [reviewer1, reviewer2, reviewer3]; + element._ccs = [cc1, cc2, cc3, cc4]; + element.push('_reviewers', cc1); + flushAsynchronousOperations(); + + assert.deepEqual(element._reviewers, + [reviewer1, reviewer2, reviewer3, cc1]); + assert.deepEqual(element._ccs, [cc2, cc3, cc4]); + assert.deepEqual(element._reviewersPendingRemove.CC, [cc1]); + + element.push('_reviewers', cc4, cc3); + flushAsynchronousOperations(); + + assert.deepEqual(element._reviewers, + [reviewer1, reviewer2, reviewer3, cc1, cc4, cc3]); + assert.deepEqual(element._ccs, [cc2]); + assert.deepEqual(element._reviewersPendingRemove.CC, [cc1, cc4, cc3]); + }); + + test('moving from reviewer to cc', () => { + element._reviewersPendingRemove = { + CC: [], + REVIEWER: [], + }; + flushAsynchronousOperations(); + + const reviewer1 = makeAccount(); + const reviewer2 = makeAccount(); + const reviewer3 = makeAccount(); + const cc1 = makeAccount(); + const cc2 = makeAccount(); + const cc3 = makeAccount(); + const cc4 = makeAccount(); + element._reviewers = [reviewer1, reviewer2, reviewer3]; + element._ccs = [cc1, cc2, cc3, cc4]; + element.push('_ccs', reviewer1); + flushAsynchronousOperations(); + + assert.deepEqual(element._reviewers, + [reviewer2, reviewer3]); + assert.deepEqual(element._ccs, [cc1, cc2, cc3, cc4, reviewer1]); + assert.deepEqual(element._reviewersPendingRemove.REVIEWER, [reviewer1]); + + element.push('_ccs', reviewer3, reviewer2); + flushAsynchronousOperations(); + + assert.deepEqual(element._reviewers, []); + assert.deepEqual(element._ccs, + [cc1, cc2, cc3, cc4, reviewer1, reviewer3, reviewer2]); + assert.deepEqual(element._reviewersPendingRemove.REVIEWER, + [reviewer1, reviewer3, reviewer2]); + }); + + test('migrate reviewers between states', done => { + element._reviewersPendingRemove = { + CC: [], + REVIEWER: [], + }; + flushAsynchronousOperations(); + const reviewers = element.$.reviewers; + const ccs = element.$.ccs; + const reviewer1 = makeAccount(); + const reviewer2 = makeAccount(); + const cc1 = makeAccount(); + const cc2 = makeAccount(); + const cc3 = makeAccount(); + element._reviewers = [reviewer1, reviewer2]; + element._ccs = [cc1, cc2, cc3]; + + const mutations = []; + + stubSaveReview(review => mutations.push(...review.reviewers)); + + sandbox.stub(element, '_removeAccount', (account, type) => { + mutations.push({state: 'REMOVED', account}); + return Promise.resolve(); + }); + + // Remove and add to other field. + reviewers.fire('remove', {account: reviewer1}); + ccs.$.entry.fire('add', {value: {account: reviewer1}}); + ccs.fire('remove', {account: cc1}); + ccs.fire('remove', {account: cc3}); + reviewers.$.entry.fire('add', {value: {account: cc1}}); + + // Add to other field without removing from former field. + // (Currently not possible in UI, but this is a good consistency check). + reviewers.$.entry.fire('add', {value: {account: cc2}}); + ccs.$.entry.fire('add', {value: {account: reviewer2}}); + const mapReviewer = function(reviewer, opt_state) { + const result = {reviewer: reviewer._account_id, confirmed: undefined}; + if (opt_state) { + result.state = opt_state; + } + return result; + }; + + // Send and purge and verify moves, delete cc3. + element.send() + .then(keepReviewers => + element._purgeReviewersPendingRemove(false, keepReviewers)) + .then(() => { + assert.deepEqual( + mutations, [ + mapReviewer(cc1), + mapReviewer(cc2), + mapReviewer(reviewer1, 'CC'), + mapReviewer(reviewer2, 'CC'), + {account: cc3, state: 'REMOVED'}, + ]); + done(); + }); + }); + + test('emits cancel on esc key', () => { + const cancelHandler = sandbox.spy(); + element.addEventListener('cancel', cancelHandler); + MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc'); + flushAsynchronousOperations(); + + assert.isTrue(cancelHandler.called); + }); + + test('should not send on enter key', () => { + stubSaveReview(() => undefined); + element.addEventListener('send', () => assert.fail('wrongly called')); + MockInteractions.pressAndReleaseKeyOn(element, 13, null, 'enter'); + flushAsynchronousOperations(); + }); + + test('emit send on ctrl+enter key', done => { + stubSaveReview(() => undefined); + element.addEventListener('send', () => done()); + MockInteractions.pressAndReleaseKeyOn(element, 13, 'ctrl', 'enter'); + flushAsynchronousOperations(); + }); + + test('_computeMessagePlaceholder', () => { + assert.equal( + element._computeMessagePlaceholder(false), + 'Say something nice...'); + assert.equal( + element._computeMessagePlaceholder(true), + 'Add a note for your reviewers...'); + }); + + test('_computeSendButtonLabel', () => { + assert.equal( + element._computeSendButtonLabel(false), + 'Send'); + assert.equal( + element._computeSendButtonLabel(true), + 'Start review'); + }); + + test('_handle400Error reviewrs and CCs', done => { + const error1 = 'error 1'; + const error2 = 'error 2'; + const error3 = 'error 3'; + const text = ')]}\'' + JSON.stringify({ + reviewers: { + username1: { + input: 'user 1', + error: error1, + }, + username2: { + input: 'user 2', + error: error2, + }, + }, + ccs: { + username3: { + input: 'user 3', + error: error3, + }, + }, + }); + element.addEventListener('server-error', e => { + e.detail.response.text().then(text => { + assert.equal(text, [error1, error2, error3].join(', ')); + done(); + }); + }); + element._handle400Error(cloneableResponse(400, text)); + }); + + test('_handle400Error CCs only', done => { + const error1 = 'error 1'; + const text = ')]}\'' + JSON.stringify({ + ccs: { + username1: { + input: 'user 1', + error: error1, + }, + }, + }); + element.addEventListener('server-error', e => { + e.detail.response.text().then(text => { + assert.equal(text, error1); + done(); + }); + }); + element._handle400Error(cloneableResponse(400, text)); + }); + + test('fires height change when the drafts comments load', done => { + // Flush DOM operations before binding to the autogrow event so we don't + // catch the events fired from the initial layout. + flush(() => { + const autoGrowHandler = sinon.stub(); + element.addEventListener('autogrow', autoGrowHandler); + element.draftCommentThreads = []; + flush(() => { + assert.isTrue(autoGrowHandler.called); + done(); + }); + }); + }); + + suite('post review API', () => { + let startReviewStub; + + setup(() => { + startReviewStub = sandbox.stub( + element.$.restAPI, + 'startReview', + () => Promise.resolve()); + }); + + test('ready property in review input on start review', () => { + stubSaveReview(review => { + assert.isTrue(review.ready); return {ready: true}; }); return element.send(true, true).then(() => { - assert.isFalse(element.disabled); + assert.isFalse(startReviewStub.called); }); }); - suite('error handling', () => { - const expectedDraft = 'draft'; - const expectedError = new Error('test'); - - setup(() => { - element.draft = expectedDraft; + test('no ready property in review input on save review', () => { + stubSaveReview(review => { + assert.isUndefined(review.ready); }); - - function assertDialogOpenAndEnabled() { - assert.strictEqual(expectedDraft, element.draft); - assert.isFalse(element.disabled); - } - - test('error occurs in _saveReview', () => { - stubSaveReview(review => { - throw expectedError; - }); - return element.send(true, true).catch(err => { - assert.strictEqual(expectedError, err); - assertDialogOpenAndEnabled(); - }); + return element.send(true, false).then(() => { + assert.isFalse(startReviewStub.called); }); - - suite('pending diff drafts?', () => { - test('yes', () => { - const promise = mockPromise(); - const refreshHandler = sandbox.stub(); - - element.addEventListener('comment-refresh', refreshHandler); - sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true); - element.$.restAPI._pendingRequests.sendDiffDraft = [promise]; - element.open(); - - assert.isFalse(refreshHandler.called); - assert.isTrue(element._savingComments); - - promise.resolve(); - - return element.$.restAPI.awaitPendingDiffDrafts().then(() => { - assert.isTrue(refreshHandler.called); - assert.isFalse(element._savingComments); - }); - }); - - test('no', () => { - sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false); - element.open(); - assert.notOk(element._savingComments); - }); - }); - }); - - test('_computeSendButtonDisabled', () => { - const fn = element._computeSendButtonDisabled.bind(element); - assert.isFalse(fn( - /* buttonLabel= */ 'Start review', - /* draftCommentThreads= */ [], - /* text= */ '', - /* reviewersMutated= */ false, - /* labelsChanged= */ false, - /* includeComments= */ false, - /* disabled= */ false - )); - assert.isTrue(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ [], - /* text= */ '', - /* reviewersMutated= */ false, - /* labelsChanged= */ false, - /* includeComments= */ false, - /* disabled= */ false - )); - // Mock nonempty comment draft array, with seding comments. - assert.isFalse(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ [{comments: [{__draft: true}]}], - /* text= */ '', - /* reviewersMutated= */ false, - /* labelsChanged= */ false, - /* includeComments= */ true, - /* disabled= */ false - )); - // Mock nonempty comment draft array, without seding comments. - assert.isTrue(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ [{comments: [{__draft: true}]}], - /* text= */ '', - /* reviewersMutated= */ false, - /* labelsChanged= */ false, - /* includeComments= */ false, - /* disabled= */ false - )); - // Mock nonempty change message. - assert.isFalse(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ {}, - /* text= */ 'test', - /* reviewersMutated= */ false, - /* labelsChanged= */ false, - /* includeComments= */ false, - /* disabled= */ false - )); - // Mock reviewers mutated. - assert.isFalse(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ {}, - /* text= */ '', - /* reviewersMutated= */ true, - /* labelsChanged= */ false, - /* includeComments= */ false, - /* disabled= */ false - )); - // Mock labels changed. - assert.isFalse(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ {}, - /* text= */ '', - /* reviewersMutated= */ false, - /* labelsChanged= */ true, - /* includeComments= */ false, - /* disabled= */ false - )); - // Whole dialog is disabled. - assert.isTrue(fn( - /* buttonLabel= */ 'Send', - /* draftCommentThreads= */ {}, - /* text= */ '', - /* reviewersMutated= */ false, - /* labelsChanged= */ true, - /* includeComments= */ false, - /* disabled= */ true - )); - }); - - test('_submit blocked when no mutations exist', () => { - const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve()); - // Stub the below function to avoid side effects from the send promise - // resolving. - sandbox.stub(element, '_purgeReviewersPendingRemove'); - element.draftCommentThreads = []; - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button.send')); - assert.isFalse(sendStub.called); - - element.draftCommentThreads = [{comments: [{__draft: true}]}]; - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button.send')); - assert.isTrue(sendStub.called); - }); - - test('getFocusStops', () => { - // Setting draftCommentThreads to an empty object causes _sendDisabled to be - // computed to false. - element.draftCommentThreads = []; - assert.equal(element.getFocusStops().end, element.$.cancelButton); - element.draftCommentThreads = [{comments: [{__draft: true}]}]; - assert.equal(element.getFocusStops().end, element.$.sendButton); - }); - - test('setPluginMessage', () => { - element.setPluginMessage('foo'); - assert.equal(element.$.pluginMessage.textContent, 'foo'); }); }); + + suite('start review and save buttons', () => { + let sendStub; + + setup(() => { + sendStub = sandbox.stub(element, 'send', () => Promise.resolve()); + element.canBeStarted = true; + // Flush to make both Start/Save buttons appear in DOM. + flushAsynchronousOperations(); + }); + + test('start review sets ready', () => { + MockInteractions.tap(element.shadowRoot + .querySelector('.send')); + flushAsynchronousOperations(); + assert.isTrue(sendStub.calledWith(true, true)); + }); + + test('save review doesn\'t set ready', () => { + MockInteractions.tap(element.shadowRoot + .querySelector('.save')); + flushAsynchronousOperations(); + assert.isTrue(sendStub.calledWith(true, false)); + }); + }); + + test('buttons disabled until all API calls are resolved', () => { + stubSaveReview(review => { + return {ready: true}; + }); + return element.send(true, true).then(() => { + assert.isFalse(element.disabled); + }); + }); + + suite('error handling', () => { + const expectedDraft = 'draft'; + const expectedError = new Error('test'); + + setup(() => { + element.draft = expectedDraft; + }); + + function assertDialogOpenAndEnabled() { + assert.strictEqual(expectedDraft, element.draft); + assert.isFalse(element.disabled); + } + + test('error occurs in _saveReview', () => { + stubSaveReview(review => { + throw expectedError; + }); + return element.send(true, true).catch(err => { + assert.strictEqual(expectedError, err); + assertDialogOpenAndEnabled(); + }); + }); + + suite('pending diff drafts?', () => { + test('yes', () => { + const promise = mockPromise(); + const refreshHandler = sandbox.stub(); + + element.addEventListener('comment-refresh', refreshHandler); + sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(true); + element.$.restAPI._pendingRequests.sendDiffDraft = [promise]; + element.open(); + + assert.isFalse(refreshHandler.called); + assert.isTrue(element._savingComments); + + promise.resolve(); + + return element.$.restAPI.awaitPendingDiffDrafts().then(() => { + assert.isTrue(refreshHandler.called); + assert.isFalse(element._savingComments); + }); + }); + + test('no', () => { + sandbox.stub(element.$.restAPI, 'hasPendingDiffDrafts').returns(false); + element.open(); + assert.notOk(element._savingComments); + }); + }); + }); + + test('_computeSendButtonDisabled', () => { + const fn = element._computeSendButtonDisabled.bind(element); + assert.isFalse(fn( + /* buttonLabel= */ 'Start review', + /* draftCommentThreads= */ [], + /* text= */ '', + /* reviewersMutated= */ false, + /* labelsChanged= */ false, + /* includeComments= */ false, + /* disabled= */ false + )); + assert.isTrue(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ [], + /* text= */ '', + /* reviewersMutated= */ false, + /* labelsChanged= */ false, + /* includeComments= */ false, + /* disabled= */ false + )); + // Mock nonempty comment draft array, with seding comments. + assert.isFalse(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ [{comments: [{__draft: true}]}], + /* text= */ '', + /* reviewersMutated= */ false, + /* labelsChanged= */ false, + /* includeComments= */ true, + /* disabled= */ false + )); + // Mock nonempty comment draft array, without seding comments. + assert.isTrue(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ [{comments: [{__draft: true}]}], + /* text= */ '', + /* reviewersMutated= */ false, + /* labelsChanged= */ false, + /* includeComments= */ false, + /* disabled= */ false + )); + // Mock nonempty change message. + assert.isFalse(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ {}, + /* text= */ 'test', + /* reviewersMutated= */ false, + /* labelsChanged= */ false, + /* includeComments= */ false, + /* disabled= */ false + )); + // Mock reviewers mutated. + assert.isFalse(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ {}, + /* text= */ '', + /* reviewersMutated= */ true, + /* labelsChanged= */ false, + /* includeComments= */ false, + /* disabled= */ false + )); + // Mock labels changed. + assert.isFalse(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ {}, + /* text= */ '', + /* reviewersMutated= */ false, + /* labelsChanged= */ true, + /* includeComments= */ false, + /* disabled= */ false + )); + // Whole dialog is disabled. + assert.isTrue(fn( + /* buttonLabel= */ 'Send', + /* draftCommentThreads= */ {}, + /* text= */ '', + /* reviewersMutated= */ false, + /* labelsChanged= */ true, + /* includeComments= */ false, + /* disabled= */ true + )); + }); + + test('_submit blocked when no mutations exist', () => { + const sendStub = sandbox.stub(element, 'send').returns(Promise.resolve()); + // Stub the below function to avoid side effects from the send promise + // resolving. + sandbox.stub(element, '_purgeReviewersPendingRemove'); + element.draftCommentThreads = []; + flushAsynchronousOperations(); + + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button.send')); + assert.isFalse(sendStub.called); + + element.draftCommentThreads = [{comments: [{__draft: true}]}]; + flushAsynchronousOperations(); + + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button.send')); + assert.isTrue(sendStub.called); + }); + + test('getFocusStops', () => { + // Setting draftCommentThreads to an empty object causes _sendDisabled to be + // computed to false. + element.draftCommentThreads = []; + assert.equal(element.getFocusStops().end, element.$.cancelButton); + element.draftCommentThreads = [{comments: [{__draft: true}]}]; + assert.equal(element.getFocusStops().end, element.$.sendButton); + }); + + test('setPluginMessage', () => { + element.setPluginMessage('foo'); + assert.equal(element.$.pluginMessage.textContent, 'foo'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js index ddc6275..a74ec5f 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,276 +14,288 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-account-chip/gr-account-chip.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-reviewer-list_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrReviewerList extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-reviewer-list'; } + /** + * Fired when the "Add reviewer..." button is tapped. + * + * @event show-reply-dialog + */ + + static get properties() { + return { + change: Object, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + mutable: { + type: Boolean, + value: false, + }, + reviewersOnly: { + type: Boolean, + value: false, + }, + ccsOnly: { + type: Boolean, + value: false, + }, + maxReviewersDisplayed: Number, + + _displayedReviewers: { + type: Array, + value() { return []; }, + }, + _reviewers: { + type: Array, + value() { return []; }, + }, + _showInput: { + type: Boolean, + value: false, + }, + _addLabel: { + type: String, + computed: '_computeAddLabel(ccsOnly)', + }, + _hiddenReviewerCount: { + type: Number, + computed: '_computeHiddenCount(_reviewers, _displayedReviewers)', + }, + + // Used for testing. + _lastAutocompleteRequest: Object, + _xhrPromise: Object, + }; + } + + static get observers() { + return [ + '_reviewersChanged(change.reviewers.*, change.owner)', + ]; + } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Converts change.permitted_labels to an array of hashes of label keys to + * numeric scores. + * Example: + * [{ + * 'Code-Review': ['-1', ' 0', '+1'] + * }] + * will be converted to + * [{ + * label: 'Code-Review', + * scores: [-1, 0, 1] + * }] */ - class GrReviewerList extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-reviewer-list'; } - /** - * Fired when the "Add reviewer..." button is tapped. - * - * @event show-reply-dialog - */ - - static get properties() { + _permittedLabelsToNumericScores(labels) { + if (!labels) return []; + return Object.keys(labels).map(label => { return { - change: Object, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - mutable: { - type: Boolean, - value: false, - }, - reviewersOnly: { - type: Boolean, - value: false, - }, - ccsOnly: { - type: Boolean, - value: false, - }, - maxReviewersDisplayed: Number, - - _displayedReviewers: { - type: Array, - value() { return []; }, - }, - _reviewers: { - type: Array, - value() { return []; }, - }, - _showInput: { - type: Boolean, - value: false, - }, - _addLabel: { - type: String, - computed: '_computeAddLabel(ccsOnly)', - }, - _hiddenReviewerCount: { - type: Number, - computed: '_computeHiddenCount(_reviewers, _displayedReviewers)', - }, - - // Used for testing. - _lastAutocompleteRequest: Object, - _xhrPromise: Object, + label, + scores: labels[label].map(v => parseInt(v, 10)), }; - } + }); + } - static get observers() { - return [ - '_reviewersChanged(change.reviewers.*, change.owner)', - ]; - } + /** + * Returns hash of labels to max permitted score. + * + * @param {!Object} change + * @returns {!Object} labels to max permitted scores hash + */ + _getMaxPermittedScores(change) { + return this._permittedLabelsToNumericScores(change.permitted_labels) + .map(({label, scores}) => { + return { + [label]: scores + .map(v => parseInt(v, 10)) + .reduce((a, b) => Math.max(a, b))}; + }) + .reduce((acc, i) => Object.assign(acc, i), {}); + } - /** - * Converts change.permitted_labels to an array of hashes of label keys to - * numeric scores. - * Example: - * [{ - * 'Code-Review': ['-1', ' 0', '+1'] - * }] - * will be converted to - * [{ - * label: 'Code-Review', - * scores: [-1, 0, 1] - * }] - */ - _permittedLabelsToNumericScores(labels) { - if (!labels) return []; - return Object.keys(labels).map(label => { - return { - label, - scores: labels[label].map(v => parseInt(v, 10)), - }; - }); - } - - /** - * Returns hash of labels to max permitted score. - * - * @param {!Object} change - * @returns {!Object} labels to max permitted scores hash - */ - _getMaxPermittedScores(change) { - return this._permittedLabelsToNumericScores(change.permitted_labels) - .map(({label, scores}) => { - return { - [label]: scores - .map(v => parseInt(v, 10)) - .reduce((a, b) => Math.max(a, b))}; - }) - .reduce((acc, i) => Object.assign(acc, i), {}); - } - - /** - * Returns max permitted score for reviewer. - * - * @param {!Object} reviewer - * @param {!Object} change - * @param {string} label - * @return {number} - */ - _getReviewerPermittedScore(reviewer, change, label) { - // Note (issue 7874): sometimes the "all" list is not included in change - // detail responses, even when DETAILED_LABELS is included in options. - if (!change.labels[label].all) { return NaN; } - const detailed = change.labels[label].all.filter( - ({_account_id}) => reviewer._account_id === _account_id).pop(); - if (!detailed) { - return NaN; - } - if (detailed.hasOwnProperty('permitted_voting_range')) { - return detailed.permitted_voting_range.max; - } else if (detailed.hasOwnProperty('value')) { - // If preset, user can vote on the label. - return 0; - } + /** + * Returns max permitted score for reviewer. + * + * @param {!Object} reviewer + * @param {!Object} change + * @param {string} label + * @return {number} + */ + _getReviewerPermittedScore(reviewer, change, label) { + // Note (issue 7874): sometimes the "all" list is not included in change + // detail responses, even when DETAILED_LABELS is included in options. + if (!change.labels[label].all) { return NaN; } + const detailed = change.labels[label].all.filter( + ({_account_id}) => reviewer._account_id === _account_id).pop(); + if (!detailed) { return NaN; } + if (detailed.hasOwnProperty('permitted_voting_range')) { + return detailed.permitted_voting_range.max; + } else if (detailed.hasOwnProperty('value')) { + // If preset, user can vote on the label. + return 0; + } + return NaN; + } - _computeReviewerTooltip(reviewer, change) { - if (!change || !change.labels) { return ''; } - const maxScores = []; - const maxPermitted = this._getMaxPermittedScores(change); - for (const label of Object.keys(change.labels)) { - const maxScore = - this._getReviewerPermittedScore(reviewer, change, label); - if (isNaN(maxScore) || maxScore < 0) { continue; } - if (maxScore > 0 && maxScore === maxPermitted[label]) { - maxScores.push(`${label}: +${maxScore}`); - } else { - maxScores.push(`${label}`); - } - } - if (maxScores.length) { - return 'Votable: ' + maxScores.join(', '); + _computeReviewerTooltip(reviewer, change) { + if (!change || !change.labels) { return ''; } + const maxScores = []; + const maxPermitted = this._getMaxPermittedScores(change); + for (const label of Object.keys(change.labels)) { + const maxScore = + this._getReviewerPermittedScore(reviewer, change, label); + if (isNaN(maxScore) || maxScore < 0) { continue; } + if (maxScore > 0 && maxScore === maxPermitted[label]) { + maxScores.push(`${label}: +${maxScore}`); } else { - return ''; + maxScores.push(`${label}`); } } - - _reviewersChanged(changeRecord, owner) { - // Polymer 2: check for undefined - if ([changeRecord, owner].some(arg => arg === undefined)) { - return; - } - - let result = []; - const reviewers = changeRecord.base; - for (const key in reviewers) { - if (this.reviewersOnly && key !== 'REVIEWER') { - continue; - } - if (this.ccsOnly && key !== 'CC') { - continue; - } - if (key === 'REVIEWER' || key === 'CC') { - result = result.concat(reviewers[key]); - } - } - this._reviewers = result - .filter(reviewer => reviewer._account_id != owner._account_id); - - // If there is one or two more than the max reviewers, don't show the - // 'show more' button, because it takes up just as much space. - if (this.maxReviewersDisplayed && - this._reviewers.length > this.maxReviewersDisplayed + 2) { - this._displayedReviewers = - this._reviewers.slice(0, this.maxReviewersDisplayed); - } else { - this._displayedReviewers = this._reviewers; - } - } - - _computeHiddenCount(reviewers, displayedReviewers) { - // Polymer 2: check for undefined - if ([reviewers, displayedReviewers].some(arg => arg === undefined)) { - return undefined; - } - - return reviewers.length - displayedReviewers.length; - } - - _computeCanRemoveReviewer(reviewer, mutable) { - if (!mutable) { return false; } - - let current; - for (let i = 0; i < this.change.removable_reviewers.length; i++) { - current = this.change.removable_reviewers[i]; - if (current._account_id === reviewer._account_id || - (!reviewer._account_id && current.email === reviewer.email)) { - return true; - } - } - return false; - } - - _handleRemove(e) { - e.preventDefault(); - const target = Polymer.dom(e).rootTarget; - if (!target.account) { return; } - const accountID = target.account._account_id || target.account.email; - this.disabled = true; - this._xhrPromise = this._removeReviewer(accountID).then(response => { - this.disabled = false; - if (!response.ok) { return response; } - - const reviewers = this.change.reviewers; - - for (const type of ['REVIEWER', 'CC']) { - reviewers[type] = reviewers[type] || []; - for (let i = 0; i < reviewers[type].length; i++) { - if (reviewers[type][i]._account_id == accountID || - reviewers[type][i].email == accountID) { - this.splice('change.reviewers.' + type, i, 1); - break; - } - } - } - }) - .catch(err => { - this.disabled = false; - throw err; - }); - } - - _handleAddTap(e) { - e.preventDefault(); - const value = {}; - if (this.reviewersOnly) { - value.reviewersOnly = true; - } - if (this.ccsOnly) { - value.ccsOnly = true; - } - this.fire('show-reply-dialog', {value}); - } - - _handleViewAll(e) { - this._displayedReviewers = this._reviewers; - } - - _removeReviewer(id) { - return this.$.restAPI.removeChangeReviewer(this.change._number, id); - } - - _computeAddLabel(ccsOnly) { - return ccsOnly ? 'Add CC' : 'Add reviewer'; + if (maxScores.length) { + return 'Votable: ' + maxScores.join(', '); + } else { + return ''; } } - customElements.define(GrReviewerList.is, GrReviewerList); -})(); + _reviewersChanged(changeRecord, owner) { + // Polymer 2: check for undefined + if ([changeRecord, owner].some(arg => arg === undefined)) { + return; + } + + let result = []; + const reviewers = changeRecord.base; + for (const key in reviewers) { + if (this.reviewersOnly && key !== 'REVIEWER') { + continue; + } + if (this.ccsOnly && key !== 'CC') { + continue; + } + if (key === 'REVIEWER' || key === 'CC') { + result = result.concat(reviewers[key]); + } + } + this._reviewers = result + .filter(reviewer => reviewer._account_id != owner._account_id); + + // If there is one or two more than the max reviewers, don't show the + // 'show more' button, because it takes up just as much space. + if (this.maxReviewersDisplayed && + this._reviewers.length > this.maxReviewersDisplayed + 2) { + this._displayedReviewers = + this._reviewers.slice(0, this.maxReviewersDisplayed); + } else { + this._displayedReviewers = this._reviewers; + } + } + + _computeHiddenCount(reviewers, displayedReviewers) { + // Polymer 2: check for undefined + if ([reviewers, displayedReviewers].some(arg => arg === undefined)) { + return undefined; + } + + return reviewers.length - displayedReviewers.length; + } + + _computeCanRemoveReviewer(reviewer, mutable) { + if (!mutable) { return false; } + + let current; + for (let i = 0; i < this.change.removable_reviewers.length; i++) { + current = this.change.removable_reviewers[i]; + if (current._account_id === reviewer._account_id || + (!reviewer._account_id && current.email === reviewer.email)) { + return true; + } + } + return false; + } + + _handleRemove(e) { + e.preventDefault(); + const target = dom(e).rootTarget; + if (!target.account) { return; } + const accountID = target.account._account_id || target.account.email; + this.disabled = true; + this._xhrPromise = this._removeReviewer(accountID).then(response => { + this.disabled = false; + if (!response.ok) { return response; } + + const reviewers = this.change.reviewers; + + for (const type of ['REVIEWER', 'CC']) { + reviewers[type] = reviewers[type] || []; + for (let i = 0; i < reviewers[type].length; i++) { + if (reviewers[type][i]._account_id == accountID || + reviewers[type][i].email == accountID) { + this.splice('change.reviewers.' + type, i, 1); + break; + } + } + } + }) + .catch(err => { + this.disabled = false; + throw err; + }); + } + + _handleAddTap(e) { + e.preventDefault(); + const value = {}; + if (this.reviewersOnly) { + value.reviewersOnly = true; + } + if (this.ccsOnly) { + value.ccsOnly = true; + } + this.fire('show-reply-dialog', {value}); + } + + _handleViewAll(e) { + this._displayedReviewers = this._reviewers; + } + + _removeReviewer(id) { + return this.$.restAPI.removeChangeReviewer(this.change._number, id); + } + + _computeAddLabel(ccsOnly) { + return ccsOnly ? 'Add CC' : 'Add reviewer'; + } +} + +customElements.define(GrReviewerList.is, GrReviewerList);
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js index 132ce11..bf7db12 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-reviewer-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -51,26 +44,13 @@ </style> <div class="container"> <template is="dom-repeat" items="[[_displayedReviewers]]" as="reviewer"> - <gr-account-chip class="reviewer" account="[[reviewer]]" - on-remove="_handleRemove" - additional-text="[[_computeReviewerTooltip(reviewer, change)]]" - removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]"> + <gr-account-chip class="reviewer" account="[[reviewer]]" on-remove="_handleRemove" additional-text="[[_computeReviewerTooltip(reviewer, change)]]" removable="[[_computeCanRemoveReviewer(reviewer, mutable)]]"> </gr-account-chip> </template> - <gr-button - class="hiddenReviewers" - link - hidden$="[[!_hiddenReviewerCount]]" - on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button> - <div class="controlsContainer" hidden$="[[!mutable]]"> - <gr-button - link - id="addReviewer" - class="addReviewer" - on-click="_handleAddTap">[[_addLabel]]</gr-button> + <gr-button class="hiddenReviewers" link="" hidden\$="[[!_hiddenReviewerCount]]" on-click="_handleViewAll">and [[_hiddenReviewerCount]] more</gr-button> + <div class="controlsContainer" hidden\$="[[!mutable]]"> + <gr-button link="" id="addReviewer" class="addReviewer" on-click="_handleAddTap">[[_addLabel]]</gr-button> </div> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-reviewer-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html index be65a86..627fa10 100644 --- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-reviewer-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-reviewer-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reviewer-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,78 +40,66 @@ </template> </test-fixture> -<script> - suite('gr-reviewer-list tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reviewer-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-reviewer-list tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - removeChangeReviewer() { - return Promise.resolve({ok: true}); - }, - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + removeChangeReviewer() { + return Promise.resolve({ok: true}); + }, }); + }); - teardown(() => { - sandbox.restore(); + teardown(() => { + sandbox.restore(); + }); + + test('controls hidden on immutable element', () => { + element.mutable = false; + assert.isTrue(element.shadowRoot + .querySelector('.controlsContainer').hasAttribute('hidden')); + element.mutable = true; + assert.isFalse(element.shadowRoot + .querySelector('.controlsContainer').hasAttribute('hidden')); + }); + + test('add reviewer button opens reply dialog', done => { + element.addEventListener('show-reply-dialog', () => { + done(); }); + MockInteractions.tap(element.shadowRoot + .querySelector('.addReviewer')); + }); - test('controls hidden on immutable element', () => { - element.mutable = false; - assert.isTrue(element.shadowRoot - .querySelector('.controlsContainer').hasAttribute('hidden')); - element.mutable = true; - assert.isFalse(element.shadowRoot - .querySelector('.controlsContainer').hasAttribute('hidden')); - }); - - test('add reviewer button opens reply dialog', done => { - element.addEventListener('show-reply-dialog', () => { - done(); - }); - MockInteractions.tap(element.shadowRoot - .querySelector('.addReviewer')); - }); - - test('only show remove for removable reviewers', () => { - element.mutable = true; - element.change = { - owner: { - _account_id: 1, - }, - reviewers: { - REVIEWER: [ - { - _account_id: 2, - name: 'Bojack Horseman', - email: 'SecretariatRulez96@hotmail.com', - }, - { - _account_id: 3, - name: 'Pinky Penguin', - }, - ], - CC: [ - { - _account_id: 4, - name: 'Diane Nguyen', - email: 'macarthurfellow2B@juno.com', - }, - { - email: 'test@e.mail', - }, - ], - }, - removable_reviewers: [ + test('only show remove for removable reviewers', () => { + element.mutable = true; + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + REVIEWER: [ + { + _account_id: 2, + name: 'Bojack Horseman', + email: 'SecretariatRulez96@hotmail.com', + }, { _account_id: 3, name: 'Pinky Penguin', }, + ], + CC: [ { _account_id: 4, name: 'Diane Nguyen', @@ -116,230 +109,245 @@ email: 'test@e.mail', }, ], - }; - flushAsynchronousOperations(); - const chips = - Polymer.dom(element.root).querySelectorAll('gr-account-chip'); - assert.equal(chips.length, 4); + }, + removable_reviewers: [ + { + _account_id: 3, + name: 'Pinky Penguin', + }, + { + _account_id: 4, + name: 'Diane Nguyen', + email: 'macarthurfellow2B@juno.com', + }, + { + email: 'test@e.mail', + }, + ], + }; + flushAsynchronousOperations(); + const chips = + dom(element.root).querySelectorAll('gr-account-chip'); + assert.equal(chips.length, 4); - for (const el of Array.from(chips)) { - const accountID = el.account._account_id || el.account.email; - assert.ok(accountID); + for (const el of Array.from(chips)) { + const accountID = el.account._account_id || el.account.email; + assert.ok(accountID); - const buttonEl = el.shadowRoot - .querySelector('gr-button'); - assert.isNotNull(buttonEl); - if (accountID == 2) { - assert.isTrue(buttonEl.hasAttribute('hidden')); - } else { - assert.isFalse(buttonEl.hasAttribute('hidden')); - } + const buttonEl = el.shadowRoot + .querySelector('gr-button'); + assert.isNotNull(buttonEl); + if (accountID == 2) { + assert.isTrue(buttonEl.hasAttribute('hidden')); + } else { + assert.isFalse(buttonEl.hasAttribute('hidden')); } - }); - - test('tracking reviewers and ccs', () => { - let counter = 0; - function makeAccount() { - return {_account_id: counter++}; - } - - const owner = makeAccount(); - const reviewer = makeAccount(); - const cc = makeAccount(); - const reviewers = { - REMOVED: [makeAccount()], - REVIEWER: [owner, reviewer], - CC: [owner, cc], - }; - - element.ccsOnly = false; - element.reviewersOnly = false; - element.change = { - owner, - reviewers, - }; - assert.deepEqual(element._reviewers, [reviewer, cc]); - - element.reviewersOnly = true; - element.change = { - owner, - reviewers, - }; - assert.deepEqual(element._reviewers, [reviewer]); - - element.ccsOnly = true; - element.reviewersOnly = false; - element.change = { - owner, - reviewers, - }; - assert.deepEqual(element._reviewers, [cc]); - }); - - test('_handleAddTap passes mode with event', () => { - const fireStub = sandbox.stub(element, 'fire'); - const e = {preventDefault() {}}; - - element.ccsOnly = false; - element.reviewersOnly = false; - element._handleAddTap(e); - assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}})); - - element.reviewersOnly = true; - element._handleAddTap(e); - assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog', - {value: {reviewersOnly: true}})); - - element.ccsOnly = true; - element.reviewersOnly = false; - element._handleAddTap(e); - assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog', - {value: {ccsOnly: true}})); - }); - - test('no show all reviewers button with 6 reviewers', () => { - const reviewers = []; - element.maxReviewersDisplayed = 5; - for (let i = 0; i < 6; i++) { - reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); - } - element.ccsOnly = true; - - element.change = { - owner: { - _account_id: 1, - }, - reviewers: { - CC: reviewers, - }, - }; - assert.equal(element._hiddenReviewerCount, 0); - assert.equal(element._displayedReviewers.length, 6); - assert.equal(element._reviewers.length, 6); - assert.isTrue(element.shadowRoot - .querySelector('.hiddenReviewers').hidden); - }); - - test('show all reviewers button with 8 reviewers', () => { - const reviewers = []; - element.maxReviewersDisplayed = 5; - for (let i = 0; i < 8; i++) { - reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); - } - element.ccsOnly = true; - - element.change = { - owner: { - _account_id: 1, - }, - reviewers: { - CC: reviewers, - }, - }; - assert.equal(element._hiddenReviewerCount, 3); - assert.equal(element._displayedReviewers.length, 5); - assert.equal(element._reviewers.length, 8); - assert.isFalse(element.shadowRoot - .querySelector('.hiddenReviewers').hidden); - }); - - test('no maxReviewersDisplayed', () => { - const reviewers = []; - for (let i = 0; i < 7; i++) { - reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); - } - element.ccsOnly = true; - - element.change = { - owner: { - _account_id: 1, - }, - reviewers: { - CC: reviewers, - }, - }; - assert.equal(element._hiddenReviewerCount, 0); - assert.equal(element._displayedReviewers.length, 7); - assert.equal(element._reviewers.length, 7); - assert.isTrue(element.shadowRoot - .querySelector('.hiddenReviewers').hidden); - }); - - test('show all reviewers button', () => { - const reviewers = []; - element.maxReviewersDisplayed = 5; - for (let i = 0; i < 100; i++) { - reviewers.push( - {email: i+'reviewer@google.com', name: 'reviewer-' + i}); - } - element.ccsOnly = true; - - element.change = { - owner: { - _account_id: 1, - }, - reviewers: { - CC: reviewers, - }, - }; - assert.equal(element._hiddenReviewerCount, 95); - assert.equal(element._displayedReviewers.length, 5); - assert.equal(element._reviewers.length, 100); - assert.isFalse(element.shadowRoot - .querySelector('.hiddenReviewers').hidden); - - MockInteractions.tap(element.shadowRoot - .querySelector('.hiddenReviewers')); - - assert.equal(element._hiddenReviewerCount, 0); - assert.equal(element._displayedReviewers.length, 100); - assert.equal(element._reviewers.length, 100); - assert.isTrue(element.shadowRoot - .querySelector('.hiddenReviewers').hidden); - }); - - test('votable labels', () => { - const change = { - labels: { - Foo: { - all: [{_account_id: 7, permitted_voting_range: {max: 2}}], - }, - Bar: { - all: [{_account_id: 1, permitted_voting_range: {max: 1}}, - {_account_id: 7, permitted_voting_range: {max: 1}}], - }, - FooBar: { - all: [{_account_id: 7, value: 0}], - }, - }, - permitted_labels: { - Foo: ['-1', ' 0', '+1', '+2'], - FooBar: ['-1', ' 0'], - }, - }; - assert.strictEqual( - element._computeReviewerTooltip({_account_id: 1}, change), - 'Votable: Bar'); - assert.strictEqual( - element._computeReviewerTooltip({_account_id: 7}, change), - 'Votable: Foo: +2, Bar, FooBar'); - assert.strictEqual( - element._computeReviewerTooltip({_account_id: 2}, change), - ''); - }); - - test('fails gracefully when all is not included', () => { - const change = { - labels: {Foo: {}}, - permitted_labels: { - Foo: ['-1', ' 0', '+1', '+2'], - }, - }; - assert.strictEqual( - element._computeReviewerTooltip({_account_id: 1}, change), ''); - }); + } }); + + test('tracking reviewers and ccs', () => { + let counter = 0; + function makeAccount() { + return {_account_id: counter++}; + } + + const owner = makeAccount(); + const reviewer = makeAccount(); + const cc = makeAccount(); + const reviewers = { + REMOVED: [makeAccount()], + REVIEWER: [owner, reviewer], + CC: [owner, cc], + }; + + element.ccsOnly = false; + element.reviewersOnly = false; + element.change = { + owner, + reviewers, + }; + assert.deepEqual(element._reviewers, [reviewer, cc]); + + element.reviewersOnly = true; + element.change = { + owner, + reviewers, + }; + assert.deepEqual(element._reviewers, [reviewer]); + + element.ccsOnly = true; + element.reviewersOnly = false; + element.change = { + owner, + reviewers, + }; + assert.deepEqual(element._reviewers, [cc]); + }); + + test('_handleAddTap passes mode with event', () => { + const fireStub = sandbox.stub(element, 'fire'); + const e = {preventDefault() {}}; + + element.ccsOnly = false; + element.reviewersOnly = false; + element._handleAddTap(e); + assert.isTrue(fireStub.calledWith('show-reply-dialog', {value: {}})); + + element.reviewersOnly = true; + element._handleAddTap(e); + assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog', + {value: {reviewersOnly: true}})); + + element.ccsOnly = true; + element.reviewersOnly = false; + element._handleAddTap(e); + assert.isTrue(fireStub.lastCall.calledWith('show-reply-dialog', + {value: {ccsOnly: true}})); + }); + + test('no show all reviewers button with 6 reviewers', () => { + const reviewers = []; + element.maxReviewersDisplayed = 5; + for (let i = 0; i < 6; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + assert.equal(element._hiddenReviewerCount, 0); + assert.equal(element._displayedReviewers.length, 6); + assert.equal(element._reviewers.length, 6); + assert.isTrue(element.shadowRoot + .querySelector('.hiddenReviewers').hidden); + }); + + test('show all reviewers button with 8 reviewers', () => { + const reviewers = []; + element.maxReviewersDisplayed = 5; + for (let i = 0; i < 8; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + assert.equal(element._hiddenReviewerCount, 3); + assert.equal(element._displayedReviewers.length, 5); + assert.equal(element._reviewers.length, 8); + assert.isFalse(element.shadowRoot + .querySelector('.hiddenReviewers').hidden); + }); + + test('no maxReviewersDisplayed', () => { + const reviewers = []; + for (let i = 0; i < 7; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + assert.equal(element._hiddenReviewerCount, 0); + assert.equal(element._displayedReviewers.length, 7); + assert.equal(element._reviewers.length, 7); + assert.isTrue(element.shadowRoot + .querySelector('.hiddenReviewers').hidden); + }); + + test('show all reviewers button', () => { + const reviewers = []; + element.maxReviewersDisplayed = 5; + for (let i = 0; i < 100; i++) { + reviewers.push( + {email: i+'reviewer@google.com', name: 'reviewer-' + i}); + } + element.ccsOnly = true; + + element.change = { + owner: { + _account_id: 1, + }, + reviewers: { + CC: reviewers, + }, + }; + assert.equal(element._hiddenReviewerCount, 95); + assert.equal(element._displayedReviewers.length, 5); + assert.equal(element._reviewers.length, 100); + assert.isFalse(element.shadowRoot + .querySelector('.hiddenReviewers').hidden); + + MockInteractions.tap(element.shadowRoot + .querySelector('.hiddenReviewers')); + + assert.equal(element._hiddenReviewerCount, 0); + assert.equal(element._displayedReviewers.length, 100); + assert.equal(element._reviewers.length, 100); + assert.isTrue(element.shadowRoot + .querySelector('.hiddenReviewers').hidden); + }); + + test('votable labels', () => { + const change = { + labels: { + Foo: { + all: [{_account_id: 7, permitted_voting_range: {max: 2}}], + }, + Bar: { + all: [{_account_id: 1, permitted_voting_range: {max: 1}}, + {_account_id: 7, permitted_voting_range: {max: 1}}], + }, + FooBar: { + all: [{_account_id: 7, value: 0}], + }, + }, + permitted_labels: { + Foo: ['-1', ' 0', '+1', '+2'], + FooBar: ['-1', ' 0'], + }, + }; + assert.strictEqual( + element._computeReviewerTooltip({_account_id: 1}, change), + 'Votable: Bar'); + assert.strictEqual( + element._computeReviewerTooltip({_account_id: 7}, change), + 'Votable: Foo: +2, Bar, FooBar'); + assert.strictEqual( + element._computeReviewerTooltip({_account_id: 2}, change), + ''); + }); + + test('fails gracefully when all is not included', () => { + const change = { + labels: {Foo: {}}, + permitted_labels: { + Foo: ['-1', ' 0', '+1', '+2'], + }, + }; + assert.strictEqual( + element._computeReviewerTooltip({_account_id: 1}, change), ''); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js index e99367e..850bfb4 100644 --- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js +++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list.js
@@ -14,200 +14,209 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-comment-thread/gr-comment-thread.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-thread-list_html.js'; + +/** + * Fired when a comment is saved or deleted + * + * @event thread-list-modified + * @extends Polymer.Element + */ +const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff ' + + 'for this change.'; +const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' + + 'patchset.'; +const FINDINGS_TAB_NAME = '__gerrit_internal_findings'; + +class GrThreadList extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-thread-list'; } + + static get properties() { + return { + /** @type {?} */ + change: Object, + threads: Array, + changeNum: String, + loggedIn: Boolean, + _sortedThreads: { + type: Array, + }, + _filteredThreads: { + type: Array, + computed: '_computeFilteredThreads(_sortedThreads, ' + + '_unresolvedOnly, _draftsOnly,' + + 'onlyShowRobotCommentsWithHumanReply)', + }, + _unresolvedOnly: { + type: Boolean, + value: false, + }, + _draftsOnly: { + type: Boolean, + value: false, + }, + /* Boolean properties used must default to false if passed as attribute + by the parent */ + onlyShowRobotCommentsWithHumanReply: { + type: Boolean, + value: false, + }, + hideToggleButtons: { + type: Boolean, + value: false, + }, + tab: { + type: String, + value: '', + }, + }; + } + + static get observers() { return ['_computeSortedThreads(threads.*)']; } + + _computeShowDraftToggle(loggedIn) { + return loggedIn ? 'show' : ''; + } + + _computeNoThreadsMessage(tab) { + if (tab === FINDINGS_TAB_NAME) { + return NO_ROBOT_COMMENTS_THREADS_MESSAGE; + } + return NO_THREADS_MESSAGE; + } /** - * Fired when a comment is saved or deleted + * Order as follows: + * - Unresolved threads with drafts (reverse chronological) + * - Unresolved threads without drafts (reverse chronological) + * - Resolved threads with drafts (reverse chronological) + * - Resolved threads without drafts (reverse chronological) * - * @event thread-list-modified - * @extends Polymer.Element + * @param {!Object} changeRecord */ - const NO_THREADS_MESSAGE = 'There are no inline comment threads on any diff ' - + 'for this change.'; - const NO_ROBOT_COMMENTS_THREADS_MESSAGE = 'There are no findings for this ' + - 'patchset.'; - const FINDINGS_TAB_NAME = '__gerrit_internal_findings'; + _computeSortedThreads(changeRecord) { + const threads = changeRecord.base; + if (!threads) { return []; } + this._updateSortedThreads(threads); + } - class GrThreadList extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-thread-list'; } - - static get properties() { - return { - /** @type {?} */ - change: Object, - threads: Array, - changeNum: String, - loggedIn: Boolean, - _sortedThreads: { - type: Array, - }, - _filteredThreads: { - type: Array, - computed: '_computeFilteredThreads(_sortedThreads, ' + - '_unresolvedOnly, _draftsOnly,' + - 'onlyShowRobotCommentsWithHumanReply)', - }, - _unresolvedOnly: { - type: Boolean, - value: false, - }, - _draftsOnly: { - type: Boolean, - value: false, - }, - /* Boolean properties used must default to false if passed as attribute - by the parent */ - onlyShowRobotCommentsWithHumanReply: { - type: Boolean, - value: false, - }, - hideToggleButtons: { - type: Boolean, - value: false, - }, - tab: { - type: String, - value: '', - }, - }; - } - - static get observers() { return ['_computeSortedThreads(threads.*)']; } - - _computeShowDraftToggle(loggedIn) { - return loggedIn ? 'show' : ''; - } - - _computeNoThreadsMessage(tab) { - if (tab === FINDINGS_TAB_NAME) { - return NO_ROBOT_COMMENTS_THREADS_MESSAGE; - } - return NO_THREADS_MESSAGE; - } - - /** - * Order as follows: - * - Unresolved threads with drafts (reverse chronological) - * - Unresolved threads without drafts (reverse chronological) - * - Resolved threads with drafts (reverse chronological) - * - Resolved threads without drafts (reverse chronological) - * - * @param {!Object} changeRecord - */ - _computeSortedThreads(changeRecord) { - const threads = changeRecord.base; - if (!threads) { return []; } - this._updateSortedThreads(threads); - } - - _updateSortedThreads(threads) { - this._sortedThreads = - threads.map(this._getThreadWithSortInfo).sort((c1, c2) => { - const c1Date = c1.__date || util.parseDate(c1.updated); - const c2Date = c2.__date || util.parseDate(c2.updated); - const dateCompare = c2Date - c1Date; - if (c2.unresolved || c1.unresolved) { - if (!c1.unresolved) { return 1; } - if (!c2.unresolved) { return -1; } - } - if (c2.hasDraft || c1.hasDraft) { - if (!c1.hasDraft) { return 1; } - if (!c2.hasDraft) { return -1; } - } - - if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { - return 0; - } - return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); - }); - } - - _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly, - onlyShowRobotCommentsWithHumanReply) { - // Polymer 2: check for undefined - if ([ - sortedThreads, - unresolvedOnly, - draftsOnly, - onlyShowRobotCommentsWithHumanReply, - ].some(arg => arg === undefined)) { - return undefined; - } - - return sortedThreads.filter(c => { - if (draftsOnly) { - return c.hasDraft; - } else if (unresolvedOnly) { - return c.unresolved; - } else { - const comments = c && c.thread && c.thread.comments; - let robotComment = false; - let humanReplyToRobotComment = false; - comments.forEach(comment => { - if (comment.robot_id) { - robotComment = true; - } else if (robotComment) { - // Robot comment exists and human comment exists after it - humanReplyToRobotComment = true; - } - }); - if (robotComment && onlyShowRobotCommentsWithHumanReply) { - return humanReplyToRobotComment; + _updateSortedThreads(threads) { + this._sortedThreads = + threads.map(this._getThreadWithSortInfo).sort((c1, c2) => { + const c1Date = c1.__date || util.parseDate(c1.updated); + const c2Date = c2.__date || util.parseDate(c2.updated); + const dateCompare = c2Date - c1Date; + if (c2.unresolved || c1.unresolved) { + if (!c1.unresolved) { return 1; } + if (!c2.unresolved) { return -1; } } - return c; - } - }).map(threadInfo => threadInfo.thread); + if (c2.hasDraft || c1.hasDraft) { + if (!c1.hasDraft) { return 1; } + if (!c2.hasDraft) { return -1; } + } + + if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { + return 0; + } + return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); + }); + } + + _computeFilteredThreads(sortedThreads, unresolvedOnly, draftsOnly, + onlyShowRobotCommentsWithHumanReply) { + // Polymer 2: check for undefined + if ([ + sortedThreads, + unresolvedOnly, + draftsOnly, + onlyShowRobotCommentsWithHumanReply, + ].some(arg => arg === undefined)) { + return undefined; } - _getThreadWithSortInfo(thread) { - const lastComment = thread.comments[thread.comments.length - 1] || {}; - - const lastNonDraftComment = - (lastComment.__draft && thread.comments.length > 1) ? - thread.comments[thread.comments.length - 2] : - lastComment; - - return { - thread, - // Use the unresolved bit for the last non draft comment. This is what - // anybody other than the current user would see. - unresolved: !!lastNonDraftComment.unresolved, - hasDraft: !!lastComment.__draft, - updated: lastComment.updated, - }; - } - - removeThread(rootId) { - for (let i = 0; i < this.threads.length; i++) { - if (this.threads[i].rootId === rootId) { - this.splice('threads', i, 1); - // Needed to ensure threads get re-rendered in the correct order. - Polymer.dom.flush(); - return; + return sortedThreads.filter(c => { + if (draftsOnly) { + return c.hasDraft; + } else if (unresolvedOnly) { + return c.unresolved; + } else { + const comments = c && c.thread && c.thread.comments; + let robotComment = false; + let humanReplyToRobotComment = false; + comments.forEach(comment => { + if (comment.robot_id) { + robotComment = true; + } else if (robotComment) { + // Robot comment exists and human comment exists after it + humanReplyToRobotComment = true; + } + }); + if (robotComment && onlyShowRobotCommentsWithHumanReply) { + return humanReplyToRobotComment; } + return c; } - } + }).map(threadInfo => threadInfo.thread); + } - _handleThreadDiscard(e) { - this.removeThread(e.detail.rootId); - } + _getThreadWithSortInfo(thread) { + const lastComment = thread.comments[thread.comments.length - 1] || {}; - _handleCommentsChanged(e) { - // Reset threads so thread computations occur on deep array changes to - // threads comments that are not observed naturally. - this._updateSortedThreads(this.threads); + const lastNonDraftComment = + (lastComment.__draft && thread.comments.length > 1) ? + thread.comments[thread.comments.length - 2] : + lastComment; - this.dispatchEvent(new CustomEvent('thread-list-modified', - {detail: {rootId: e.detail.rootId, path: e.detail.path}})); - } + return { + thread, + // Use the unresolved bit for the last non draft comment. This is what + // anybody other than the current user would see. + unresolved: !!lastNonDraftComment.unresolved, + hasDraft: !!lastComment.__draft, + updated: lastComment.updated, + }; + } - _isOnParent(side) { - return !!side; + removeThread(rootId) { + for (let i = 0; i < this.threads.length; i++) { + if (this.threads[i].rootId === rootId) { + this.splice('threads', i, 1); + // Needed to ensure threads get re-rendered in the correct order. + flush(); + return; + } } } - customElements.define(GrThreadList.is, GrThreadList); -})(); + _handleThreadDiscard(e) { + this.removeThread(e.detail.rootId); + } + + _handleCommentsChanged(e) { + // Reset threads so thread computations occur on deep array changes to + // threads comments that are not observed naturally. + this._updateSortedThreads(this.threads); + + this.dispatchEvent(new CustomEvent('thread-list-modified', + {detail: {rootId: e.detail.rootId, path: e.detail.path}})); + } + + _isOnParent(side) { + return !!side; + } +} + +customElements.define(GrThreadList.is, GrThreadList);
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js index f04a39a..b9f0b01 100644 --- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js +++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html"> - -<dom-module id="gr-thread-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> #threads { display: block; @@ -62,14 +57,10 @@ <template is="dom-if" if="[[!hideToggleButtons]]"> <div class="header"> <div class="toggleItem"> - <paper-toggle-button - id="unresolvedToggle" - checked="{{_unresolvedOnly}}"></paper-toggle-button> + <paper-toggle-button id="unresolvedToggle" checked="{{_unresolvedOnly}}"></paper-toggle-button> Only unresolved threads</div> - <div class$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]"> - <paper-toggle-button - id="draftToggle" - checked="{{_draftsOnly}}"></paper-toggle-button> + <div class\$="toggleItem draftToggle [[_computeShowDraftToggle(loggedIn)]]"> + <paper-toggle-button id="draftToggle" checked="{{_draftsOnly}}"></paper-toggle-button> Only threads with drafts</div> </div> </template> @@ -77,27 +68,8 @@ <template is="dom-if" if="[[!threads.length]]"> [[_computeNoThreadsMessage(tab)]] </template> - <template - is="dom-repeat" - items="[[_filteredThreads]]" - as="thread" - initial-count="5" - target-framerate="60"> - <gr-comment-thread - show-file-path - change-num="[[changeNum]]" - comments="[[thread.comments]]" - comment-side="[[thread.commentSide]]" - project-name="[[change.project]]" - is-on-parent="[[_isOnParent(thread.commentSide)]]" - line-num="[[thread.line]]" - patch-num="[[thread.patchNum]]" - path="[[thread.path]]" - root-id="{{thread.rootId}}" - on-thread-changed="_handleCommentsChanged" - on-thread-discard="_handleThreadDiscard"></gr-comment-thread> + <template is="dom-repeat" items="[[_filteredThreads]]" as="thread" initial-count="5" target-framerate="60"> + <gr-comment-thread show-file-path="" change-num="[[changeNum]]" comments="[[thread.comments]]" comment-side="[[thread.commentSide]]" project-name="[[change.project]]" is-on-parent="[[_isOnParent(thread.commentSide)]]" line-num="[[thread.line]]" patch-num="[[thread.patchNum]]" path="[[thread.path]]" root-id="{{thread.rootId}}" on-thread-changed="_handleCommentsChanged" on-thread-discard="_handleThreadDiscard"></gr-comment-thread> </template> </div> - </template> - <script src="gr-thread-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html index 30c598a..a2c3620 100644 --- a/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html +++ b/polygerrit-ui/app/elements/change/gr-thread-list/gr-thread-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-thread-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-thread-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-thread-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-thread-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,338 +40,341 @@ </template> </test-fixture> -<script> - suite('gr-thread-list tests', async () => { - await readyToTest(); - let element; - let sandbox; - let threadElements; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-thread-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-thread-list tests', () => { + let element; + let sandbox; + let threadElements; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.onlyShowRobotCommentsWithHumanReply = true; - element.threads = [ - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'ecf0b9fa_fe1a5f62', - line: 5, - updated: '2018-02-08 18:49:18.000000000', - message: 'test', - unresolved: true, + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.onlyShowRobotCommentsWithHumanReply = true; + element.threads = [ + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - { - id: '503008e2_0ab203ee', - path: '/COMMIT_MSG', - line: 5, - in_reply_to: 'ecf0b9fa_fe1a5f62', - updated: '2018-02-13 22:48:48.018000000', - message: 'draft', - unresolved: false, - __draft: true, - __draftID: '0.m683trwff68', - __editing: false, - patch_set: '2', + patch_set: 4, + id: 'ecf0b9fa_fe1a5f62', + line: 5, + updated: '2018-02-08 18:49:18.000000000', + message: 'test', + unresolved: true, + }, + { + id: '503008e2_0ab203ee', + path: '/COMMIT_MSG', + line: 5, + in_reply_to: 'ecf0b9fa_fe1a5f62', + updated: '2018-02-13 22:48:48.018000000', + message: 'draft', + unresolved: false, + __draft: true, + __draftID: '0.m683trwff68', + __editing: false, + patch_set: '2', + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 5, + rootId: 'ecf0b9fa_fe1a5f62', + start_datetime: '2018-02-08 18:49:18.000000000', + }, + { + comments: [ + { + __path: 'test.txt', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 5, - rootId: 'ecf0b9fa_fe1a5f62', - start_datetime: '2018-02-08 18:49:18.000000000', - }, - { - comments: [ - { - __path: 'test.txt', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 3, - id: '09a9fb0a_1484e6cf', - side: 'PARENT', - updated: '2018-02-13 22:47:19.000000000', - message: 'Some comment on another patchset.', - unresolved: false, + patch_set: 3, + id: '09a9fb0a_1484e6cf', + side: 'PARENT', + updated: '2018-02-13 22:47:19.000000000', + message: 'Some comment on another patchset.', + unresolved: false, + }, + ], + patchNum: 3, + path: 'test.txt', + rootId: '09a9fb0a_1484e6cf', + start_datetime: '2018-02-13 22:47:19.000000000', + commentSide: 'PARENT', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - ], - patchNum: 3, - path: 'test.txt', - rootId: '09a9fb0a_1484e6cf', - start_datetime: '2018-02-13 22:47:19.000000000', - commentSide: 'PARENT', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 2, - id: '8caddf38_44770ec1', - line: 4, - updated: '2018-02-13 22:48:40.000000000', - message: 'Another unresolved comment', - unresolved: true, + patch_set: 2, + id: '8caddf38_44770ec1', + line: 4, + updated: '2018-02-13 22:48:40.000000000', + message: 'Another unresolved comment', + unresolved: true, + }, + ], + patchNum: 2, + path: '/COMMIT_MSG', + line: 4, + rootId: '8caddf38_44770ec1', + start_datetime: '2018-02-13 22:48:40.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - ], - patchNum: 2, - path: '/COMMIT_MSG', - line: 4, - rootId: '8caddf38_44770ec1', - start_datetime: '2018-02-13 22:48:40.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 2, - id: 'scaddf38_44770ec1', - line: 4, - updated: '2018-02-14 22:48:40.000000000', - message: 'Yet another unresolved comment', - unresolved: true, + patch_set: 2, + id: 'scaddf38_44770ec1', + line: 4, + updated: '2018-02-14 22:48:40.000000000', + message: 'Yet another unresolved comment', + unresolved: true, + }, + ], + patchNum: 2, + path: '/COMMIT_MSG', + line: 4, + rootId: 'scaddf38_44770ec1', + start_datetime: '2018-02-14 22:48:40.000000000', + }, + { + comments: [ + { + id: 'zcf0b9fa_fe1a5f62', + path: '/COMMIT_MSG', + line: 6, + updated: '2018-02-15 22:48:48.018000000', + message: 'resolved draft', + unresolved: false, + __draft: true, + __draftID: '0.m683trwff68', + __editing: false, + patch_set: '2', + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 6, + rootId: 'zcf0b9fa_fe1a5f62', + start_datetime: '2018-02-09 18:49:18.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - ], - patchNum: 2, - path: '/COMMIT_MSG', - line: 4, - rootId: 'scaddf38_44770ec1', - start_datetime: '2018-02-14 22:48:40.000000000', - }, - { - comments: [ - { - id: 'zcf0b9fa_fe1a5f62', - path: '/COMMIT_MSG', - line: 6, - updated: '2018-02-15 22:48:48.018000000', - message: 'resolved draft', - unresolved: false, - __draft: true, - __draftID: '0.m683trwff68', - __editing: false, - patch_set: '2', + patch_set: 4, + id: 'rc1', + line: 5, + updated: '2019-02-08 18:49:18.000000000', + message: 'test', + unresolved: true, + robot_id: 'rc1', + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 5, + rootId: 'rc1', + start_datetime: '2019-02-08 18:49:18.000000000', + }, + { + comments: [ + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 6, - rootId: 'zcf0b9fa_fe1a5f62', - start_datetime: '2018-02-09 18:49:18.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'rc1', - line: 5, - updated: '2019-02-08 18:49:18.000000000', - message: 'test', - unresolved: true, - robot_id: 'rc1', + patch_set: 4, + id: 'rc2', + line: 5, + updated: '2019-03-08 18:49:18.000000000', + message: 'test', + unresolved: true, + robot_id: 'rc2', + }, + { + __path: '/COMMIT_MSG', + author: { + _account_id: 1000000, + name: 'user', + username: 'user', }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 5, - rootId: 'rc1', - start_datetime: '2019-02-08 18:49:18.000000000', - }, - { - comments: [ - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'rc2', - line: 5, - updated: '2019-03-08 18:49:18.000000000', - message: 'test', - unresolved: true, - robot_id: 'rc2', - }, - { - __path: '/COMMIT_MSG', - author: { - _account_id: 1000000, - name: 'user', - username: 'user', - }, - patch_set: 4, - id: 'c2_1', - line: 5, - updated: '2019-03-08 18:49:18.000000000', - message: 'test', - unresolved: true, - }, - ], - patchNum: 4, - path: '/COMMIT_MSG', - line: 5, - rootId: 'rc2', - start_datetime: '2019-03-08 18:49:18.000000000', - }, - ]; - flushAsynchronousOperations(); - threadElements = Polymer.dom(element.root) - .querySelectorAll('gr-comment-thread'); - }); + patch_set: 4, + id: 'c2_1', + line: 5, + updated: '2019-03-08 18:49:18.000000000', + message: 'test', + unresolved: true, + }, + ], + patchNum: 4, + path: '/COMMIT_MSG', + line: 5, + rootId: 'rc2', + start_datetime: '2019-03-08 18:49:18.000000000', + }, + ]; + flushAsynchronousOperations(); + threadElements = dom(element.root) + .querySelectorAll('gr-comment-thread'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('draft toggle only appears when logged in', () => { - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.draftToggle')).display, - 'none'); - element.loggedIn = true; - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.draftToggle')).display, - 'none'); - }); + test('draft toggle only appears when logged in', () => { + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.draftToggle')).display, + 'none'); + element.loggedIn = true; + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.draftToggle')).display, + 'none'); + }); - test('there are five threads by default', () => { - assert.equal(Polymer.dom(element.root) - .querySelectorAll('gr-comment-thread').length, 5); - }); + test('there are five threads by default', () => { + assert.equal(dom(element.root) + .querySelectorAll('gr-comment-thread').length, 5); + }); - test('_computeSortedThreads', () => { - assert.equal(element._sortedThreads.length, 7); - // Draft and unresolved - assert.equal(element._sortedThreads[0].thread.rootId, - 'ecf0b9fa_fe1a5f62'); - // Unresolved robot comment - assert.equal(element._sortedThreads[1].thread.rootId, - 'rc2'); - // Unresolved robot comment - assert.equal(element._sortedThreads[2].thread.rootId, - 'rc1'); - // unresolved - assert.equal(element._sortedThreads[3].thread.rootId, - 'scaddf38_44770ec1'); - // unresolved - assert.equal(element._sortedThreads[4].thread.rootId, - '8caddf38_44770ec1'); - // resolved and draft - assert.equal(element._sortedThreads[5].thread.rootId, - 'zcf0b9fa_fe1a5f62'); - // resolved - assert.equal(element._sortedThreads[6].thread.rootId, - '09a9fb0a_1484e6cf'); - }); + test('_computeSortedThreads', () => { + assert.equal(element._sortedThreads.length, 7); + // Draft and unresolved + assert.equal(element._sortedThreads[0].thread.rootId, + 'ecf0b9fa_fe1a5f62'); + // Unresolved robot comment + assert.equal(element._sortedThreads[1].thread.rootId, + 'rc2'); + // Unresolved robot comment + assert.equal(element._sortedThreads[2].thread.rootId, + 'rc1'); + // unresolved + assert.equal(element._sortedThreads[3].thread.rootId, + 'scaddf38_44770ec1'); + // unresolved + assert.equal(element._sortedThreads[4].thread.rootId, + '8caddf38_44770ec1'); + // resolved and draft + assert.equal(element._sortedThreads[5].thread.rootId, + 'zcf0b9fa_fe1a5f62'); + // resolved + assert.equal(element._sortedThreads[6].thread.rootId, + '09a9fb0a_1484e6cf'); + }); - test('filtered threads do not contain robot comments without reply', () => { - const thread = element.threads.find(thread => thread.rootId === 'rc1'); - assert.equal(element._filteredThreads.includes(thread), false); - }); + test('filtered threads do not contain robot comments without reply', () => { + const thread = element.threads.find(thread => thread.rootId === 'rc1'); + assert.equal(element._filteredThreads.includes(thread), false); + }); - test('filtered threads contains robot comments with reply', () => { - const thread = element.threads.find(thread => thread.rootId === 'rc2'); - assert.equal(element._filteredThreads.includes(thread), true); - }); + test('filtered threads contains robot comments with reply', () => { + const thread = element.threads.find(thread => thread.rootId === 'rc2'); + assert.equal(element._filteredThreads.includes(thread), true); + }); - test('thread removal', () => { - threadElements[1].fire('thread-discard', {rootId: 'rc2'}); - flushAsynchronousOperations(); - assert.equal(element._sortedThreads.length, 6); - assert.equal(element._sortedThreads[0].thread.rootId, - 'ecf0b9fa_fe1a5f62'); - // Unresolved robot comment - assert.equal(element._sortedThreads[1].thread.rootId, - 'rc1'); - // unresolved - assert.equal(element._sortedThreads[2].thread.rootId, - 'scaddf38_44770ec1'); - // unresolved - assert.equal(element._sortedThreads[3].thread.rootId, - '8caddf38_44770ec1'); - // resolved and draft - assert.equal(element._sortedThreads[4].thread.rootId, - 'zcf0b9fa_fe1a5f62'); - // resolved - assert.equal(element._sortedThreads[5].thread.rootId, - '09a9fb0a_1484e6cf'); - }); + test('thread removal', () => { + threadElements[1].fire('thread-discard', {rootId: 'rc2'}); + flushAsynchronousOperations(); + assert.equal(element._sortedThreads.length, 6); + assert.equal(element._sortedThreads[0].thread.rootId, + 'ecf0b9fa_fe1a5f62'); + // Unresolved robot comment + assert.equal(element._sortedThreads[1].thread.rootId, + 'rc1'); + // unresolved + assert.equal(element._sortedThreads[2].thread.rootId, + 'scaddf38_44770ec1'); + // unresolved + assert.equal(element._sortedThreads[3].thread.rootId, + '8caddf38_44770ec1'); + // resolved and draft + assert.equal(element._sortedThreads[4].thread.rootId, + 'zcf0b9fa_fe1a5f62'); + // resolved + assert.equal(element._sortedThreads[5].thread.rootId, + '09a9fb0a_1484e6cf'); + }); - test('toggle unresolved only shows unresolved comments', () => { - MockInteractions.tap(element.shadowRoot.querySelector( - '#unresolvedToggle')); - flushAsynchronousOperations(); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('gr-comment-thread').length, 5); - }); + test('toggle unresolved only shows unresolved comments', () => { + MockInteractions.tap(element.shadowRoot.querySelector( + '#unresolvedToggle')); + flushAsynchronousOperations(); + assert.equal(dom(element.root) + .querySelectorAll('gr-comment-thread').length, 5); + }); - test('toggle drafts only shows threads with draft comments', () => { - MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle')); - flushAsynchronousOperations(); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('gr-comment-thread').length, 2); - }); + test('toggle drafts only shows threads with draft comments', () => { + MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle')); + flushAsynchronousOperations(); + assert.equal(dom(element.root) + .querySelectorAll('gr-comment-thread').length, 2); + }); - test('toggle drafts and unresolved only shows threads with drafts and ' + - 'publicly unresolved ', () => { - MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle')); - MockInteractions.tap(element.shadowRoot.querySelector( - '#unresolvedToggle')); - flushAsynchronousOperations(); - assert.equal(Polymer.dom(element.root) - .querySelectorAll('gr-comment-thread').length, 2); - }); + test('toggle drafts and unresolved only shows threads with drafts and ' + + 'publicly unresolved ', () => { + MockInteractions.tap(element.shadowRoot.querySelector('#draftToggle')); + MockInteractions.tap(element.shadowRoot.querySelector( + '#unresolvedToggle')); + flushAsynchronousOperations(); + assert.equal(dom(element.root) + .querySelectorAll('gr-comment-thread').length, 2); + }); - test('modification events are consumed and displatched', () => { - sandbox.spy(element, '_handleCommentsChanged'); - const dispatchSpy = sandbox.stub(); - element.addEventListener('thread-list-modified', dispatchSpy); - threadElements[0].fire('thread-changed', { - rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'}); - assert.isTrue(element._handleCommentsChanged.called); - assert.isTrue(dispatchSpy.called); - assert.equal(dispatchSpy.lastCall.args[0].detail.rootId, - 'ecf0b9fa_fe1a5f62'); - assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG'); - }); + test('modification events are consumed and displatched', () => { + sandbox.spy(element, '_handleCommentsChanged'); + const dispatchSpy = sandbox.stub(); + element.addEventListener('thread-list-modified', dispatchSpy); + threadElements[0].fire('thread-changed', { + rootId: 'ecf0b9fa_fe1a5f62', path: '/COMMIT_MSG'}); + assert.isTrue(element._handleCommentsChanged.called); + assert.isTrue(dispatchSpy.called); + assert.equal(dispatchSpy.lastCall.args[0].detail.rootId, + 'ecf0b9fa_fe1a5f62'); + assert.equal(dispatchSpy.lastCall.args[0].detail.path, '/COMMIT_MSG'); + }); - suite('findings tab', () => { - setup(done => { - element.hideToggleButtons = true; - flush(() => { - done(); - }); + suite('findings tab', () => { + setup(done => { + element.hideToggleButtons = true; + flush(() => { + done(); }); - test('toggle buttons are hidden', () => { - assert.equal(element.shadowRoot.querySelector('.header').style.display, - 'none'); - }); + }); + test('toggle buttons are hidden', () => { + assert.equal(element.shadowRoot.querySelector('.header').style.display, + 'none'); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js index 60cbd42..1ab3926 100644 --- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js +++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog.js
@@ -14,133 +14,144 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit'; - const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-shell-command/gr-shell-command.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-upload-help-dialog_html.js'; - // Command names correspond to download plugin definitions. - const PREFERRED_FETCH_COMMAND_ORDER = [ - 'checkout', - 'cherry pick', - 'pull', - ]; +const COMMIT_COMMAND = 'git add . && git commit --amend --no-edit'; +const PUSH_COMMAND_PREFIX = 'git push origin HEAD:refs/for/'; +// Command names correspond to download plugin definitions. +const PREFERRED_FETCH_COMMAND_ORDER = [ + 'checkout', + 'cherry pick', + 'pull', +]; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrUploadHelpDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-upload-help-dialog'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the user presses the close button. + * + * @event close */ - class GrUploadHelpDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-upload-help-dialog'; } - /** - * Fired when the user presses the close button. - * - * @event close - */ - static get properties() { - return { - revision: Object, - targetBranch: String, - _commitCommand: { - type: String, - value: COMMIT_COMMAND, - readOnly: true, - }, - _fetchCommand: { - type: String, - computed: '_computeFetchCommand(revision, ' + - '_preferredDownloadCommand, _preferredDownloadScheme)', - }, - _preferredDownloadCommand: String, - _preferredDownloadScheme: String, - _pushCommand: { - type: String, - computed: '_computePushCommand(targetBranch)', - }, - }; - } + static get properties() { + return { + revision: Object, + targetBranch: String, + _commitCommand: { + type: String, + value: COMMIT_COMMAND, + readOnly: true, + }, + _fetchCommand: { + type: String, + computed: '_computeFetchCommand(revision, ' + + '_preferredDownloadCommand, _preferredDownloadScheme)', + }, + _preferredDownloadCommand: String, + _preferredDownloadScheme: String, + _pushCommand: { + type: String, + computed: '_computePushCommand(targetBranch)', + }, + }; + } - /** @override */ - attached() { - super.attached(); - this.$.restAPI.getLoggedIn() - .then(loggedIn => { - if (loggedIn) { - return this.$.restAPI.getPreferences(); - } - }) - .then(prefs => { - if (prefs) { - this._preferredDownloadCommand = prefs.download_command; - this._preferredDownloadScheme = prefs.download_scheme; - } - }); - } + /** @override */ + attached() { + super.attached(); + this.$.restAPI.getLoggedIn() + .then(loggedIn => { + if (loggedIn) { + return this.$.restAPI.getPreferences(); + } + }) + .then(prefs => { + if (prefs) { + this._preferredDownloadCommand = prefs.download_command; + this._preferredDownloadScheme = prefs.download_scheme; + } + }); + } - _handleCloseTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('close', null, {bubbles: false}); - } + _handleCloseTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('close', null, {bubbles: false}); + } - _computeFetchCommand(revision, preferredDownloadCommand, - preferredDownloadScheme) { - // Polymer 2: check for undefined - if ([ - revision, - preferredDownloadCommand, - preferredDownloadScheme, - ].some(arg => arg === undefined)) { - return undefined; - } - - if (!revision) { return; } - if (!revision || !revision.fetch) { return; } - - let scheme = preferredDownloadScheme; - if (!scheme) { - const keys = Object.keys(revision.fetch).sort(); - if (keys.length === 0) { - return; - } - scheme = keys[0]; - } - - if (!revision.fetch[scheme] || !revision.fetch[scheme].commands) { - return; - } - - const cmds = {}; - Object.entries(revision.fetch[scheme].commands).forEach(([key, cmd]) => { - cmds[key.toLowerCase()] = cmd; - }); - - if (preferredDownloadCommand && - cmds[preferredDownloadCommand.toLowerCase()]) { - return cmds[preferredDownloadCommand.toLowerCase()]; - } - - // If no supported command preference is given, look for known commands - // from the downloads plugin in order of preference. - for (let i = 0; i < PREFERRED_FETCH_COMMAND_ORDER.length; i++) { - if (cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]) { - return cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]; - } - } - + _computeFetchCommand(revision, preferredDownloadCommand, + preferredDownloadScheme) { + // Polymer 2: check for undefined + if ([ + revision, + preferredDownloadCommand, + preferredDownloadScheme, + ].some(arg => arg === undefined)) { return undefined; } - _computePushCommand(targetBranch) { - return PUSH_COMMAND_PREFIX + targetBranch; + if (!revision) { return; } + if (!revision || !revision.fetch) { return; } + + let scheme = preferredDownloadScheme; + if (!scheme) { + const keys = Object.keys(revision.fetch).sort(); + if (keys.length === 0) { + return; + } + scheme = keys[0]; } + + if (!revision.fetch[scheme] || !revision.fetch[scheme].commands) { + return; + } + + const cmds = {}; + Object.entries(revision.fetch[scheme].commands).forEach(([key, cmd]) => { + cmds[key.toLowerCase()] = cmd; + }); + + if (preferredDownloadCommand && + cmds[preferredDownloadCommand.toLowerCase()]) { + return cmds[preferredDownloadCommand.toLowerCase()]; + } + + // If no supported command preference is given, look for known commands + // from the downloads plugin in order of preference. + for (let i = 0; i < PREFERRED_FETCH_COMMAND_ORDER.length; i++) { + if (cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]) { + return cmds[PREFERRED_FETCH_COMMAND_ORDER[i]]; + } + } + + return undefined; } - customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog); -})(); + _computePushCommand(targetBranch) { + return PUSH_COMMAND_PREFIX + targetBranch; + } +} + +customElements.define(GrUploadHelpDialog.is, GrUploadHelpDialog);
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js index e3cee56..ccf22e6 100644 --- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js +++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-upload-help-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background-color: var(--dialog-background-color); @@ -40,10 +33,7 @@ margin-bottom: var(--spacing-m); } </style> - <gr-dialog - confirm-label="Done" - cancel-label="" - on-confirm="_handleCloseTap"> + <gr-dialog confirm-label="Done" cancel-label="" on-confirm="_handleCloseTap"> <div class="header" slot="header">How to update this change:</div> <div class="main" slot="main"> <ol> @@ -77,6 +67,4 @@ </div> </gr-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-upload-help-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html index 3af5449..2aa71c6 100644 --- a/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html +++ b/polygerrit-ui/app/elements/change/gr-upload-help-dialog/gr-upload-help-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-upload-help-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-upload-help-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-upload-help-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-upload-help-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,93 +40,95 @@ </template> </test-fixture> -<script> - suite('gr-upload-help-dialog tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-upload-help-dialog.js'; +suite('gr-upload-help-dialog tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); + setup(() => { + element = fixture('basic'); + }); - test('constructs push command from branch', () => { - element.targetBranch = 'foo'; - assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo'); + test('constructs push command from branch', () => { + element.targetBranch = 'foo'; + assert.equal(element._pushCommand, 'git push origin HEAD:refs/for/foo'); - element.targetBranch = 'master'; - assert.equal(element._pushCommand, - 'git push origin HEAD:refs/for/master'); - }); + element.targetBranch = 'master'; + assert.equal(element._pushCommand, + 'git push origin HEAD:refs/for/master'); + }); - suite('fetch command', () => { - const testRev = { - fetch: { - http: { - commands: { - Checkout: 'http checkout', - Pull: 'http pull', - }, - }, - ssh: { - commands: { - Pull: 'ssh pull', - }, + suite('fetch command', () => { + const testRev = { + fetch: { + http: { + commands: { + Checkout: 'http checkout', + Pull: 'http pull', }, }, - }; + ssh: { + commands: { + Pull: 'ssh pull', + }, + }, + }, + }; - test('null cases', () => { - assert.isUndefined(element._computeFetchCommand()); - assert.isUndefined(element._computeFetchCommand({})); - assert.isUndefined(element._computeFetchCommand({fetch: null})); - assert.isUndefined(element._computeFetchCommand({fetch: {}})); - }); + test('null cases', () => { + assert.isUndefined(element._computeFetchCommand()); + assert.isUndefined(element._computeFetchCommand({})); + assert.isUndefined(element._computeFetchCommand({fetch: null})); + assert.isUndefined(element._computeFetchCommand({fetch: {}})); + }); - test('not all defined', () => { - assert.isUndefined( - element._computeFetchCommand(testRev, undefined, '')); - assert.isUndefined( - element._computeFetchCommand(testRev, '', undefined)); - assert.isUndefined( - element._computeFetchCommand(undefined, '', '')); - }); + test('not all defined', () => { + assert.isUndefined( + element._computeFetchCommand(testRev, undefined, '')); + assert.isUndefined( + element._computeFetchCommand(testRev, '', undefined)); + assert.isUndefined( + element._computeFetchCommand(undefined, '', '')); + }); - test('insufficiently defined scheme', () => { - assert.isUndefined( - element._computeFetchCommand(testRev, '', 'badscheme')); + test('insufficiently defined scheme', () => { + assert.isUndefined( + element._computeFetchCommand(testRev, '', 'badscheme')); - const rev = Object.assign({}, testRev); - rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}}); - assert.isUndefined( - element._computeFetchCommand(rev, '', 'nocmds')); + const rev = Object.assign({}, testRev); + rev.fetch = Object.assign({}, testRev.fetch, {nocmds: {commands: {}}}); + assert.isUndefined( + element._computeFetchCommand(rev, '', 'nocmds')); - rev.fetch.nocmds.commands.unsupported = 'unsupported'; - assert.isUndefined( - element._computeFetchCommand(rev, '', 'nocmds')); - }); + rev.fetch.nocmds.commands.unsupported = 'unsupported'; + assert.isUndefined( + element._computeFetchCommand(rev, '', 'nocmds')); + }); - test('default scheme and command', () => { - const cmd = element._computeFetchCommand(testRev, '', ''); - assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull'); - }); + test('default scheme and command', () => { + const cmd = element._computeFetchCommand(testRev, '', ''); + assert.isTrue(cmd === 'http checkout' || cmd === 'ssh pull'); + }); - test('default command', () => { - assert.strictEqual( - element._computeFetchCommand(testRev, '', 'http'), - 'http checkout'); - assert.strictEqual( - element._computeFetchCommand(testRev, '', 'ssh'), - 'ssh pull'); - }); + test('default command', () => { + assert.strictEqual( + element._computeFetchCommand(testRev, '', 'http'), + 'http checkout'); + assert.strictEqual( + element._computeFetchCommand(testRev, '', 'ssh'), + 'ssh pull'); + }); - test('user preferred scheme and command', () => { - assert.strictEqual( - element._computeFetchCommand(testRev, 'PULL', 'http'), - 'http pull'); - assert.strictEqual( - element._computeFetchCommand(testRev, 'badcmd', 'http'), - 'http checkout'); - }); + test('user preferred scheme and command', () => { + assert.strictEqual( + element._computeFetchCommand(testRev, 'PULL', 'http'), + 'http pull'); + assert.strictEqual( + element._computeFetchCommand(testRev, 'badcmd', 'http'), + 'http checkout'); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js index 66c00f9..6d9f9d7 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,106 +14,118 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js'; - const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g; +import '../../../scripts/bundled-polymer.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-avatar/gr-avatar.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-dropdown_html.js'; - /** - * @appliesMixin Gerrit.DisplayNameMixin - * @extends Polymer.Element - */ - class GrAccountDropdown extends Polymer.mixinBehaviors( [ - Gerrit.DisplayNameBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-dropdown'; } +const INTERPOLATE_URL_PATTERN = /\$\{([\w]+)\}/g; - static get properties() { - return { - account: Object, - config: Object, - links: { - type: Array, - computed: '_getLinks(_switchAccountUrl, _path)', - }, - topContent: { - type: Array, - computed: '_getTopContent(account)', - }, - _path: { - type: String, - value: '/', - }, - _hasAvatars: Boolean, - _switchAccountUrl: String, - }; - } +/** + * @appliesMixin Gerrit.DisplayNameMixin + * @extends Polymer.Element + */ +class GrAccountDropdown extends mixinBehaviors( [ + Gerrit.DisplayNameBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this._handleLocationChange(); - this.listen(window, 'location-change', '_handleLocationChange'); - this.$.restAPI.getConfig().then(cfg => { - this.config = cfg; + static get is() { return 'gr-account-dropdown'; } - if (cfg && cfg.auth && cfg.auth.switch_account_url) { - this._switchAccountUrl = cfg.auth.switch_account_url; - } else { - this._switchAccountUrl = ''; - } - this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); - }); - } - - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'location-change', '_handleLocationChange'); - } - - _getLinks(switchAccountUrl, path) { - // Polymer 2: check for undefined - if ([switchAccountUrl, path].some(arg => arg === undefined)) { - return undefined; - } - - const links = [{name: 'Settings', url: '/settings/'}]; - if (switchAccountUrl) { - const replacements = {path}; - const url = this._interpolateUrl(switchAccountUrl, replacements); - links.push({name: 'Switch account', url, external: true}); - } - links.push({name: 'Sign out', url: '/logout'}); - return links; - } - - _getTopContent(account) { - return [ - {text: this._accountName(account), bold: true}, - {text: account.email ? account.email : ''}, - ]; - } - - _handleLocationChange() { - this._path = - window.location.pathname + - window.location.search + - window.location.hash; - } - - _interpolateUrl(url, replacements) { - return url.replace( - INTERPOLATE_URL_PATTERN, - (match, p1) => replacements[p1] || ''); - } - - _accountName(account) { - return this.getUserName(this.config, account, true); - } + static get properties() { + return { + account: Object, + config: Object, + links: { + type: Array, + computed: '_getLinks(_switchAccountUrl, _path)', + }, + topContent: { + type: Array, + computed: '_getTopContent(account)', + }, + _path: { + type: String, + value: '/', + }, + _hasAvatars: Boolean, + _switchAccountUrl: String, + }; } - customElements.define(GrAccountDropdown.is, GrAccountDropdown); -})(); + /** @override */ + attached() { + super.attached(); + this._handleLocationChange(); + this.listen(window, 'location-change', '_handleLocationChange'); + this.$.restAPI.getConfig().then(cfg => { + this.config = cfg; + + if (cfg && cfg.auth && cfg.auth.switch_account_url) { + this._switchAccountUrl = cfg.auth.switch_account_url; + } else { + this._switchAccountUrl = ''; + } + this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + }); + } + + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'location-change', '_handleLocationChange'); + } + + _getLinks(switchAccountUrl, path) { + // Polymer 2: check for undefined + if ([switchAccountUrl, path].some(arg => arg === undefined)) { + return undefined; + } + + const links = [{name: 'Settings', url: '/settings/'}]; + if (switchAccountUrl) { + const replacements = {path}; + const url = this._interpolateUrl(switchAccountUrl, replacements); + links.push({name: 'Switch account', url, external: true}); + } + links.push({name: 'Sign out', url: '/logout'}); + return links; + } + + _getTopContent(account) { + return [ + {text: this._accountName(account), bold: true}, + {text: account.email ? account.email : ''}, + ]; + } + + _handleLocationChange() { + this._path = + window.location.pathname + + window.location.search + + window.location.hash; + } + + _interpolateUrl(url, replacements) { + return url.replace( + INTERPOLATE_URL_PATTERN, + (match, p1) => replacements[p1] || ''); + } + + _accountName(account) { + return this.getUserName(this.config, account, true); + } +} + +customElements.define(GrAccountDropdown.is, GrAccountDropdown);
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js index 5152ef9..e22db65 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-avatar/gr-avatar.html"> - -<dom-module id="gr-account-dropdown"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> gr-dropdown { padding: 0 var(--spacing-m); @@ -41,16 +33,9 @@ vertical-align: middle; } </style> - <gr-dropdown - link - items=[[links]] - top-content=[[topContent]] - horizontal-align="right"> - <span hidden$="[[_hasAvatars]]" hidden>[[_accountName(account)]]</span> - <gr-avatar account="[[account]]" hidden$="[[!_hasAvatars]]" hidden - image-size="56" aria-label="Account avatar"></gr-avatar> + <gr-dropdown link="" items="[[links]]" top-content="[[topContent]]" horizontal-align="right"> + <span hidden\$="[[_hasAvatars]]" hidden="">[[_accountName(account)]]</span> + <gr-avatar account="[[account]]" hidden\$="[[!_hasAvatars]]" hidden="" image-size="56" aria-label="Account avatar"></gr-avatar> </gr-dropdown> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-account-dropdown.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html index 0a11df1..fa0c7a7 100644 --- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html +++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-dropdown</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-account-dropdown.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-account-dropdown.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-dropdown.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,93 +40,95 @@ </template> </test-fixture> -<script> - suite('gr-account-dropdown tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-dropdown.js'; +suite('gr-account-dropdown tests', () => { + let element; - setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - }); - element = fixture('basic'); + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + }); + + test('account information', () => { + element.account = {name: 'John Doe', email: 'john@doe.com'}; + assert.deepEqual(element.topContent, + [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]); + }); + + test('test for account without a name', () => { + element.account = {id: '0001'}; + assert.deepEqual(element.topContent, + [{text: 'Anonymous', bold: true}, {text: ''}]); + }); + + test('test for account without a name but using config', () => { + element.config = { + user: { + anonymous_coward_name: 'WikiGerrit', + }, + }; + element.account = {id: '0001'}; + assert.deepEqual(element.topContent, + [{text: 'WikiGerrit', bold: true}, {text: ''}]); + }); + + test('test for account name as an email', () => { + element.config = { + user: { + anonymous_coward_name: 'WikiGerrit', + }, + }; + element.account = {email: 'john@doe.com'}; + assert.deepEqual(element.topContent, + [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]); + }); + + test('switch account', () => { + // Missing params. + assert.isUndefined(element._getLinks()); + assert.isUndefined(element._getLinks(null)); + + // No switch account link. + assert.equal(element._getLinks(null, '').length, 2); + + // Unparameterized switch account link. + let links = element._getLinks('/switch-account', ''); + assert.equal(links.length, 3); + assert.deepEqual(links[1], { + name: 'Switch account', + url: '/switch-account', + external: true, }); - test('account information', () => { - element.account = {name: 'John Doe', email: 'john@doe.com'}; - assert.deepEqual(element.topContent, - [{text: 'John Doe', bold: true}, {text: 'john@doe.com'}]); - }); - - test('test for account without a name', () => { - element.account = {id: '0001'}; - assert.deepEqual(element.topContent, - [{text: 'Anonymous', bold: true}, {text: ''}]); - }); - - test('test for account without a name but using config', () => { - element.config = { - user: { - anonymous_coward_name: 'WikiGerrit', - }, - }; - element.account = {id: '0001'}; - assert.deepEqual(element.topContent, - [{text: 'WikiGerrit', bold: true}, {text: ''}]); - }); - - test('test for account name as an email', () => { - element.config = { - user: { - anonymous_coward_name: 'WikiGerrit', - }, - }; - element.account = {email: 'john@doe.com'}; - assert.deepEqual(element.topContent, - [{text: 'john@doe.com', bold: true}, {text: 'john@doe.com'}]); - }); - - test('switch account', () => { - // Missing params. - assert.isUndefined(element._getLinks()); - assert.isUndefined(element._getLinks(null)); - - // No switch account link. - assert.equal(element._getLinks(null, '').length, 2); - - // Unparameterized switch account link. - let links = element._getLinks('/switch-account', ''); - assert.equal(links.length, 3); - assert.deepEqual(links[1], { - name: 'Switch account', - url: '/switch-account', - external: true, - }); - - // Parameterized switch account link. - links = element._getLinks('/switch-account${path}', '/c/123'); - assert.equal(links.length, 3); - assert.deepEqual(links[1], { - name: 'Switch account', - url: '/switch-account/c/123', - external: true, - }); - }); - - test('_interpolateUrl', () => { - const replacements = { - foo: 'bar', - test: 'TEST', - }; - const interpolate = function(url) { - return element._interpolateUrl(url, replacements); - }; - - assert.equal(interpolate('test'), 'test'); - assert.equal(interpolate('${test}'), 'TEST'); - assert.equal( - interpolate('${}, ${test}, ${TEST}, ${foo}'), - '${}, TEST, , bar'); + // Parameterized switch account link. + links = element._getLinks('/switch-account${path}', '/c/123'); + assert.equal(links.length, 3); + assert.deepEqual(links[1], { + name: 'Switch account', + url: '/switch-account/c/123', + external: true, }); }); + + test('_interpolateUrl', () => { + const replacements = { + foo: 'bar', + test: 'TEST', + }; + const interpolate = function(url) { + return element._interpolateUrl(url, replacements); + }; + + assert.equal(interpolate('test'), 'test'); + assert.equal(interpolate('${test}'), 'TEST'); + assert.equal( + interpolate('${}, ${test}, ${TEST}, ${foo}'), + '${}, TEST, , bar'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js index 63339c9..6814d89 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog.js
@@ -14,44 +14,51 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrErrorDialog extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-error-dialog'; } - /** - * Fired when the dismiss button is pressed. - * - * @event dismiss - */ +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-error-dialog_html.js'; - static get properties() { - return { - text: String, - /** - * loginUrl to open on "sign in" button click - */ - loginUrl: { - type: String, - value: '/login', - }, - /** - * Show/hide "Sign In" button in dialog - */ - showSignInButton: { - type: Boolean, - value: false, - }, - }; - } +/** @extends Polymer.Element */ +class GrErrorDialog extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _handleConfirm() { - this.dispatchEvent(new CustomEvent('dismiss')); - } + static get is() { return 'gr-error-dialog'; } + /** + * Fired when the dismiss button is pressed. + * + * @event dismiss + */ + + static get properties() { + return { + text: String, + /** + * loginUrl to open on "sign in" button click + */ + loginUrl: { + type: String, + value: '/login', + }, + /** + * Show/hide "Sign In" button in dialog + */ + showSignInButton: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrErrorDialog.is, GrErrorDialog); -})(); + _handleConfirm() { + this.dispatchEvent(new CustomEvent('dismiss')); + } +} + +customElements.define(GrErrorDialog.is, GrErrorDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js index ffd7f896..e18d1bd 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_html.js
@@ -1,26 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-error-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .main { max-height: 40em; @@ -38,23 +34,11 @@ text-decoration: none; } </style> - <gr-dialog - id="dialog" - cancel-label="" - on-confirm="_handleConfirm" - confirm-label="Dismiss" - confirm-on-enter> + <gr-dialog id="dialog" cancel-label="" on-confirm="_handleConfirm" confirm-label="Dismiss" confirm-on-enter=""> <div class="header" slot="header">An error occurred</div> <div class="main" slot="main">[[text]]</div> - <gr-button - id="signIn" - class$="signInLink" - hidden$="[[!showSignInButton]]" - link - slot="footer"> - <a href$="[[loginUrl]]" class="signInLink">Sign in</a> + <gr-button id="signIn" class\$="signInLink" hidden\$="[[!showSignInButton]]" link="" slot="footer"> + <a href\$="[[loginUrl]]" class="signInLink">Sign in</a> </gr-button> </gr-dialog> - </template> - <script src="gr-error-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html index c87f8bb..296c6f0 100644 --- a/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-dialog/gr-error-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-error-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-error-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-error-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-error-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,18 +40,20 @@ </template> </test-fixture> -<script> - suite('gr-error-dialog tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-error-dialog.js'; +suite('gr-error-dialog tests', () => { + let element; - setup(() => { - element = fixture('basic'); - }); - - test('dismiss tap fires event', done => { - element.addEventListener('dismiss', () => { done(); }); - MockInteractions.tap(element.$.dialog.$.confirm); - }); + setup(() => { + element = fixture('basic'); }); + + test('dismiss tap fires event', done => { + element.addEventListener('dismiss', () => { done(); }); + MockInteractions.tap(element.$.dialog.$.confirm); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js index b828774..e2284a9 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,384 +14,405 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +/* Import to get Gerrit interface */ +/* TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface */ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const HIDE_ALERT_TIMEOUT_MS = 5000; - const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000; - const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000; - const SIGN_IN_WIDTH_PX = 690; - const SIGN_IN_HEIGHT_PX = 500; - const TOO_MANY_FILES = 'too many files to find conflicts'; - const AUTHENTICATION_REQUIRED = 'Authentication required\n'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-error-dialog/gr-error-dialog.js'; +import '../gr-reporting/gr-reporting.js'; +import '../../shared/gr-alert/gr-alert.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-error-manager_html.js'; + +const HIDE_ALERT_TIMEOUT_MS = 5000; +const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000; +const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000; +const SIGN_IN_WIDTH_PX = 690; +const SIGN_IN_HEIGHT_PX = 500; +const TOO_MANY_FILES = 'too many files to find conflicts'; +const AUTHENTICATION_REQUIRED = 'Authentication required\n'; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrErrorManager extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-error-manager'; } + + static get properties() { + return { + /** + * The ID of the account that was logged in when the app was launched. If + * not set, then there was no account at launch. + */ + knownAccountId: Number, + + /** @type {?Object} */ + _alertElement: Object, + /** @type {?number} */ + _hideAlertHandle: Number, + _refreshingCredentials: { + type: Boolean, + value: false, + }, + + /** + * The time (in milliseconds) since the most recent credential check. + */ + _lastCredentialCheck: { + type: Number, + value() { return Date.now(); }, + }, + + loginUrl: { + type: String, + value: '/login', + }, + }; + } + + constructor() { + super(); + + /** @type {!Gerrit.Auth} */ + this._authService = Gerrit.Auth; + + /** @type {?Function} */ + this._authErrorHandlerDeregistrationHook; + } + + /** @override */ + attached() { + super.attached(); + this.listen(document, 'server-error', '_handleServerError'); + this.listen(document, 'network-error', '_handleNetworkError'); + this.listen(document, 'show-alert', '_handleShowAlert'); + this.listen(document, 'show-error', '_handleShowErrorDialog'); + this.listen(document, 'visibilitychange', '_handleVisibilityChange'); + this.listen(document, 'show-auth-required', '_handleAuthRequired'); + + this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error', + event => { + this._handleAuthError(event.message, event.action); + }); + } + + /** @override */ + detached() { + super.detached(); + this._clearHideAlertHandle(); + this.unlisten(document, 'server-error', '_handleServerError'); + this.unlisten(document, 'network-error', '_handleNetworkError'); + this.unlisten(document, 'show-auth-required', '_handleAuthRequired'); + this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); + this.unlisten(document, 'show-error', '_handleShowErrorDialog'); + + this._authErrorHandlerDeregistrationHook(); + } + + _shouldSuppressError(msg) { + return msg.includes(TOO_MANY_FILES); + } + + _handleAuthRequired() { + this._showAuthErrorAlert( + 'Log in is required to perform that action.', 'Log in.'); + } + + _handleAuthError(msg, action) { + this.$.noInteractionOverlay.open().then(() => { + this._showAuthErrorAlert(msg, action); + }); + } + + _handleServerError(e) { + const {request, response} = e.detail; + response.text().then(errorText => { + const url = request && (request.anonymizedUrl || request.url); + const {status, statusText} = response; + if (response.status === 403 + && !this._authService.isAuthed + && errorText === AUTHENTICATION_REQUIRED) { + // if not authed previously, this is trying to access auth required APIs + // show auth required alert + this._handleAuthRequired(); + } else if (response.status === 403 + && this._authService.isAuthed + && errorText === AUTHENTICATION_REQUIRED) { + // The app was logged at one point and is now getting auth errors. + // This indicates the auth token may no longer valid. + // Re-check on auth + this._authService.clearCache(); + this.$.restAPI.getLoggedIn(); + } else if (!this._shouldSuppressError(errorText)) { + const trace = + response.headers && response.headers.get('X-Gerrit-Trace'); + if (response.status === 404) { + this._showNotFoundMessageWithTip({ + status, + statusText, + errorText, + url, + trace, + }); + } else { + this._showErrorDialog(this._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + trace, + })); + } + } + console.log(`server error: ${errorText}`); + }); + } + + _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) { + this.$.restAPI.getLoggedIn().then(isLoggedIn => { + const tip = isLoggedIn ? + 'You might have not enough privileges.' : + 'You might have not enough privileges. Sign in and try again.'; + this._showErrorDialog(this._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + trace, + tip, + }), { + showSignInButton: !isLoggedIn, + }); + }); + return; + } + + _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) { + let err = ''; + if (tip) { + err += `${tip}\n\n`; + } + err += `Error ${status}`; + if (statusText) { err += ` (${statusText})`; } + if (errorText || url) { err += ': '; } + if (errorText) { err += errorText; } + if (url) { err += `\nEndpoint: ${url}`; } + if (trace) { err += `\nTrace Id: ${trace}`; } + return err; + } + + _handleShowAlert(e) { + this._showAlert(e.detail.message, e.detail.action, e.detail.callback, + e.detail.dismissOnNavigation); + } + + _handleNetworkError(e) { + this._showAlert('Server unavailable'); + console.error(e.detail.error.message); + } /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * @param {string} text + * @param {?string=} opt_actionText + * @param {?Function=} opt_actionCallback + * @param {?boolean=} opt_dismissOnNavigation */ - class GrErrorManager extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-error-manager'; } - - static get properties() { - return { - /** - * The ID of the account that was logged in when the app was launched. If - * not set, then there was no account at launch. - */ - knownAccountId: Number, - - /** @type {?Object} */ - _alertElement: Object, - /** @type {?number} */ - _hideAlertHandle: Number, - _refreshingCredentials: { - type: Boolean, - value: false, - }, - - /** - * The time (in milliseconds) since the most recent credential check. - */ - _lastCredentialCheck: { - type: Number, - value() { return Date.now(); }, - }, - - loginUrl: { - type: String, - value: '/login', - }, - }; - } - - constructor() { - super(); - - /** @type {!Gerrit.Auth} */ - this._authService = Gerrit.Auth; - - /** @type {?Function} */ - this._authErrorHandlerDeregistrationHook; - } - - /** @override */ - attached() { - super.attached(); - this.listen(document, 'server-error', '_handleServerError'); - this.listen(document, 'network-error', '_handleNetworkError'); - this.listen(document, 'show-alert', '_handleShowAlert'); - this.listen(document, 'show-error', '_handleShowErrorDialog'); - this.listen(document, 'visibilitychange', '_handleVisibilityChange'); - this.listen(document, 'show-auth-required', '_handleAuthRequired'); - - this._authErrorHandlerDeregistrationHook = Gerrit.on('auth-error', - event => { - this._handleAuthError(event.message, event.action); - }); - } - - /** @override */ - detached() { - super.detached(); - this._clearHideAlertHandle(); - this.unlisten(document, 'server-error', '_handleServerError'); - this.unlisten(document, 'network-error', '_handleNetworkError'); - this.unlisten(document, 'show-auth-required', '_handleAuthRequired'); - this.unlisten(document, 'visibilitychange', '_handleVisibilityChange'); - this.unlisten(document, 'show-error', '_handleShowErrorDialog'); - - this._authErrorHandlerDeregistrationHook(); - } - - _shouldSuppressError(msg) { - return msg.includes(TOO_MANY_FILES); - } - - _handleAuthRequired() { - this._showAuthErrorAlert( - 'Log in is required to perform that action.', 'Log in.'); - } - - _handleAuthError(msg, action) { - this.$.noInteractionOverlay.open().then(() => { - this._showAuthErrorAlert(msg, action); - }); - } - - _handleServerError(e) { - const {request, response} = e.detail; - response.text().then(errorText => { - const url = request && (request.anonymizedUrl || request.url); - const {status, statusText} = response; - if (response.status === 403 - && !this._authService.isAuthed - && errorText === AUTHENTICATION_REQUIRED) { - // if not authed previously, this is trying to access auth required APIs - // show auth required alert - this._handleAuthRequired(); - } else if (response.status === 403 - && this._authService.isAuthed - && errorText === AUTHENTICATION_REQUIRED) { - // The app was logged at one point and is now getting auth errors. - // This indicates the auth token may no longer valid. - // Re-check on auth - this._authService.clearCache(); - this.$.restAPI.getLoggedIn(); - } else if (!this._shouldSuppressError(errorText)) { - const trace = - response.headers && response.headers.get('X-Gerrit-Trace'); - if (response.status === 404) { - this._showNotFoundMessageWithTip({ - status, - statusText, - errorText, - url, - trace, - }); - } else { - this._showErrorDialog(this._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - trace, - })); - } - } - console.log(`server error: ${errorText}`); - }); - } - - _showNotFoundMessageWithTip({status, statusText, errorText, url, trace}) { - this.$.restAPI.getLoggedIn().then(isLoggedIn => { - const tip = isLoggedIn ? - 'You might have not enough privileges.' : - 'You might have not enough privileges. Sign in and try again.'; - this._showErrorDialog(this._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - trace, - tip, - }), { - showSignInButton: !isLoggedIn, - }); - }); - return; - } - - _constructServerErrorMsg({errorText, status, statusText, url, trace, tip}) { - let err = ''; - if (tip) { - err += `${tip}\n\n`; - } - err += `Error ${status}`; - if (statusText) { err += ` (${statusText})`; } - if (errorText || url) { err += ': '; } - if (errorText) { err += errorText; } - if (url) { err += `\nEndpoint: ${url}`; } - if (trace) { err += `\nTrace Id: ${trace}`; } - return err; - } - - _handleShowAlert(e) { - this._showAlert(e.detail.message, e.detail.action, e.detail.callback, - e.detail.dismissOnNavigation); - } - - _handleNetworkError(e) { - this._showAlert('Server unavailable'); - console.error(e.detail.error.message); - } - - /** - * @param {string} text - * @param {?string=} opt_actionText - * @param {?Function=} opt_actionCallback - * @param {?boolean=} opt_dismissOnNavigation - */ - _showAlert(text, opt_actionText, opt_actionCallback, - opt_dismissOnNavigation) { - if (this._alertElement) { - // do not override auth alerts - if (this._alertElement.type === 'AUTH') return; - this._hideAlert(); - } - - this._clearHideAlertHandle(); - if (opt_dismissOnNavigation) { - // Persist alert until navigation. - this.listen(document, 'location-change', '_hideAlert'); - } else { - this._hideAlertHandle = - this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS); - } - const el = this._createToastAlert(); - el.show(text, opt_actionText, opt_actionCallback); - this._alertElement = el; - } - - _hideAlert() { - if (!this._alertElement) { return; } - - this._alertElement.hide(); - this._alertElement = null; - - // Remove listener for page navigation, if it exists. - this.unlisten(document, 'location-change', '_hideAlert'); - } - - _clearHideAlertHandle() { - if (this._hideAlertHandle != null) { - this.cancelAsync(this._hideAlertHandle); - this._hideAlertHandle = null; - } - } - - _showAuthErrorAlert(errorText, actionText) { - // hide any existing alert like `reload` - // as auth error should have the highest priority - if (this._alertElement) { - this._alertElement.hide(); - } - - this._alertElement = this._createToastAlert(); - this._alertElement.type = 'AUTH'; - this._alertElement.show(errorText, actionText, - this._createLoginPopup.bind(this)); - - this._refreshingCredentials = true; - this._requestCheckLoggedIn(); - if (!document.hidden) { - this._handleVisibilityChange(); - } - } - - _createToastAlert() { - const el = document.createElement('gr-alert'); - el.toast = true; - return el; - } - - _handleVisibilityChange() { - // Ignore when the page is transitioning to hidden (or hidden is - // undefined). - if (document.hidden !== false) { return; } - - // If not currently refreshing credentials and the credentials are old, - // request them to confirm their validity or (display an auth toast if it - // fails). - const timeSinceLastCheck = Date.now() - this._lastCredentialCheck; - if (!this._refreshingCredentials && - this.knownAccountId !== undefined && - timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) { - this._lastCredentialCheck = Date.now(); - - // check auth status in case: - // - user signed out - // - user switched account - this._checkSignedIn(); - } - } - - _requestCheckLoggedIn() { - this.debounce( - 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); - } - - _checkSignedIn() { - this._lastCredentialCheck = Date.now(); - - // force to refetch account info - this.$.restAPI.invalidateAccountsCache(); - this._authService.clearCache(); - - this.$.restAPI.getLoggedIn().then(isLoggedIn => { - // do nothing if its refreshing - if (!this._refreshingCredentials) return; - - if (!isLoggedIn) { - // check later - // 1. guest mode - // 2. or signed out - // in case #2, auth-error is taken care of separately - this._requestCheckLoggedIn(); - } else { - // check account - this.$.restAPI.getAccount().then(account => { - if (this._refreshingCredentials) { - // If the credentials were refreshed but the account is different - // then reload the page completely. - if (account._account_id !== this.knownAccountId) { - this._reloadPage(); - return; - } - - this._handleCredentialRefreshed(); - } - }); - } - }); - } - - _reloadPage() { - window.location.reload(); - } - - _createLoginPopup() { - const left = window.screenLeft + - (window.outerWidth - SIGN_IN_WIDTH_PX) / 2; - const top = window.screenTop + - (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2; - const options = [ - 'width=' + SIGN_IN_WIDTH_PX, - 'height=' + SIGN_IN_HEIGHT_PX, - 'left=' + left, - 'top=' + top, - ]; - window.open(this.getBaseUrl() + - '/login/%3FcloseAfterLogin', '_blank', options.join(',')); - this.listen(window, 'focus', '_handleWindowFocus'); - } - - _handleCredentialRefreshed() { - this.unlisten(window, 'focus', '_handleWindowFocus'); - this._refreshingCredentials = false; + _showAlert(text, opt_actionText, opt_actionCallback, + opt_dismissOnNavigation) { + if (this._alertElement) { + // do not override auth alerts + if (this._alertElement.type === 'AUTH') return; this._hideAlert(); - this._showAlert('Credentials refreshed.'); - this.$.noInteractionOverlay.close(); - - // Clear the cache for auth - this._authService.clearCache(); } - _handleWindowFocus() { - this.flushDebouncer('checkLoggedIn'); + this._clearHideAlertHandle(); + if (opt_dismissOnNavigation) { + // Persist alert until navigation. + this.listen(document, 'location-change', '_hideAlert'); + } else { + this._hideAlertHandle = + this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS); } + const el = this._createToastAlert(); + el.show(text, opt_actionText, opt_actionCallback); + this._alertElement = el; + } - _handleShowErrorDialog(e) { - this._showErrorDialog(e.detail.message); - } + _hideAlert() { + if (!this._alertElement) { return; } - _handleDismissErrorDialog() { - this.$.errorOverlay.close(); - } + this._alertElement.hide(); + this._alertElement = null; - _showErrorDialog(message, opt_options) { - this.$.reporting.reportErrorDialog(message); - this.$.errorDialog.text = message; - this.$.errorDialog.showSignInButton = - opt_options && opt_options.showSignInButton; - this.$.errorOverlay.open(); + // Remove listener for page navigation, if it exists. + this.unlisten(document, 'location-change', '_hideAlert'); + } + + _clearHideAlertHandle() { + if (this._hideAlertHandle != null) { + this.cancelAsync(this._hideAlertHandle); + this._hideAlertHandle = null; } } - customElements.define(GrErrorManager.is, GrErrorManager); -})(); + _showAuthErrorAlert(errorText, actionText) { + // hide any existing alert like `reload` + // as auth error should have the highest priority + if (this._alertElement) { + this._alertElement.hide(); + } + + this._alertElement = this._createToastAlert(); + this._alertElement.type = 'AUTH'; + this._alertElement.show(errorText, actionText, + this._createLoginPopup.bind(this)); + + this._refreshingCredentials = true; + this._requestCheckLoggedIn(); + if (!document.hidden) { + this._handleVisibilityChange(); + } + } + + _createToastAlert() { + const el = document.createElement('gr-alert'); + el.toast = true; + return el; + } + + _handleVisibilityChange() { + // Ignore when the page is transitioning to hidden (or hidden is + // undefined). + if (document.hidden !== false) { return; } + + // If not currently refreshing credentials and the credentials are old, + // request them to confirm their validity or (display an auth toast if it + // fails). + const timeSinceLastCheck = Date.now() - this._lastCredentialCheck; + if (!this._refreshingCredentials && + this.knownAccountId !== undefined && + timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) { + this._lastCredentialCheck = Date.now(); + + // check auth status in case: + // - user signed out + // - user switched account + this._checkSignedIn(); + } + } + + _requestCheckLoggedIn() { + this.debounce( + 'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS); + } + + _checkSignedIn() { + this._lastCredentialCheck = Date.now(); + + // force to refetch account info + this.$.restAPI.invalidateAccountsCache(); + this._authService.clearCache(); + + this.$.restAPI.getLoggedIn().then(isLoggedIn => { + // do nothing if its refreshing + if (!this._refreshingCredentials) return; + + if (!isLoggedIn) { + // check later + // 1. guest mode + // 2. or signed out + // in case #2, auth-error is taken care of separately + this._requestCheckLoggedIn(); + } else { + // check account + this.$.restAPI.getAccount().then(account => { + if (this._refreshingCredentials) { + // If the credentials were refreshed but the account is different + // then reload the page completely. + if (account._account_id !== this.knownAccountId) { + this._reloadPage(); + return; + } + + this._handleCredentialRefreshed(); + } + }); + } + }); + } + + _reloadPage() { + window.location.reload(); + } + + _createLoginPopup() { + const left = window.screenLeft + + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2; + const top = window.screenTop + + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2; + const options = [ + 'width=' + SIGN_IN_WIDTH_PX, + 'height=' + SIGN_IN_HEIGHT_PX, + 'left=' + left, + 'top=' + top, + ]; + window.open(this.getBaseUrl() + + '/login/%3FcloseAfterLogin', '_blank', options.join(',')); + this.listen(window, 'focus', '_handleWindowFocus'); + } + + _handleCredentialRefreshed() { + this.unlisten(window, 'focus', '_handleWindowFocus'); + this._refreshingCredentials = false; + this._hideAlert(); + this._showAlert('Credentials refreshed.'); + this.$.noInteractionOverlay.close(); + + // Clear the cache for auth + this._authService.clearCache(); + } + + _handleWindowFocus() { + this.flushDebouncer('checkLoggedIn'); + } + + _handleShowErrorDialog(e) { + this._showErrorDialog(e.detail.message); + } + + _handleDismissErrorDialog() { + this.$.errorOverlay.close(); + } + + _showErrorDialog(message, opt_options) { + this.$.reporting.reportErrorDialog(message); + this.$.errorDialog.text = message; + this.$.errorDialog.showSignInButton = + opt_options && opt_options.showSignInButton; + this.$.errorOverlay.open(); + } +} + +customElements.define(GrErrorManager.is, GrErrorManager);
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js index 104d5b0..5661d1e 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_html.js
@@ -1,52 +1,27 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../core/gr-error-dialog/gr-error-dialog.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-alert/gr-alert.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<!-- Import to get Gerrit interface --> -<!-- TODO(taoalpha): decouple gr-gerrit from gr-js-api-interface --> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-error-manager"> - <template> - <gr-overlay with-backdrop id="errorOverlay"> - <gr-error-dialog - id="errorDialog" - on-dismiss="_handleDismissErrorDialog" - confirm-label="Dismiss" - confirm-on-enter - login-url="[[loginUrl]]" - ></gr-error-dialog> +export const htmlTemplate = html` + <gr-overlay with-backdrop="" id="errorOverlay"> + <gr-error-dialog id="errorDialog" on-dismiss="_handleDismissErrorDialog" confirm-label="Dismiss" confirm-on-enter="" login-url="[[loginUrl]]"></gr-error-dialog> </gr-overlay> - <gr-overlay - id="noInteractionOverlay" - with-backdrop - always-on-top - no-cancel-on-esc-key - no-cancel-on-outside-click> + <gr-overlay id="noInteractionOverlay" with-backdrop="" always-on-top="" no-cancel-on-esc-key="" no-cancel-on-outside-click=""> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-error-manager.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html index e984577..a4a9a7d 100644 --- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html +++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -19,14 +19,18 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-error-manager</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<link rel="import" href="../../../test/common-test-setup.html" /> -<link rel="import" href="gr-error-manager.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-error-manager.js"></script> -<script>void (0);</script> +<script type="module"> +import '../../../test/common-test-setup.js'; +import './gr-error-manager.js'; +void (0); +</script> <test-fixture id="basic"> <template> @@ -34,465 +38,467 @@ </template> </test-fixture> -<script> - suite('gr-error-manager tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/common-test-setup.js'; +import './gr-error-manager.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-error-manager tests', () => { + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('when authed', () => { setup(() => { - sandbox = sinon.sandbox.create(); + sandbox.stub(window, 'fetch') + .returns(Promise.resolve({ok: true, status: 204})); + element = fixture('basic'); + element._authService.clearCache(); }); - teardown(() => { - sandbox.restore(); + test('does not show auth error on 403 by default', done => { + const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); + const responseText = Promise.resolve('server says no.'); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + flush(() => { + assert.isFalse(showAuthErrorStub.calledOnce); + done(); + }); }); - suite('when authed', () => { - setup(() => { - sandbox.stub(window, 'fetch') - .returns(Promise.resolve({ok: true, status: 204})); - element = fixture('basic'); - element._authService.clearCache(); - }); - - test('does not show auth error on 403 by default', done => { - const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert'); - const responseText = Promise.resolve('server says no.'); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - flush(() => { - assert.isFalse(showAuthErrorStub.calledOnce); - done(); - }); - }); - - test('show auth required for 403 with auth error and not authed before', - done => { - const showAuthErrorStub = sandbox.stub( - element, '_showAuthErrorAlert' - ); - const responseText = Promise.resolve('Authentication required\n'); - sinon.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - flush(() => { - assert.isTrue(showAuthErrorStub.calledOnce); - done(); - }); - }); - - test('recheck auth for 403 with auth error if authed before', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const responseText = Promise.resolve('Authentication required\n'); - sinon.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - flush(() => { - assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce); - done(); - }); - }); - - test('show logged in error', () => { - sandbox.stub(element, '_showAuthErrorAlert'); - element.fire('show-auth-required'); - assert.isTrue(element._showAuthErrorAlert.calledWithExactly( - 'Log in is required to perform that action.', 'Log in.')); - }); - - test('show normal Error', done => { - const showErrorStub = sandbox.stub(element, '_showErrorDialog'); - const textSpy = sandbox.spy(() => Promise.resolve('ZOMG')); - element.fire('server-error', {response: {status: 500, text: textSpy}}); - - assert.isTrue(textSpy.called); - flush(() => { - assert.isTrue(showErrorStub.calledOnce); - assert.isTrue(showErrorStub.lastCall.calledWithExactly( - 'Error 500: ZOMG')); - done(); - }); - }); - - test('_constructServerErrorMsg', () => { - const errorText = 'change conflicts'; - const status = 409; - const statusText = 'Conflict'; - const url = '/my/test/url'; - - assert.equal(element._constructServerErrorMsg({status}), - 'Error 409'); - assert.equal(element._constructServerErrorMsg({status, url}), - 'Error 409: \nEndpoint: /my/test/url'); - assert.equal(element. - _constructServerErrorMsg({status, statusText, url}), - 'Error 409 (Conflict): \nEndpoint: /my/test/url'); - assert.equal(element._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - }), 'Error 409 (Conflict): change conflicts' + - '\nEndpoint: /my/test/url'); - assert.equal(element._constructServerErrorMsg({ - status, - statusText, - errorText, - url, - trace: 'xxxxx', - }), 'Error 409 (Conflict): change conflicts' + - '\nEndpoint: /my/test/url\nTrace Id: xxxxx'); - }); - - test('extract trace id from headers if exists', done => { - const textSpy = sandbox.spy( - () => Promise.resolve('500') - ); - const headers = new Headers(); - headers.set('X-Gerrit-Trace', 'xxxx'); - element.fire('server-error', { - response: { - headers, - status: 500, - text: textSpy, - }, - }); - flush(() => { - assert.equal( - element.$.errorDialog.text, - 'Error 500: 500\nTrace Id: xxxx' + test('show auth required for 403 with auth error and not authed before', + done => { + const showAuthErrorStub = sandbox.stub( + element, '_showAuthErrorAlert' ); - done(); - }); - }); - - test('suppress TOO_MANY_FILES error', done => { - const showAlertStub = sandbox.stub(element, '_showAlert'); - const textSpy = sandbox.spy( - () => Promise.resolve('too many files to find conflicts') - ); - element.fire('server-error', {response: {status: 500, text: textSpy}}); - - assert.isTrue(textSpy.called); - flush(() => { - assert.isFalse(showAlertStub.called); - done(); - }); - }); - - test('show network error', done => { - const consoleErrorStub = sandbox.stub(console, 'error'); - const showAlertStub = sandbox.stub(element, '_showAlert'); - element.fire('network-error', {error: new Error('ZOMG')}); - flush(() => { - assert.isTrue(showAlertStub.calledOnce); - assert.isTrue(showAlertStub.lastCall.calledWithExactly( - 'Server unavailable')); - assert.isTrue(consoleErrorStub.calledOnce); - assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG')); - done(); - }); - }); - - test('show auth refresh toast', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount', - () => Promise.resolve({})); - const toastSpy = sandbox.spy(element, '_createToastAlert'); - const windowOpen = sandbox.stub(window, 'open'); - const responseText = Promise.resolve('Authentication required\n'); - // fake failed auth - window.fetch.returns(Promise.resolve({status: 403})); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - assert.equal(window.fetch.callCount, 1); - flush(() => { - // here needs two flush as there are two chanined - // promises on server-error handler and flush only flushes one - assert.equal(window.fetch.callCount, 2); + const responseText = Promise.resolve('Authentication required\n'); + sinon.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); flush(() => { - // auth-error fired - assert.isTrue(toastSpy.called); + assert.isTrue(showAuthErrorStub.calledOnce); + done(); + }); + }); - // toast - let toast = toastSpy.lastCall.returnValue; + test('recheck auth for 403 with auth error if authed before', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const responseText = Promise.resolve('Authentication required\n'); + sinon.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + flush(() => { + assert.isTrue(element.$.restAPI.getLoggedIn.calledOnce); + done(); + }); + }); + + test('show logged in error', () => { + sandbox.stub(element, '_showAuthErrorAlert'); + element.fire('show-auth-required'); + assert.isTrue(element._showAuthErrorAlert.calledWithExactly( + 'Log in is required to perform that action.', 'Log in.')); + }); + + test('show normal Error', done => { + const showErrorStub = sandbox.stub(element, '_showErrorDialog'); + const textSpy = sandbox.spy(() => Promise.resolve('ZOMG')); + element.fire('server-error', {response: {status: 500, text: textSpy}}); + + assert.isTrue(textSpy.called); + flush(() => { + assert.isTrue(showErrorStub.calledOnce); + assert.isTrue(showErrorStub.lastCall.calledWithExactly( + 'Error 500: ZOMG')); + done(); + }); + }); + + test('_constructServerErrorMsg', () => { + const errorText = 'change conflicts'; + const status = 409; + const statusText = 'Conflict'; + const url = '/my/test/url'; + + assert.equal(element._constructServerErrorMsg({status}), + 'Error 409'); + assert.equal(element._constructServerErrorMsg({status, url}), + 'Error 409: \nEndpoint: /my/test/url'); + assert.equal(element. + _constructServerErrorMsg({status, statusText, url}), + 'Error 409 (Conflict): \nEndpoint: /my/test/url'); + assert.equal(element._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + }), 'Error 409 (Conflict): change conflicts' + + '\nEndpoint: /my/test/url'); + assert.equal(element._constructServerErrorMsg({ + status, + statusText, + errorText, + url, + trace: 'xxxxx', + }), 'Error 409 (Conflict): change conflicts' + + '\nEndpoint: /my/test/url\nTrace Id: xxxxx'); + }); + + test('extract trace id from headers if exists', done => { + const textSpy = sandbox.spy( + () => Promise.resolve('500') + ); + const headers = new Headers(); + headers.set('X-Gerrit-Trace', 'xxxx'); + element.fire('server-error', { + response: { + headers, + status: 500, + text: textSpy, + }, + }); + flush(() => { + assert.equal( + element.$.errorDialog.text, + 'Error 500: 500\nTrace Id: xxxx' + ); + done(); + }); + }); + + test('suppress TOO_MANY_FILES error', done => { + const showAlertStub = sandbox.stub(element, '_showAlert'); + const textSpy = sandbox.spy( + () => Promise.resolve('too many files to find conflicts') + ); + element.fire('server-error', {response: {status: 500, text: textSpy}}); + + assert.isTrue(textSpy.called); + flush(() => { + assert.isFalse(showAlertStub.called); + done(); + }); + }); + + test('show network error', done => { + const consoleErrorStub = sandbox.stub(console, 'error'); + const showAlertStub = sandbox.stub(element, '_showAlert'); + element.fire('network-error', {error: new Error('ZOMG')}); + flush(() => { + assert.isTrue(showAlertStub.calledOnce); + assert.isTrue(showAlertStub.lastCall.calledWithExactly( + 'Server unavailable')); + assert.isTrue(consoleErrorStub.calledOnce); + assert.isTrue(consoleErrorStub.lastCall.calledWithExactly('ZOMG')); + done(); + }); + }); + + test('show auth refresh toast', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const refreshStub = sandbox.stub(element.$.restAPI, 'getAccount', + () => Promise.resolve({})); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const windowOpen = sandbox.stub(window, 'open'); + const responseText = Promise.resolve('Authentication required\n'); + // fake failed auth + window.fetch.returns(Promise.resolve({status: 403})); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + assert.equal(window.fetch.callCount, 1); + flush(() => { + // here needs two flush as there are two chanined + // promises on server-error handler and flush only flushes one + assert.equal(window.fetch.callCount, 2); + flush(() => { + // auth-error fired + assert.isTrue(toastSpy.called); + + // toast + let toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + assert.include( + dom(toast.root).textContent, 'Refresh credentials'); + + // noInteractionOverlay + const noInteractionOverlay = element.$.noInteractionOverlay; + assert.isOk(noInteractionOverlay); + sinon.spy(noInteractionOverlay, 'close'); + assert.equal( + noInteractionOverlay.backdropElement.getAttribute('opened'), + ''); + assert.isFalse(windowOpen.called); + MockInteractions.tap(toast.shadowRoot + .querySelector('gr-button.action')); + assert.isTrue(windowOpen.called); + + // @see Issue 5822: noopener breaks closeAfterLogin + assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'), + -1); + + const hideToastSpy = sandbox.spy(toast, 'hide'); + + // now fake authed + window.fetch.returns(Promise.resolve({status: 204})); + element._handleWindowFocus(); + element.flushDebouncer('checkLoggedIn'); + flush(() => { + assert.isTrue(refreshStub.called); + assert.isTrue(hideToastSpy.called); + + // toast update + assert.notStrictEqual(toastSpy.lastCall.returnValue, toast); + toast = toastSpy.lastCall.returnValue; assert.isOk(toast); assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - assert.include( - Polymer.dom(toast.root).textContent, 'Refresh credentials'); + dom(toast.root).textContent, 'Credentials refreshed'); - // noInteractionOverlay - const noInteractionOverlay = element.$.noInteractionOverlay; - assert.isOk(noInteractionOverlay); - sinon.spy(noInteractionOverlay, 'close'); - assert.equal( - noInteractionOverlay.backdropElement.getAttribute('opened'), - ''); - assert.isFalse(windowOpen.called); - MockInteractions.tap(toast.shadowRoot - .querySelector('gr-button.action')); - assert.isTrue(windowOpen.called); - - // @see Issue 5822: noopener breaks closeAfterLogin - assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'), - -1); - - const hideToastSpy = sandbox.spy(toast, 'hide'); - - // now fake authed - window.fetch.returns(Promise.resolve({status: 204})); - element._handleWindowFocus(); - element.flushDebouncer('checkLoggedIn'); - flush(() => { - assert.isTrue(refreshStub.called); - assert.isTrue(hideToastSpy.called); - - // toast update - assert.notStrictEqual(toastSpy.lastCall.returnValue, toast); - toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials refreshed'); - - // close overlay - assert.isTrue(noInteractionOverlay.close.called); - done(); - }); - }); - }); - }); - - test('auth toast should dismiss existing toast', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const toastSpy = sandbox.spy(element, '_createToastAlert'); - const responseText = Promise.resolve('Authentication required\n'); - - // fake an alert - element.fire('show-alert', {message: 'test reload', action: 'reload'}); - const toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'test reload'); - - // fake auth - window.fetch.returns(Promise.resolve({status: 403})); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - assert.equal(window.fetch.callCount, 1); - flush(() => { - // here needs two flush as there are two chanined - // promises on server-error handler and flush only flushes one - assert.equal(window.fetch.callCount, 2); - flush(() => { - // toast - const toast = toastSpy.lastCall.returnValue; - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - assert.include( - Polymer.dom(toast.root).textContent, 'Refresh credentials'); + // close overlay + assert.isTrue(noInteractionOverlay.close.called); done(); }); }); }); + }); - test('regular toast should dismiss regular toast', () => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const toastSpy = sandbox.spy(element, '_createToastAlert'); + test('auth toast should dismiss existing toast', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const responseText = Promise.resolve('Authentication required\n'); - // fake an alert - element.fire('show-alert', {message: 'test reload', action: 'reload'}); - let toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'test reload'); + // fake an alert + element.fire('show-alert', {message: 'test reload', action: 'reload'}); + const toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'test reload'); - // new alert - element.fire('show-alert', {message: 'second-test', action: 'reload'}); - - toast = toastSpy.lastCall.returnValue; - assert.include(Polymer.dom(toast.root).textContent, 'second-test'); - }); - - test('regular toast should not dismiss auth toast', done => { - // starts with authed state - element.$.restAPI.getLoggedIn(); - const toastSpy = sandbox.spy(element, '_createToastAlert'); - const responseText = Promise.resolve('Authentication required\n'); - - // fake auth - window.fetch.returns(Promise.resolve({status: 403})); - element.fire('server-error', - {response: {status: 403, text() { return responseText; }}} - ); - assert.equal(window.fetch.callCount, 1); + // fake auth + window.fetch.returns(Promise.resolve({status: 403})); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + assert.equal(window.fetch.callCount, 1); + flush(() => { + // here needs two flush as there are two chanined + // promises on server-error handler and flush only flushes one + assert.equal(window.fetch.callCount, 2); flush(() => { - // here needs two flush as there are two chanined - // promises on server-error handler and flush only flushes one - assert.equal(window.fetch.callCount, 2); - flush(() => { - let toast = toastSpy.lastCall.returnValue; - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - assert.include( - Polymer.dom(toast.root).textContent, 'Refresh credentials'); + // toast + const toast = toastSpy.lastCall.returnValue; + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + assert.include( + dom(toast.root).textContent, 'Refresh credentials'); + done(); + }); + }); + }); - // fake an alert - element.fire('show-alert', { - message: 'test-alert', action: 'reload', - }); - flush(() => { - toast = toastSpy.lastCall.returnValue; - assert.isOk(toast); - assert.include( - Polymer.dom(toast.root).textContent, 'Credentials expired.'); - done(); - }); + test('regular toast should dismiss regular toast', () => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + + // fake an alert + element.fire('show-alert', {message: 'test reload', action: 'reload'}); + let toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'test reload'); + + // new alert + element.fire('show-alert', {message: 'second-test', action: 'reload'}); + + toast = toastSpy.lastCall.returnValue; + assert.include(dom(toast.root).textContent, 'second-test'); + }); + + test('regular toast should not dismiss auth toast', done => { + // starts with authed state + element.$.restAPI.getLoggedIn(); + const toastSpy = sandbox.spy(element, '_createToastAlert'); + const responseText = Promise.resolve('Authentication required\n'); + + // fake auth + window.fetch.returns(Promise.resolve({status: 403})); + element.fire('server-error', + {response: {status: 403, text() { return responseText; }}} + ); + assert.equal(window.fetch.callCount, 1); + flush(() => { + // here needs two flush as there are two chanined + // promises on server-error handler and flush only flushes one + assert.equal(window.fetch.callCount, 2); + flush(() => { + let toast = toastSpy.lastCall.returnValue; + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + assert.include( + dom(toast.root).textContent, 'Refresh credentials'); + + // fake an alert + element.fire('show-alert', { + message: 'test-alert', action: 'reload', + }); + flush(() => { + toast = toastSpy.lastCall.returnValue; + assert.isOk(toast); + assert.include( + dom(toast.root).textContent, 'Credentials expired.'); + done(); }); }); }); + }); - test('show alert', () => { - const alertObj = {message: 'foo'}; - sandbox.stub(element, '_showAlert'); - element.fire('show-alert', alertObj); - assert.isTrue(element._showAlert.calledOnce); - assert.equal(element._showAlert.lastCall.args[0], 'foo'); - assert.isNotOk(element._showAlert.lastCall.args[1]); - assert.isNotOk(element._showAlert.lastCall.args[2]); - }); + test('show alert', () => { + const alertObj = {message: 'foo'}; + sandbox.stub(element, '_showAlert'); + element.fire('show-alert', alertObj); + assert.isTrue(element._showAlert.calledOnce); + assert.equal(element._showAlert.lastCall.args[0], 'foo'); + assert.isNotOk(element._showAlert.lastCall.args[1]); + assert.isNotOk(element._showAlert.lastCall.args[2]); + }); - test('checks stale credentials on visibility change', () => { - const refreshStub = sandbox.stub(element, - '_checkSignedIn'); - sandbox.stub(Date, 'now').returns(999999); - element._lastCredentialCheck = 0; - element._handleVisibilityChange(); + test('checks stale credentials on visibility change', () => { + const refreshStub = sandbox.stub(element, + '_checkSignedIn'); + sandbox.stub(Date, 'now').returns(999999); + element._lastCredentialCheck = 0; + element._handleVisibilityChange(); - // Since there is no known account, it should not test credentials. - assert.isFalse(refreshStub.called); - assert.equal(element._lastCredentialCheck, 0); + // Since there is no known account, it should not test credentials. + assert.isFalse(refreshStub.called); + assert.equal(element._lastCredentialCheck, 0); - element.knownAccountId = 123; - element._handleVisibilityChange(); + element.knownAccountId = 123; + element._handleVisibilityChange(); - // Should test credentials, since there is a known account. - assert.isTrue(refreshStub.called); - assert.equal(element._lastCredentialCheck, 999999); - }); + // Should test credentials, since there is a known account. + assert.isTrue(refreshStub.called); + assert.equal(element._lastCredentialCheck, 999999); + }); - test('refreshes with same credentials', done => { - const accountPromise = Promise.resolve({_account_id: 1234}); - sandbox.stub(element.$.restAPI, 'getAccount') - .returns(accountPromise); - const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); - const handleRefreshStub = sandbox.stub(element, - '_handleCredentialRefreshed'); - const reloadStub = sandbox.stub(element, '_reloadPage'); + test('refreshes with same credentials', done => { + const accountPromise = Promise.resolve({_account_id: 1234}); + sandbox.stub(element.$.restAPI, 'getAccount') + .returns(accountPromise); + const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, + '_handleCredentialRefreshed'); + const reloadStub = sandbox.stub(element, '_reloadPage'); - element.knownAccountId = 1234; - element._refreshingCredentials = true; - element._checkSignedIn(); + element.knownAccountId = 1234; + element._refreshingCredentials = true; + element._checkSignedIn(); - flush(() => { - assert.isFalse(requestCheckStub.called); - assert.isTrue(handleRefreshStub.called); - assert.isFalse(reloadStub.called); - done(); - }); - }); - - test('_showAlert hides existing alerts', () => { - element._alertElement = element._createToastAlert(); - const hideStub = sandbox.stub(element, '_hideAlert'); - element._showAlert(); - assert.isTrue(hideStub.calledOnce); - }); - - test('show-error', () => { - const openStub = sandbox.stub(element.$.errorOverlay, 'open'); - const closeStub = sandbox.stub(element.$.errorOverlay, 'close'); - const reportStub = sandbox.stub( - element.$.reporting, - 'reportErrorDialog' - ); - - const message = 'test message'; - element.fire('show-error', {message}); - flushAsynchronousOperations(); - - assert.isTrue(openStub.called); - assert.isTrue(reportStub.called); - assert.equal(element.$.errorDialog.text, message); - - element.$.errorDialog.fire('dismiss'); - flushAsynchronousOperations(); - - assert.isTrue(closeStub.called); - }); - - test('reloads when refreshed credentials differ', done => { - const accountPromise = Promise.resolve({_account_id: 1234}); - sandbox.stub(element.$.restAPI, 'getAccount') - .returns(accountPromise); - const requestCheckStub = sandbox.stub( - element, - '_requestCheckLoggedIn'); - const handleRefreshStub = sandbox.stub(element, - '_handleCredentialRefreshed'); - const reloadStub = sandbox.stub(element, '_reloadPage'); - - element.knownAccountId = 4321; // Different from 1234 - element._refreshingCredentials = true; - element._checkSignedIn(); - - flush(() => { - assert.isFalse(requestCheckStub.called); - assert.isFalse(handleRefreshStub.called); - assert.isTrue(reloadStub.called); - done(); - }); + flush(() => { + assert.isFalse(requestCheckStub.called); + assert.isTrue(handleRefreshStub.called); + assert.isFalse(reloadStub.called); + done(); }); }); - suite('when not authed', () => { - setup(() => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - }); - element = fixture('basic'); - }); + test('_showAlert hides existing alerts', () => { + element._alertElement = element._createToastAlert(); + const hideStub = sandbox.stub(element, '_hideAlert'); + element._showAlert(); + assert.isTrue(hideStub.calledOnce); + }); - test('refresh loop continues on credential fail', done => { - const requestCheckStub = sandbox.stub( - element, - '_requestCheckLoggedIn'); - const handleRefreshStub = sandbox.stub(element, - '_handleCredentialRefreshed'); - const reloadStub = sandbox.stub(element, '_reloadPage'); + test('show-error', () => { + const openStub = sandbox.stub(element.$.errorOverlay, 'open'); + const closeStub = sandbox.stub(element.$.errorOverlay, 'close'); + const reportStub = sandbox.stub( + element.$.reporting, + 'reportErrorDialog' + ); - element._refreshingCredentials = true; - element._checkSignedIn(); + const message = 'test message'; + element.fire('show-error', {message}); + flushAsynchronousOperations(); - flush(() => { - assert.isTrue(requestCheckStub.called); - assert.isFalse(handleRefreshStub.called); - assert.isFalse(reloadStub.called); - done(); - }); + assert.isTrue(openStub.called); + assert.isTrue(reportStub.called); + assert.equal(element.$.errorDialog.text, message); + + element.$.errorDialog.fire('dismiss'); + flushAsynchronousOperations(); + + assert.isTrue(closeStub.called); + }); + + test('reloads when refreshed credentials differ', done => { + const accountPromise = Promise.resolve({_account_id: 1234}); + sandbox.stub(element.$.restAPI, 'getAccount') + .returns(accountPromise); + const requestCheckStub = sandbox.stub( + element, + '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, + '_handleCredentialRefreshed'); + const reloadStub = sandbox.stub(element, '_reloadPage'); + + element.knownAccountId = 4321; // Different from 1234 + element._refreshingCredentials = true; + element._checkSignedIn(); + + flush(() => { + assert.isFalse(requestCheckStub.called); + assert.isFalse(handleRefreshStub.called); + assert.isTrue(reloadStub.called); + done(); }); }); }); + + suite('when not authed', () => { + setup(() => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + }); + + test('refresh loop continues on credential fail', done => { + const requestCheckStub = sandbox.stub( + element, + '_requestCheckLoggedIn'); + const handleRefreshStub = sandbox.stub(element, + '_handleCredentialRefreshed'); + const reloadStub = sandbox.stub(element, '_reloadPage'); + + element._refreshingCredentials = true; + element._checkSignedIn(); + + flush(() => { + assert.isTrue(requestCheckStub.called); + assert.isFalse(handleRefreshStub.called); + assert.isFalse(reloadStub.called); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js index 3d424bc..5d7ec27 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display.js
@@ -14,30 +14,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrKeyBindingDisplay extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-key-binding-display'; } +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-key-binding-display_html.js'; - static get properties() { - return { - /** @type {Array<string>} */ - binding: Array, - }; - } +/** @extends Polymer.Element */ +class GrKeyBindingDisplay extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _computeModifiers(binding) { - return binding.slice(0, binding.length - 1); - } + static get is() { return 'gr-key-binding-display'; } - _computeKey(binding) { - return binding[binding.length - 1]; - } + static get properties() { + return { + /** @type {Array<string>} */ + binding: Array, + }; } - customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay); -})(); + _computeModifiers(binding) { + return binding.slice(0, binding.length - 1); + } + + _computeKey(binding) { + return binding[binding.length - 1]; + } +} + +customElements.define(GrKeyBindingDisplay.is, GrKeyBindingDisplay);
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js index a863276..f98be3a 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-key-binding-display"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .key { background-color: var(--chip-background-color); @@ -35,14 +32,9 @@ <template is="dom-if" if="[[index]]"> or </template> - <template - is="dom-repeat" - items="[[_computeModifiers(item)]]" - as="modifier"> + <template is="dom-repeat" items="[[_computeModifiers(item)]]" as="modifier"> <span class="key modifier">[[modifier]]</span> </template> <span class="key">[[_computeKey(item)]]</span> </template> - </template> - <script src="gr-key-binding-display.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html index f682f0a..bb449c3 100644 --- a/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html +++ b/polygerrit-ui/app/elements/core/gr-key-binding-display/gr-key-binding-display_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-key-binding-display</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-key-binding-display.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-key-binding-display.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-key-binding-display.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,37 +39,39 @@ </template> </test-fixture> -<script> - suite('gr-key-binding-display tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-key-binding-display.js'; +suite('gr-key-binding-display tests', () => { + let element; - setup(() => { - element = fixture('basic'); + setup(() => { + element = fixture('basic'); + }); + + suite('_computeKey', () => { + test('unmodified key', () => { + assert.strictEqual(element._computeKey(['x']), 'x'); }); - suite('_computeKey', () => { - test('unmodified key', () => { - assert.strictEqual(element._computeKey(['x']), 'x'); - }); - - test('key with modifiers', () => { - assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x'); - assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x'); - }); - }); - - suite('_computeModifiers', () => { - test('single unmodified key', () => { - assert.deepEqual(element._computeModifiers(['x']), []); - }); - - test('key with modifiers', () => { - assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']); - assert.deepEqual( - element._computeModifiers(['Shift', 'Meta', 'x']), - ['Shift', 'Meta']); - }); + test('key with modifiers', () => { + assert.strictEqual(element._computeKey(['Ctrl', 'x']), 'x'); + assert.strictEqual(element._computeKey(['Shift', 'Meta', 'x']), 'x'); }); }); + + suite('_computeModifiers', () => { + test('single unmodified key', () => { + assert.deepEqual(element._computeModifiers(['x']), []); + }); + + test('key with modifiers', () => { + assert.deepEqual(element._computeModifiers(['Ctrl', 'x']), ['Ctrl']); + assert.deepEqual( + element._computeModifiers(['Shift', 'Meta', 'x']), + ['Shift', 'Meta']); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js index 4630ca7..371ba02 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js
@@ -14,129 +14,140 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../shared/gr-button/gr-button.js'; +import '../gr-key-binding-display/gr-key-binding-display.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-keyboard-shortcuts-dialog_html.js'; +const {ShortcutSection} = window.Gerrit.KeyboardShortcutBinder; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrKeyboardShortcutsDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-keyboard-shortcuts-dialog'; } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when the user presses the close button. + * + * @event close */ - class GrKeyboardShortcutsDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-keyboard-shortcuts-dialog'; } - /** - * Fired when the user presses the close button. - * - * @event close - */ - static get properties() { - return { - _left: Array, - _right: Array, + static get properties() { + return { + _left: Array, + _right: Array, - _propertyBySection: { - type: Object, - value() { - return { - [ShortcutSection.EVERYWHERE]: '_everywhere', - [ShortcutSection.NAVIGATION]: '_navigation', - [ShortcutSection.DASHBOARD]: '_dashboard', - [ShortcutSection.CHANGE_LIST]: '_changeList', - [ShortcutSection.ACTIONS]: '_actions', - [ShortcutSection.REPLY_DIALOG]: '_replyDialog', - [ShortcutSection.FILE_LIST]: '_fileList', - [ShortcutSection.DIFFS]: '_diffs', - }; - }, + _propertyBySection: { + type: Object, + value() { + return { + [ShortcutSection.EVERYWHERE]: '_everywhere', + [ShortcutSection.NAVIGATION]: '_navigation', + [ShortcutSection.DASHBOARD]: '_dashboard', + [ShortcutSection.CHANGE_LIST]: '_changeList', + [ShortcutSection.ACTIONS]: '_actions', + [ShortcutSection.REPLY_DIALOG]: '_replyDialog', + [ShortcutSection.FILE_LIST]: '_fileList', + [ShortcutSection.DIFFS]: '_diffs', + }; }, - }; - } - - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'dialog'); - } - - /** @override */ - attached() { - super.attached(); - this.addKeyboardShortcutDirectoryListener( - this._onDirectoryUpdated.bind(this)); - } - - /** @override */ - detached() { - super.detached(); - this.removeKeyboardShortcutDirectoryListener( - this._onDirectoryUpdated.bind(this)); - } - - _handleCloseTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('close', null, {bubbles: false}); - } - - _onDirectoryUpdated(directory) { - const left = []; - const right = []; - - if (directory.has(ShortcutSection.EVERYWHERE)) { - left.push({ - section: ShortcutSection.EVERYWHERE, - shortcuts: directory.get(ShortcutSection.EVERYWHERE), - }); - } - - if (directory.has(ShortcutSection.NAVIGATION)) { - left.push({ - section: ShortcutSection.NAVIGATION, - shortcuts: directory.get(ShortcutSection.NAVIGATION), - }); - } - - if (directory.has(ShortcutSection.ACTIONS)) { - right.push({ - section: ShortcutSection.ACTIONS, - shortcuts: directory.get(ShortcutSection.ACTIONS), - }); - } - - if (directory.has(ShortcutSection.REPLY_DIALOG)) { - right.push({ - section: ShortcutSection.REPLY_DIALOG, - shortcuts: directory.get(ShortcutSection.REPLY_DIALOG), - }); - } - - if (directory.has(ShortcutSection.FILE_LIST)) { - right.push({ - section: ShortcutSection.FILE_LIST, - shortcuts: directory.get(ShortcutSection.FILE_LIST), - }); - } - - if (directory.has(ShortcutSection.DIFFS)) { - right.push({ - section: ShortcutSection.DIFFS, - shortcuts: directory.get(ShortcutSection.DIFFS), - }); - } - - this.set('_left', left); - this.set('_right', right); - } + }, + }; } - customElements.define(GrKeyboardShortcutsDialog.is, - GrKeyboardShortcutsDialog); -})(); + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } + + /** @override */ + attached() { + super.attached(); + this.addKeyboardShortcutDirectoryListener( + this._onDirectoryUpdated.bind(this)); + } + + /** @override */ + detached() { + super.detached(); + this.removeKeyboardShortcutDirectoryListener( + this._onDirectoryUpdated.bind(this)); + } + + _handleCloseTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('close', null, {bubbles: false}); + } + + _onDirectoryUpdated(directory) { + const left = []; + const right = []; + + if (directory.has(ShortcutSection.EVERYWHERE)) { + left.push({ + section: ShortcutSection.EVERYWHERE, + shortcuts: directory.get(ShortcutSection.EVERYWHERE), + }); + } + + if (directory.has(ShortcutSection.NAVIGATION)) { + left.push({ + section: ShortcutSection.NAVIGATION, + shortcuts: directory.get(ShortcutSection.NAVIGATION), + }); + } + + if (directory.has(ShortcutSection.ACTIONS)) { + right.push({ + section: ShortcutSection.ACTIONS, + shortcuts: directory.get(ShortcutSection.ACTIONS), + }); + } + + if (directory.has(ShortcutSection.REPLY_DIALOG)) { + right.push({ + section: ShortcutSection.REPLY_DIALOG, + shortcuts: directory.get(ShortcutSection.REPLY_DIALOG), + }); + } + + if (directory.has(ShortcutSection.FILE_LIST)) { + right.push({ + section: ShortcutSection.FILE_LIST, + shortcuts: directory.get(ShortcutSection.FILE_LIST), + }); + } + + if (directory.has(ShortcutSection.DIFFS)) { + right.push({ + section: ShortcutSection.DIFFS, + shortcuts: directory.get(ShortcutSection.DIFFS), + }); + } + + this.set('_left', left); + this.set('_right', right); + } +} + +customElements.define(GrKeyboardShortcutsDialog.is, + GrKeyboardShortcutsDialog);
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js index a4424a2..380228f 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../gr-key-binding-display/gr-key-binding-display.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-keyboard-shortcuts-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -63,7 +56,7 @@ </style> <header> <h3>Keyboard shortcuts</h3> - <gr-button link on-click="_handleCloseTap">Close</gr-button> + <gr-button link="" on-click="_handleCloseTap">Close</gr-button> </header> <main> <table> @@ -106,6 +99,4 @@ </template> </main> <footer></footer> - </template> - <script src="gr-keyboard-shortcuts-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html index cc53db17..eedd166 100644 --- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html +++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-key-binding-display</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-keyboard-shortcuts-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-keyboard-shortcuts-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-keyboard-shortcuts-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,150 +39,152 @@ </template> </test-fixture> -<script> - suite('gr-keyboard-shortcuts-dialog tests', async () => { - await readyToTest(); - const kb = window.Gerrit.KeyboardShortcutBinder; - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-keyboard-shortcuts-dialog.js'; +suite('gr-keyboard-shortcuts-dialog tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + let element; - setup(() => { - element = fixture('basic'); + setup(() => { + element = fixture('basic'); + }); + + function update(directory) { + element._onDirectoryUpdated(directory); + flushAsynchronousOperations(); + } + + suite('_left and _right contents', () => { + test('empty dialog', () => { + assert.strictEqual(element._left.length, 0); + assert.strictEqual(element._right.length, 0); }); - function update(directory) { - element._onDirectoryUpdated(directory); - flushAsynchronousOperations(); - } + test('everywhere goes on left', () => { + update(new Map([ + [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], + ])); + assert.deepEqual( + element._left, + [ + { + section: kb.ShortcutSection.EVERYWHERE, + shortcuts: ['everywhere shortcuts'], + }, + ]); + assert.strictEqual(element._right.length, 0); + }); - suite('_left and _right contents', () => { - test('empty dialog', () => { - assert.strictEqual(element._left.length, 0); - assert.strictEqual(element._right.length, 0); - }); + test('navigation goes on left', () => { + update(new Map([ + [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], + ])); + assert.deepEqual( + element._left, + [ + { + section: kb.ShortcutSection.NAVIGATION, + shortcuts: ['navigation shortcuts'], + }, + ]); + assert.strictEqual(element._right.length, 0); + }); - test('everywhere goes on left', () => { - update(new Map([ - [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], - ])); - assert.deepEqual( - element._left, - [ - { - section: kb.ShortcutSection.EVERYWHERE, - shortcuts: ['everywhere shortcuts'], - }, - ]); - assert.strictEqual(element._right.length, 0); - }); + test('actions go on right', () => { + update(new Map([ + [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.ACTIONS, + shortcuts: ['actions shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('navigation goes on left', () => { - update(new Map([ - [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], - ])); - assert.deepEqual( - element._left, - [ - { - section: kb.ShortcutSection.NAVIGATION, - shortcuts: ['navigation shortcuts'], - }, - ]); - assert.strictEqual(element._right.length, 0); - }); + test('reply dialog goes on right', () => { + update(new Map([ + [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.REPLY_DIALOG, + shortcuts: ['reply dialog shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('actions go on right', () => { - update(new Map([ - [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.ACTIONS, - shortcuts: ['actions shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); + test('file list goes on right', () => { + update(new Map([ + [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.FILE_LIST, + shortcuts: ['file list shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('reply dialog goes on right', () => { - update(new Map([ - [kb.ShortcutSection.REPLY_DIALOG, ['reply dialog shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.REPLY_DIALOG, - shortcuts: ['reply dialog shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); + test('diffs go on right', () => { + update(new Map([ + [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], + ])); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.DIFFS, + shortcuts: ['diffs shortcuts'], + }, + ]); + assert.strictEqual(element._left.length, 0); + }); - test('file list goes on right', () => { - update(new Map([ - [kb.ShortcutSection.FILE_LIST, ['file list shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.FILE_LIST, - shortcuts: ['file list shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); - - test('diffs go on right', () => { - update(new Map([ - [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], - ])); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.DIFFS, - shortcuts: ['diffs shortcuts'], - }, - ]); - assert.strictEqual(element._left.length, 0); - }); - - test('multiple sections on each side', () => { - update(new Map([ - [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], - [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], - [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], - [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], - ])); - assert.deepEqual( - element._left, - [ - { - section: kb.ShortcutSection.EVERYWHERE, - shortcuts: ['everywhere shortcuts'], - }, - { - section: kb.ShortcutSection.NAVIGATION, - shortcuts: ['navigation shortcuts'], - }, - ]); - assert.deepEqual( - element._right, - [ - { - section: kb.ShortcutSection.ACTIONS, - shortcuts: ['actions shortcuts'], - }, - { - section: kb.ShortcutSection.DIFFS, - shortcuts: ['diffs shortcuts'], - }, - ]); - }); + test('multiple sections on each side', () => { + update(new Map([ + [kb.ShortcutSection.ACTIONS, ['actions shortcuts']], + [kb.ShortcutSection.DIFFS, ['diffs shortcuts']], + [kb.ShortcutSection.EVERYWHERE, ['everywhere shortcuts']], + [kb.ShortcutSection.NAVIGATION, ['navigation shortcuts']], + ])); + assert.deepEqual( + element._left, + [ + { + section: kb.ShortcutSection.EVERYWHERE, + shortcuts: ['everywhere shortcuts'], + }, + { + section: kb.ShortcutSection.NAVIGATION, + shortcuts: ['navigation shortcuts'], + }, + ]); + assert.deepEqual( + element._right, + [ + { + section: kb.ShortcutSection.ACTIONS, + shortcuts: ['actions shortcuts'], + }, + { + section: kb.ShortcutSection.DIFFS, + shortcuts: ['diffs shortcuts'], + }, + ]); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js index 05765fb..c8ed50c 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,332 +14,349 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DEFAULT_LINKS = [{ - title: 'Changes', - links: [ - { - url: '/q/status:open', - name: 'Open', +import '../../../behaviors/docs-url-behavior/docs-url-behavior.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-account-dropdown/gr-account-dropdown.js'; +import '../gr-smart-search/gr-smart-search.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-main-header_html.js'; + +const DEFAULT_LINKS = [{ + title: 'Changes', + links: [ + { + url: '/q/status:open', + name: 'Open', + }, + { + url: '/q/status:merged', + name: 'Merged', + }, + { + url: '/q/status:abandoned', + name: 'Abandoned', + }, + ], +}]; + +const DOCUMENTATION_LINKS = [ + { + url: '/index.html', + name: 'Table of Contents', + }, + { + url: '/user-search.html', + name: 'Searching', + }, + { + url: '/user-upload.html', + name: 'Uploading', + }, + { + url: '/access-control.html', + name: 'Access Control', + }, + { + url: '/rest-api.html', + name: 'REST API', + }, + { + url: '/intro-project-owner.html', + name: 'Project Owner Guide', + }, +]; + +// Set of authentication methods that can provide custom registration page. +const AUTH_TYPES_WITH_REGISTER_URL = new Set([ + 'LDAP', + 'LDAP_BIND', + 'CUSTOM_EXTENSION', +]); + +/** + * @appliesMixin Gerrit.AdminNavMixin + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.DocsUrlMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrMainHeader extends mixinBehaviors( [ + Gerrit.AdminNavBehavior, + Gerrit.BaseUrlBehavior, + Gerrit.DocsUrlBehavior, + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-main-header'; } + + static get properties() { + return { + searchQuery: { + type: String, + notify: true, }, - { - url: '/q/status:merged', - name: 'Merged', + loggedIn: { + type: Boolean, + reflectToAttribute: true, }, - { - url: '/q/status:abandoned', - name: 'Abandoned', + loading: { + type: Boolean, + reflectToAttribute: true, }, - ], - }]; - const DOCUMENTATION_LINKS = [ - { - url: '/index.html', - name: 'Table of Contents', - }, - { - url: '/user-search.html', - name: 'Searching', - }, - { - url: '/user-upload.html', - name: 'Uploading', - }, - { - url: '/access-control.html', - name: 'Access Control', - }, - { - url: '/rest-api.html', - name: 'REST API', - }, - { - url: '/intro-project-owner.html', - name: 'Project Owner Guide', - }, - ]; + /** @type {?Object} */ + _account: Object, + _adminLinks: { + type: Array, + value() { return []; }, + }, + _defaultLinks: { + type: Array, + value() { + return DEFAULT_LINKS; + }, + }, + _docBaseUrl: { + type: String, + value: null, + }, + _links: { + type: Array, + computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + + '_topMenus, _docBaseUrl)', + }, + loginUrl: { + type: String, + value: '/login', + }, + _userLinks: { + type: Array, + value() { return []; }, + }, + _topMenus: { + type: Array, + value() { return []; }, + }, + _registerText: { + type: String, + value: 'Sign up', + }, + _registerURL: { + type: String, + value: null, + }, + }; + } - // Set of authentication methods that can provide custom registration page. - const AUTH_TYPES_WITH_REGISTER_URL = new Set([ - 'LDAP', - 'LDAP_BIND', - 'CUSTOM_EXTENSION', - ]); + static get observers() { + return [ + '_accountLoaded(_account)', + ]; + } - /** - * @appliesMixin Gerrit.AdminNavMixin - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.DocsUrlMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrMainHeader extends Polymer.mixinBehaviors( [ - Gerrit.AdminNavBehavior, - Gerrit.BaseUrlBehavior, - Gerrit.DocsUrlBehavior, - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-main-header'; } + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'banner'); + } - static get properties() { + /** @override */ + attached() { + super.attached(); + this._loadAccount(); + this._loadConfig(); + } + + /** @override */ + detached() { + super.detached(); + } + + reload() { + this._loadAccount(); + } + + _computeRelativeURL(path) { + return '//' + window.location.host + this.getBaseUrl() + path; + } + + _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) { + // Polymer 2: check for undefined + if ([ + defaultLinks, + userLinks, + adminLinks, + topMenus, + docBaseUrl, + ].some(arg => arg === undefined)) { + return undefined; + } + + const links = defaultLinks.map(menu => { return { - searchQuery: { - type: String, - notify: true, - }, - loggedIn: { - type: Boolean, - reflectToAttribute: true, - }, - loading: { - type: Boolean, - reflectToAttribute: true, - }, - - /** @type {?Object} */ - _account: Object, - _adminLinks: { - type: Array, - value() { return []; }, - }, - _defaultLinks: { - type: Array, - value() { - return DEFAULT_LINKS; - }, - }, - _docBaseUrl: { - type: String, - value: null, - }, - _links: { - type: Array, - computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks, ' + - '_topMenus, _docBaseUrl)', - }, - loginUrl: { - type: String, - value: '/login', - }, - _userLinks: { - type: Array, - value() { return []; }, - }, - _topMenus: { - type: Array, - value() { return []; }, - }, - _registerText: { - type: String, - value: 'Sign up', - }, - _registerURL: { - type: String, - value: null, - }, + title: menu.title, + links: menu.links.slice(), }; - } - - static get observers() { - return [ - '_accountLoaded(_account)', - ]; - } - - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'banner'); - } - - /** @override */ - attached() { - super.attached(); - this._loadAccount(); - this._loadConfig(); - } - - /** @override */ - detached() { - super.detached(); - } - - reload() { - this._loadAccount(); - } - - _computeRelativeURL(path) { - return '//' + window.location.host + this.getBaseUrl() + path; - } - - _computeLinks(defaultLinks, userLinks, adminLinks, topMenus, docBaseUrl) { - // Polymer 2: check for undefined - if ([ - defaultLinks, - userLinks, - adminLinks, - topMenus, - docBaseUrl, - ].some(arg => arg === undefined)) { - return undefined; - } - - const links = defaultLinks.map(menu => { - return { - title: menu.title, - links: menu.links.slice(), - }; - }); - if (userLinks && userLinks.length > 0) { - links.push({ - title: 'Your', - links: userLinks.slice(), - }); - } - const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS); - if (docLinks.length) { - links.push({ - title: 'Documentation', - links: docLinks, - class: 'hideOnMobile', - }); - } + }); + if (userLinks && userLinks.length > 0) { links.push({ - title: 'Browse', - links: adminLinks.slice(), + title: 'Your', + links: userLinks.slice(), }); - const topMenuLinks = []; - links.forEach(link => { topMenuLinks[link.title] = link.links; }); - for (const m of topMenus) { - const items = m.items.map(this._fixCustomMenuItem).filter(link => - // Ignore GWT project links - !link.url.includes('${projectName}') - ); - if (m.name in topMenuLinks) { - items.forEach(link => { topMenuLinks[m.name].push(link); }); - } else { - links.push({ - title: m.name, - links: topMenuLinks[m.name] = items, + } + const docLinks = this._getDocLinks(docBaseUrl, DOCUMENTATION_LINKS); + if (docLinks.length) { + links.push({ + title: 'Documentation', + links: docLinks, + class: 'hideOnMobile', + }); + } + links.push({ + title: 'Browse', + links: adminLinks.slice(), + }); + const topMenuLinks = []; + links.forEach(link => { topMenuLinks[link.title] = link.links; }); + for (const m of topMenus) { + const items = m.items.map(this._fixCustomMenuItem).filter(link => + // Ignore GWT project links + !link.url.includes('${projectName}') + ); + if (m.name in topMenuLinks) { + items.forEach(link => { topMenuLinks[m.name].push(link); }); + } else { + links.push({ + title: m.name, + links: topMenuLinks[m.name] = items, + }); + } + } + return links; + } + + _getDocLinks(docBaseUrl, docLinks) { + if (!docBaseUrl || !docLinks) { + return []; + } + return docLinks.map(link => { + let url = docBaseUrl; + if (url && url[url.length - 1] === '/') { + url = url.substring(0, url.length - 1); + } + return { + url: url + link.url, + name: link.name, + target: '_blank', + }; + }); + } + + _loadAccount() { + this.loading = true; + const promises = [ + this.$.restAPI.getAccount(), + this.$.restAPI.getTopMenus(), + Gerrit.awaitPluginsLoaded(), + ]; + + return Promise.all(promises).then(result => { + const account = result[0]; + this._account = account; + this.loggedIn = !!account; + this.loading = false; + this._topMenus = result[1]; + + return this.getAdminLinks(account, + this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI), + this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI)) + .then(res => { + this._adminLinks = res.links; }); - } + }); + } + + _loadConfig() { + this.$.restAPI.getConfig() + .then(config => { + this._retrieveRegisterURL(config); + return this.getDocsBaseUrl(config, this.$.restAPI); + }) + .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; }); + } + + _accountLoaded(account) { + if (!account) { return; } + + this.$.restAPI.getPreferences().then(prefs => { + this._userLinks = prefs && prefs.my ? + prefs.my.map(this._fixCustomMenuItem) : []; + }); + } + + _retrieveRegisterURL(config) { + if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) { + this._registerURL = config.auth.register_url; + if (config.auth.register_text) { + this._registerText = config.auth.register_text; } - return links; - } - - _getDocLinks(docBaseUrl, docLinks) { - if (!docBaseUrl || !docLinks) { - return []; - } - return docLinks.map(link => { - let url = docBaseUrl; - if (url && url[url.length - 1] === '/') { - url = url.substring(0, url.length - 1); - } - return { - url: url + link.url, - name: link.name, - target: '_blank', - }; - }); - } - - _loadAccount() { - this.loading = true; - const promises = [ - this.$.restAPI.getAccount(), - this.$.restAPI.getTopMenus(), - Gerrit.awaitPluginsLoaded(), - ]; - - return Promise.all(promises).then(result => { - const account = result[0]; - this._account = account; - this.loggedIn = !!account; - this.loading = false; - this._topMenus = result[1]; - - return this.getAdminLinks(account, - this.$.restAPI.getAccountCapabilities.bind(this.$.restAPI), - this.$.jsAPI.getAdminMenuLinks.bind(this.$.jsAPI)) - .then(res => { - this._adminLinks = res.links; - }); - }); - } - - _loadConfig() { - this.$.restAPI.getConfig() - .then(config => { - this._retrieveRegisterURL(config); - return this.getDocsBaseUrl(config, this.$.restAPI); - }) - .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; }); - } - - _accountLoaded(account) { - if (!account) { return; } - - this.$.restAPI.getPreferences().then(prefs => { - this._userLinks = prefs && prefs.my ? - prefs.my.map(this._fixCustomMenuItem) : []; - }); - } - - _retrieveRegisterURL(config) { - if (AUTH_TYPES_WITH_REGISTER_URL.has(config.auth.auth_type)) { - this._registerURL = config.auth.register_url; - if (config.auth.register_text) { - this._registerText = config.auth.register_text; - } - } - } - - _computeIsInvisible(registerURL) { - return registerURL ? '' : 'invisible'; - } - - _fixCustomMenuItem(linkObj) { - // Normalize all urls to PolyGerrit style. - if (linkObj.url.startsWith('#')) { - linkObj.url = linkObj.url.slice(1); - } - - // Delete target property due to complications of - // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888 - // - // The server tries to guess whether URL is a view within the UI. - // If not, it sets target='_blank' on the menu item. The server - // makes assumptions that work for the GWT UI, but not PolyGerrit, - // so we'll just disable it altogether for now. - delete linkObj.target; - - return linkObj; - } - - _generateSettingsLink() { - return this.getBaseUrl() + '/settings/'; - } - - _onMobileSearchTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('mobile-search', null, {bubbles: false}); - } - - _computeLinkGroupClass(linkGroup) { - if (linkGroup && linkGroup.class) { - return linkGroup.class; - } - - return ''; } } - customElements.define(GrMainHeader.is, GrMainHeader); -})(); + _computeIsInvisible(registerURL) { + return registerURL ? '' : 'invisible'; + } + + _fixCustomMenuItem(linkObj) { + // Normalize all urls to PolyGerrit style. + if (linkObj.url.startsWith('#')) { + linkObj.url = linkObj.url.slice(1); + } + + // Delete target property due to complications of + // https://bugs.chromium.org/p/gerrit/issues/detail?id=5888 + // + // The server tries to guess whether URL is a view within the UI. + // If not, it sets target='_blank' on the menu item. The server + // makes assumptions that work for the GWT UI, but not PolyGerrit, + // so we'll just disable it altogether for now. + delete linkObj.target; + + return linkObj; + } + + _generateSettingsLink() { + return this.getBaseUrl() + '/settings/'; + } + + _onMobileSearchTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('mobile-search', null, {bubbles: false}); + } + + _computeLinkGroupClass(linkGroup) { + if (linkGroup && linkGroup.class) { + return linkGroup.class; + } + + return ''; + } +} + +customElements.define(GrMainHeader.is, GrMainHeader);
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js index 51717a8..307a081 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_html.js
@@ -1,35 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-admin-nav-behavior/gr-admin-nav-behavior.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-account-dropdown/gr-account-dropdown.html"> -<link rel="import" href="../gr-smart-search/gr-smart-search.html"> - -<dom-module id="gr-main-header"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -183,19 +170,15 @@ } </style> <nav> - <a href$="[[_computeRelativeURL('/')]]" class="bigTitle"> + <a href\$="[[_computeRelativeURL('/')]]" class="bigTitle"> <gr-endpoint-decorator name="header-title"> <span class="titleText"></span> </gr-endpoint-decorator> </a> <ul class="links"> <template is="dom-repeat" items="[[_links]]" as="linkGroup"> - <li class$="[[_computeLinkGroupClass(linkGroup)]]"> - <gr-dropdown - link - down-arrow - items = [[linkGroup.links]] - horizontal-align="left"> + <li class\$="[[_computeLinkGroupClass(linkGroup)]]"> + <gr-dropdown link="" down-arrow="" items="[[linkGroup.links]]" horizontal-align="left"> <span class="linksTitle" id="[[linkGroup.title]]"> [[linkGroup.title]] </span> @@ -204,29 +187,18 @@ </template> </ul> <div class="rightItems"> - <gr-endpoint-decorator - class="hideOnMobile" - name="header-small-banner"></gr-endpoint-decorator> - <gr-smart-search - id="search" - search-query="{{searchQuery}}"></gr-smart-search> - <gr-endpoint-decorator - class="hideOnMobile" - name="header-browse-source"></gr-endpoint-decorator> + <gr-endpoint-decorator class="hideOnMobile" name="header-small-banner"></gr-endpoint-decorator> + <gr-smart-search id="search" search-query="{{searchQuery}}"></gr-smart-search> + <gr-endpoint-decorator class="hideOnMobile" name="header-browse-source"></gr-endpoint-decorator> <div class="accountContainer" id="accountContainer"> - <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap='_onMobileSearchTap'></iron-icon> - <div class$="[[_computeIsInvisible(_registerURL)]]"> - <a - class="registerButton" - href$="[[_registerURL]]"> + <iron-icon id="mobileSearch" icon="gr-icons:search" on-tap="_onMobileSearchTap"></iron-icon> + <div class\$="[[_computeIsInvisible(_registerURL)]]"> + <a class="registerButton" href\$="[[_registerURL]]"> [[_registerText]] </a> </div> - <a class="loginButton" href$="[[loginUrl]]">Sign in</a> - <a - class="settingsButton" - href$="[[_generateSettingsLink()]]" - title="Settings"> + <a class="loginButton" href\$="[[loginUrl]]">Sign in</a> + <a class="settingsButton" href\$="[[_generateSettingsLink()]]" title="Settings"> <iron-icon icon="gr-icons:settings"></iron-icon> </a> <gr-account-dropdown account="[[_account]]"></gr-account-dropdown> @@ -235,6 +207,4 @@ </nav> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-main-header.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html index 8fe3fca..817643b 100644 --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-main-header</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-main-header.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-main-header.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-main-header.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,379 +40,381 @@ </template> </test-fixture> -<script> - suite('gr-main-header tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-main-header.js'; +suite('gr-main-header tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - probePath(path) { return Promise.resolve(false); }, - }); - stub('gr-main-header', { - _loadAccount() {}, - }); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + probePath(path) { return Promise.resolve(false); }, }); - - teardown(() => { - sandbox.restore(); + stub('gr-main-header', { + _loadAccount() {}, }); - - test('link visibility', () => { - element.loading = true; - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.accountContainer')).display, - 'none'); - element.loading = false; - element.loggedIn = false; - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.accountContainer')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.loginButton')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.registerButton')).display, - 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('gr-account-dropdown')).display, - 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.settingsButton')).display, - 'none'); - element.loggedIn = true; - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.loginButton')).display, - 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.registerButton')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('gr-account-dropdown')) - .display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.settingsButton')).display, - 'none'); - }); - - test('fix my menu item', () => { - assert.deepEqual([ - {url: 'https://awesometown.com/#hashyhash'}, - {url: 'url', target: '_blank'}, - ].map(element._fixCustomMenuItem), [ - {url: 'https://awesometown.com/#hashyhash'}, - {url: 'url'}, - ]); - }); - - test('user links', () => { - const defaultLinks = [{ - title: 'Faves', - links: [{ - name: 'Pinterest', - url: 'https://pinterest.com', - }], - }]; - const userLinks = [{ - name: 'Facebook', - url: 'https://facebook.com', - }]; - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - - // When no admin links are passed, it should use the default. - assert.deepEqual(element._computeLinks( - defaultLinks, - /* userLinks= */[], - adminLinks, - /* topMenus= */[], - /* docBaseUrl= */ '' - ), - defaultLinks.concat({ - title: 'Browse', - links: adminLinks, - })); - assert.deepEqual(element._computeLinks( - defaultLinks, - userLinks, - adminLinks, - /* topMenus= */[], - /* docBaseUrl= */ '' - ), - defaultLinks.concat([ - { - title: 'Your', - links: userLinks, - }, - { - title: 'Browse', - links: adminLinks, - }]) - ); - }); - - test('documentation links', () => { - const docLinks = [ - { - name: 'Table of Contents', - url: '/index.html', - }, - ]; - - assert.deepEqual(element._getDocLinks(null, docLinks), []); - assert.deepEqual(element._getDocLinks('', docLinks), []); - assert.deepEqual(element._getDocLinks('base', null), []); - assert.deepEqual(element._getDocLinks('base', []), []); - - assert.deepEqual(element._getDocLinks('base', docLinks), [{ - name: 'Table of Contents', - target: '_blank', - url: 'base/index.html', - }]); - - assert.deepEqual(element._getDocLinks('base/', docLinks), [{ - name: 'Table of Contents', - target: '_blank', - url: 'base/index.html', - }]); - }); - - test('top menus', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Plugins', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks, - }, - { - title: 'Plugins', - links: [{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]); - }); - - test('ignore top project menus', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Projects', - items: [{ - name: 'Project Settings', - target: '_blank', - url: '/plugins/myplugin/${projectName}', - }, { - name: 'Project List', - target: '_blank', - url: '/plugins/myplugin/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks, - }, - { - title: 'Projects', - links: [{ - name: 'Project List', - url: '/plugins/myplugin/index.html', - }], - }]); - }); - - test('merge top menus', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Plugins', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }, { - name: 'Plugins', - items: [{ - name: 'Create', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/create.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks, - }, { - title: 'Plugins', - links: [{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }, { - name: 'Create', - url: 'https://gerrit/plugins/plugin-manager/static/create.html', - }], - }]); - }); - - test('merge top menus in default links', () => { - const defaultLinks = [{ - title: 'Faves', - links: [{ - name: 'Pinterest', - url: 'https://pinterest.com', - }], - }]; - const topMenus = [{ - name: 'Faves', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - defaultLinks, - /* userLinks= */ [], - /* adminLinks= */ [], - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Faves', - links: defaultLinks[0].links.concat([{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }]), - }, { - title: 'Browse', - links: [], - }]); - }); - - test('merge top menus in user links', () => { - const userLinks = [{ - name: 'Facebook', - url: 'https://facebook.com', - }]; - const topMenus = [{ - name: 'Your', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - userLinks, - /* adminLinks= */ [], - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Your', - links: userLinks.concat([{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }]), - }, { - title: 'Browse', - links: [], - }]); - }); - - test('merge top menus in admin links', () => { - const adminLinks = [{ - name: 'Repos', - url: '/repos', - }]; - const topMenus = [{ - name: 'Browse', - items: [{ - name: 'Manage', - target: '_blank', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }], - }]; - assert.deepEqual(element._computeLinks( - /* defaultLinks= */ [], - /* userLinks= */ [], - adminLinks, - topMenus, - /* baseDocUrl= */ '' - ), [{ - title: 'Browse', - links: adminLinks.concat([{ - name: 'Manage', - url: 'https://gerrit/plugins/plugin-manager/static/index.html', - }]), - }]); - }); - - test('register URL', () => { - const config = { - auth: { - auth_type: 'LDAP', - register_url: 'https//gerrit.example.com/register', - }, - }; - element._retrieveRegisterURL(config); - assert.equal(element._registerURL, config.auth.register_url); - assert.equal(element._registerText, 'Sign up'); - - config.auth.register_text = 'Create account'; - element._retrieveRegisterURL(config); - assert.equal(element._registerURL, config.auth.register_url); - assert.equal(element._registerText, config.auth.register_text); - }); - - test('register URL ignored for wrong auth type', () => { - const config = { - auth: { - auth_type: 'OPENID', - register_url: 'https//gerrit.example.com/register', - }, - }; - element._retrieveRegisterURL(config); - assert.equal(element._registerURL, null); - assert.equal(element._registerText, 'Sign up'); - }); + element = fixture('basic'); }); - </script> + + teardown(() => { + sandbox.restore(); + }); + + test('link visibility', () => { + element.loading = true; + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.accountContainer')).display, + 'none'); + element.loading = false; + element.loggedIn = false; + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.accountContainer')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.loginButton')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.registerButton')).display, + 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('gr-account-dropdown')).display, + 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.settingsButton')).display, + 'none'); + element.loggedIn = true; + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.loginButton')).display, + 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.registerButton')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('gr-account-dropdown')) + .display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.settingsButton')).display, + 'none'); + }); + + test('fix my menu item', () => { + assert.deepEqual([ + {url: 'https://awesometown.com/#hashyhash'}, + {url: 'url', target: '_blank'}, + ].map(element._fixCustomMenuItem), [ + {url: 'https://awesometown.com/#hashyhash'}, + {url: 'url'}, + ]); + }); + + test('user links', () => { + const defaultLinks = [{ + title: 'Faves', + links: [{ + name: 'Pinterest', + url: 'https://pinterest.com', + }], + }]; + const userLinks = [{ + name: 'Facebook', + url: 'https://facebook.com', + }]; + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + + // When no admin links are passed, it should use the default. + assert.deepEqual(element._computeLinks( + defaultLinks, + /* userLinks= */[], + adminLinks, + /* topMenus= */[], + /* docBaseUrl= */ '' + ), + defaultLinks.concat({ + title: 'Browse', + links: adminLinks, + })); + assert.deepEqual(element._computeLinks( + defaultLinks, + userLinks, + adminLinks, + /* topMenus= */[], + /* docBaseUrl= */ '' + ), + defaultLinks.concat([ + { + title: 'Your', + links: userLinks, + }, + { + title: 'Browse', + links: adminLinks, + }]) + ); + }); + + test('documentation links', () => { + const docLinks = [ + { + name: 'Table of Contents', + url: '/index.html', + }, + ]; + + assert.deepEqual(element._getDocLinks(null, docLinks), []); + assert.deepEqual(element._getDocLinks('', docLinks), []); + assert.deepEqual(element._getDocLinks('base', null), []); + assert.deepEqual(element._getDocLinks('base', []), []); + + assert.deepEqual(element._getDocLinks('base', docLinks), [{ + name: 'Table of Contents', + target: '_blank', + url: 'base/index.html', + }]); + + assert.deepEqual(element._getDocLinks('base/', docLinks), [{ + name: 'Table of Contents', + target: '_blank', + url: 'base/index.html', + }]); + }); + + test('top menus', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Plugins', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks, + }, + { + title: 'Plugins', + links: [{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]); + }); + + test('ignore top project menus', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Projects', + items: [{ + name: 'Project Settings', + target: '_blank', + url: '/plugins/myplugin/${projectName}', + }, { + name: 'Project List', + target: '_blank', + url: '/plugins/myplugin/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks, + }, + { + title: 'Projects', + links: [{ + name: 'Project List', + url: '/plugins/myplugin/index.html', + }], + }]); + }); + + test('merge top menus', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Plugins', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }, { + name: 'Plugins', + items: [{ + name: 'Create', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/create.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks, + }, { + title: 'Plugins', + links: [{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }, { + name: 'Create', + url: 'https://gerrit/plugins/plugin-manager/static/create.html', + }], + }]); + }); + + test('merge top menus in default links', () => { + const defaultLinks = [{ + title: 'Faves', + links: [{ + name: 'Pinterest', + url: 'https://pinterest.com', + }], + }]; + const topMenus = [{ + name: 'Faves', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + defaultLinks, + /* userLinks= */ [], + /* adminLinks= */ [], + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Faves', + links: defaultLinks[0].links.concat([{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }]), + }, { + title: 'Browse', + links: [], + }]); + }); + + test('merge top menus in user links', () => { + const userLinks = [{ + name: 'Facebook', + url: 'https://facebook.com', + }]; + const topMenus = [{ + name: 'Your', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + userLinks, + /* adminLinks= */ [], + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Your', + links: userLinks.concat([{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }]), + }, { + title: 'Browse', + links: [], + }]); + }); + + test('merge top menus in admin links', () => { + const adminLinks = [{ + name: 'Repos', + url: '/repos', + }]; + const topMenus = [{ + name: 'Browse', + items: [{ + name: 'Manage', + target: '_blank', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }], + }]; + assert.deepEqual(element._computeLinks( + /* defaultLinks= */ [], + /* userLinks= */ [], + adminLinks, + topMenus, + /* baseDocUrl= */ '' + ), [{ + title: 'Browse', + links: adminLinks.concat([{ + name: 'Manage', + url: 'https://gerrit/plugins/plugin-manager/static/index.html', + }]), + }]); + }); + + test('register URL', () => { + const config = { + auth: { + auth_type: 'LDAP', + register_url: 'https//gerrit.example.com/register', + }, + }; + element._retrieveRegisterURL(config); + assert.equal(element._registerURL, config.auth.register_url); + assert.equal(element._registerText, 'Sign up'); + + config.auth.register_text = 'Create account'; + element._retrieveRegisterURL(config); + assert.equal(element._registerURL, config.auth.register_url); + assert.equal(element._registerText, config.auth.register_text); + }); + + test('register URL ignored for wrong auth type', () => { + const config = { + auth: { + auth_type: 'OPENID', + register_url: 'https//gerrit.example.com/register', + }, + }; + element._retrieveRegisterURL(config); + assert.equal(element._registerURL, null); + assert.equal(element._registerText, 'Sign up'); + }); +}); +</script>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js index e79277a..c8724c3 100644 --- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.js
@@ -1,750 +1,748 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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. + */ +(function(window) { + 'use strict'; -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 + // Navigation parameters object format: + // + // Each object has a `view` property with a value from Gerrit.Nav.View. The + // remaining properties depend on the value used for view. + // + // - Gerrit.Nav.View.CHANGE: + // - `changeNum`, required, String: the numeric ID of the change. + // - `project`, optional, String: the project name. + // - `patchNum`, optional, Number: the patch for the right-hand-side of + // the diff. + // - `basePatchNum`, optional, Number: the patch for the left-hand-side + // of the diff. If `basePatchNum` is provided, then `patchNum` must + // also be provided. + // - `edit`, optional, Boolean: whether or not to load the file list with + // edit controls. + // - `messageHash`, optional, String: the hash of the change message to + // scroll to. + // + // - Gerrit.Nav.View.SEARCH: + // - `query`, optional, String: the literal search query. If provided, + // the string will be used as the query, and all other params will be + // ignored. + // - `owner`, optional, String: the owner name. + // - `project`, optional, String: the project name. + // - `branch`, optional, String: the branch name. + // - `topic`, optional, String: the topic name. + // - `hashtag`, optional, String: the hashtag name. + // - `statuses`, optional, Array<String>: the list of change statuses to + // search for. If more than one is provided, the search will OR them + // together. + // - `offset`, optional, Number: the offset for the query. + // + // - Gerrit.Nav.View.DIFF: + // - `changeNum`, required, String: the numeric ID of the change. + // - `path`, required, String: the filepath of the diff. + // - `patchNum`, required, Number: the patch for the right-hand-side of + // the diff. + // - `basePatchNum`, optional, Number: the patch for the left-hand-side + // of the diff. If `basePatchNum` is provided, then `patchNum` must + // also be provided. + // - `lineNum`, optional, Number: the line number to be selected on load. + // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value + // of true selects the line from base of the patch range. False by + // default. + // + // - Gerrit.Nav.View.GROUP: + // - `groupId`, required, String: the ID of the group. + // - `detail`, optional, String: the name of the group detail view. + // Takes any value from Gerrit.Nav.GroupDetailView. + // + // - Gerrit.Nav.View.REPO: + // - `repoName`, required, String: the name of the repo + // - `detail`, optional, String: the name of the repo detail view. + // Takes any value from Gerrit.Nav.RepoDetailView. + // + // - Gerrit.Nav.View.DASHBOARD + // - `repo`, optional, String. + // - `sections`, optional, Array of objects with `title` and `query` + // strings. + // - `user`, optional, String. + // + // - Gerrit.Nav.View.ROOT: + // - no possible parameters. -http://www.apache.org/licenses/LICENSE-2.0 + window.Gerrit = window.Gerrit || {}; -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. ---> -<script> - (function(window) { - 'use strict'; + // Prevent redefinition. + if (window.Gerrit.hasOwnProperty('Nav')) { return; } - // Navigation parameters object format: - // - // Each object has a `view` property with a value from Gerrit.Nav.View. The - // remaining properties depend on the value used for view. - // - // - Gerrit.Nav.View.CHANGE: - // - `changeNum`, required, String: the numeric ID of the change. - // - `project`, optional, String: the project name. - // - `patchNum`, optional, Number: the patch for the right-hand-side of - // the diff. - // - `basePatchNum`, optional, Number: the patch for the left-hand-side - // of the diff. If `basePatchNum` is provided, then `patchNum` must - // also be provided. - // - `edit`, optional, Boolean: whether or not to load the file list with - // edit controls. - // - `messageHash`, optional, String: the hash of the change message to - // scroll to. - // - // - Gerrit.Nav.View.SEARCH: - // - `query`, optional, String: the literal search query. If provided, - // the string will be used as the query, and all other params will be - // ignored. - // - `owner`, optional, String: the owner name. - // - `project`, optional, String: the project name. - // - `branch`, optional, String: the branch name. - // - `topic`, optional, String: the topic name. - // - `hashtag`, optional, String: the hashtag name. - // - `statuses`, optional, Array<String>: the list of change statuses to - // search for. If more than one is provided, the search will OR them - // together. - // - `offset`, optional, Number: the offset for the query. - // - // - Gerrit.Nav.View.DIFF: - // - `changeNum`, required, String: the numeric ID of the change. - // - `path`, required, String: the filepath of the diff. - // - `patchNum`, required, Number: the patch for the right-hand-side of - // the diff. - // - `basePatchNum`, optional, Number: the patch for the left-hand-side - // of the diff. If `basePatchNum` is provided, then `patchNum` must - // also be provided. - // - `lineNum`, optional, Number: the line number to be selected on load. - // - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value - // of true selects the line from base of the patch range. False by - // default. - // - // - Gerrit.Nav.View.GROUP: - // - `groupId`, required, String: the ID of the group. - // - `detail`, optional, String: the name of the group detail view. - // Takes any value from Gerrit.Nav.GroupDetailView. - // - // - Gerrit.Nav.View.REPO: - // - `repoName`, required, String: the name of the repo - // - `detail`, optional, String: the name of the repo detail view. - // Takes any value from Gerrit.Nav.RepoDetailView. - // - // - Gerrit.Nav.View.DASHBOARD - // - `repo`, optional, String. - // - `sections`, optional, Array of objects with `title` and `query` - // strings. - // - `user`, optional, String. - // - // - Gerrit.Nav.View.ROOT: - // - no possible parameters. + const uninitialized = () => { + console.warn('Use of uninitialized routing'); + }; - window.Gerrit = window.Gerrit || {}; + const EDIT_PATCHNUM = 'edit'; + const PARENT_PATCHNUM = 'PARENT'; - // Prevent redefinition. - if (window.Gerrit.hasOwnProperty('Nav')) { return; } + const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g; - const uninitialized = () => { - console.warn('Use of uninitialized routing'); - }; + // NOTE: These queries are tested in Java. Any changes made to definitions + // here require corresponding changes to: + // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java + const DEFAULT_SECTIONS = [ + { + // Changes with unpublished draft comments. This section is omitted when + // viewing other users, so we don't need to filter anything out. + name: 'Has draft comments', + query: 'has:draft', + selfOnly: true, + hideIfEmpty: true, + suffixForDashboard: 'limit:10', + }, + { + // Changes that are assigned to the viewed user. + name: 'Assigned reviews', + query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' + + 'is:open -is:ignored', + hideIfEmpty: true, + suffixForDashboard: 'limit:25', + }, + { + // WIP open changes owned by viewing user. This section is omitted when + // viewing other users, so we don't need to filter anything out. + name: 'Work in progress', + query: 'is:open owner:${user} is:wip', + selfOnly: true, + hideIfEmpty: true, + suffixForDashboard: 'limit:25', + }, + { + // Non-WIP open changes owned by viewed user. Filter out changes ignored + // by the viewing user. + name: 'Outgoing reviews', + query: 'is:open owner:${user} -is:wip -is:ignored', + isOutgoing: true, + suffixForDashboard: 'limit:25', + }, + { + // Non-WIP open changes not owned by the viewed user, that the viewed user + // is associated with (as either a reviewer or the assignee). Changes + // ignored by the viewing user are filtered out. + name: 'Incoming reviews', + query: 'is:open -owner:${user} -is:wip -is:ignored ' + + '(reviewer:${user} OR assignee:${user})', + suffixForDashboard: 'limit:25', + }, + { + // Open changes the viewed user is CCed on. Changes ignored by the viewing + // user are filtered out. + name: 'CCed on', + query: 'is:open -is:ignored cc:${user}', + suffixForDashboard: 'limit:10', + }, + { + name: 'Recently closed', + // Closed changes where viewed user is owner, reviewer, or assignee. + // Changes ignored by the viewing user are filtered out, and so are WIP + // changes not owned by the viewing user (the one instance of + // 'owner:self' is intentional and implements this logic). + query: 'is:closed -is:ignored (-is:wip OR owner:self) ' + + '(owner:${user} OR reviewer:${user} OR assignee:${user} ' + + 'OR cc:${user})', + suffixForDashboard: '-age:4w limit:10', + }, + ]; - const EDIT_PATCHNUM = 'edit'; - const PARENT_PATCHNUM = 'PARENT'; + window.Gerrit.Nav = { - const USER_PLACEHOLDER_PATTERN = /\$\{user\}/g; + View: { + ADMIN: 'admin', + AGREEMENTS: 'agreements', + CHANGE: 'change', + DASHBOARD: 'dashboard', + DIFF: 'diff', + DOCUMENTATION_SEARCH: 'documentation-search', + EDIT: 'edit', + GROUP: 'group', + PLUGIN_SCREEN: 'plugin-screen', + REPO: 'repo', + ROOT: 'root', + SEARCH: 'search', + SETTINGS: 'settings', + }, - // NOTE: These queries are tested in Java. Any changes made to definitions - // here require corresponding changes to: - // javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java - const DEFAULT_SECTIONS = [ - { - // Changes with unpublished draft comments. This section is omitted when - // viewing other users, so we don't need to filter anything out. - name: 'Has draft comments', - query: 'has:draft', - selfOnly: true, - hideIfEmpty: true, - suffixForDashboard: 'limit:10', - }, - { - // Changes that are assigned to the viewed user. - name: 'Assigned reviews', - query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' + - 'is:open -is:ignored', - hideIfEmpty: true, - suffixForDashboard: 'limit:25', - }, - { - // WIP open changes owned by viewing user. This section is omitted when - // viewing other users, so we don't need to filter anything out. - name: 'Work in progress', - query: 'is:open owner:${user} is:wip', - selfOnly: true, - hideIfEmpty: true, - suffixForDashboard: 'limit:25', - }, - { - // Non-WIP open changes owned by viewed user. Filter out changes ignored - // by the viewing user. - name: 'Outgoing reviews', - query: 'is:open owner:${user} -is:wip -is:ignored', - isOutgoing: true, - suffixForDashboard: 'limit:25', - }, - { - // Non-WIP open changes not owned by the viewed user, that the viewed user - // is associated with (as either a reviewer or the assignee). Changes - // ignored by the viewing user are filtered out. - name: 'Incoming reviews', - query: 'is:open -owner:${user} -is:wip -is:ignored ' + - '(reviewer:${user} OR assignee:${user})', - suffixForDashboard: 'limit:25', - }, - { - // Open changes the viewed user is CCed on. Changes ignored by the viewing - // user are filtered out. - name: 'CCed on', - query: 'is:open -is:ignored cc:${user}', - suffixForDashboard: 'limit:10', - }, - { - name: 'Recently closed', - // Closed changes where viewed user is owner, reviewer, or assignee. - // Changes ignored by the viewing user are filtered out, and so are WIP - // changes not owned by the viewing user (the one instance of - // 'owner:self' is intentional and implements this logic). - query: 'is:closed -is:ignored (-is:wip OR owner:self) ' + - '(owner:${user} OR reviewer:${user} OR assignee:${user} ' + - 'OR cc:${user})', - suffixForDashboard: '-age:4w limit:10', - }, - ]; + GroupDetailView: { + MEMBERS: 'members', + LOG: 'log', + }, - window.Gerrit.Nav = { + RepoDetailView: { + ACCESS: 'access', + BRANCHES: 'branches', + COMMANDS: 'commands', + DASHBOARDS: 'dashboards', + TAGS: 'tags', + }, - View: { - ADMIN: 'admin', - AGREEMENTS: 'agreements', - CHANGE: 'change', - DASHBOARD: 'dashboard', - DIFF: 'diff', - DOCUMENTATION_SEARCH: 'documentation-search', - EDIT: 'edit', - GROUP: 'group', - PLUGIN_SCREEN: 'plugin-screen', - REPO: 'repo', - ROOT: 'root', - SEARCH: 'search', - SETTINGS: 'settings', - }, + WeblinkType: { + CHANGE: 'change', + FILE: 'file', + PATCHSET: 'patchset', + }, - GroupDetailView: { - MEMBERS: 'members', - LOG: 'log', - }, + /** @type {Function} */ + _navigate: uninitialized, - RepoDetailView: { - ACCESS: 'access', - BRANCHES: 'branches', - COMMANDS: 'commands', - DASHBOARDS: 'dashboards', - TAGS: 'tags', - }, + /** @type {Function} */ + _generateUrl: uninitialized, - WeblinkType: { - CHANGE: 'change', - FILE: 'file', - PATCHSET: 'patchset', - }, + /** @type {Function} */ + _generateWeblinks: uninitialized, - /** @type {Function} */ - _navigate: uninitialized, + /** @type {Function} */ + mapCommentlinks: uninitialized, - /** @type {Function} */ - _generateUrl: uninitialized, + /** + * @param {number=} patchNum + * @param {number|string=} basePatchNum + */ + _checkPatchRange(patchNum, basePatchNum) { + if (basePatchNum && !patchNum) { + throw new Error('Cannot use base patch number without patch number.'); + } + }, - /** @type {Function} */ - _generateWeblinks: uninitialized, + /** + * Setup router implementation. + * + * @param {function(!string)} navigate the router-abstracted equivalent of + * `window.location.href = ...`. Takes a string. + * @param {function(!Object): string} generateUrl generates a URL given + * navigation parameters, detailed in the file header. + * @param {function(!Object): string} generateWeblinks weblinks generator + * function takes single payload parameter with type property that + * determines which + * part of the UI is the consumer of the weblinks. type property can + * be one of file, change, or patchset. + * - For file type, payload will also contain string properties: repo, + * commit, file. + * - For patchset type, payload will also contain string properties: + * repo, commit. + * - For change type, payload will also contain string properties: + * repo, commit. If server provides weblinks, those will be passed + * as options.weblinks property on the main payload object. + * @param {function(!Object): Object} mapCommentlinks provides an escape + * hatch to modify the commentlinks object, e.g. if it contains any + * relative URLs. + */ + setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) { + this._navigate = navigate; + this._generateUrl = generateUrl; + this._generateWeblinks = generateWeblinks; + this.mapCommentlinks = mapCommentlinks; + }, - /** @type {Function} */ - mapCommentlinks: uninitialized, + destroy() { + this._navigate = uninitialized; + this._generateUrl = uninitialized; + this._generateWeblinks = uninitialized; + this.mapCommentlinks = uninitialized; + }, - /** - * @param {number=} patchNum - * @param {number|string=} basePatchNum - */ - _checkPatchRange(patchNum, basePatchNum) { - if (basePatchNum && !patchNum) { - throw new Error('Cannot use base patch number without patch number.'); - } - }, + /** + * Generate a URL for the given route parameters. + * + * @param {Object} params + * @return {string} + */ + _getUrlFor(params) { + return this._generateUrl(params); + }, - /** - * Setup router implementation. - * - * @param {function(!string)} navigate the router-abstracted equivalent of - * `window.location.href = ...`. Takes a string. - * @param {function(!Object): string} generateUrl generates a URL given - * navigation parameters, detailed in the file header. - * @param {function(!Object): string} generateWeblinks weblinks generator - * function takes single payload parameter with type property that - * determines which - * part of the UI is the consumer of the weblinks. type property can - * be one of file, change, or patchset. - * - For file type, payload will also contain string properties: repo, - * commit, file. - * - For patchset type, payload will also contain string properties: - * repo, commit. - * - For change type, payload will also contain string properties: - * repo, commit. If server provides weblinks, those will be passed - * as options.weblinks property on the main payload object. - * @param {function(!Object): Object} mapCommentlinks provides an escape - * hatch to modify the commentlinks object, e.g. if it contains any - * relative URLs. - */ - setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) { - this._navigate = navigate; - this._generateUrl = generateUrl; - this._generateWeblinks = generateWeblinks; - this.mapCommentlinks = mapCommentlinks; - }, + getUrlForSearchQuery(query, opt_offset) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + query, + offset: opt_offset, + }); + }, - destroy() { - this._navigate = uninitialized; - this._generateUrl = uninitialized; - this._generateWeblinks = uninitialized; - this.mapCommentlinks = uninitialized; - }, + /** + * @param {!string} project The name of the project. + * @param {boolean=} opt_openOnly When true, only search open changes in + * the project. + * @param {string=} opt_host The host in which to search. + * @return {string} + */ + getUrlForProjectChanges(project, opt_openOnly, opt_host) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + project, + statuses: opt_openOnly ? ['open'] : [], + host: opt_host, + }); + }, - /** - * Generate a URL for the given route parameters. - * - * @param {Object} params - * @return {string} - */ - _getUrlFor(params) { - return this._generateUrl(params); - }, + /** + * @param {string} branch The name of the branch. + * @param {string} project The name of the project. + * @param {string=} opt_status The status to search. + * @param {string=} opt_host The host in which to search. + * @return {string} + */ + getUrlForBranch(branch, project, opt_status, opt_host) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + branch, + project, + statuses: opt_status ? [opt_status] : undefined, + host: opt_host, + }); + }, - getUrlForSearchQuery(query, opt_offset) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - query, - offset: opt_offset, - }); - }, + /** + * @param {string} topic The name of the topic. + * @param {string=} opt_host The host in which to search. + * @return {string} + */ + getUrlForTopic(topic, opt_host) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + topic, + statuses: ['open', 'merged'], + host: opt_host, + }); + }, - /** - * @param {!string} project The name of the project. - * @param {boolean=} opt_openOnly When true, only search open changes in - * the project. - * @param {string=} opt_host The host in which to search. - * @return {string} - */ - getUrlForProjectChanges(project, opt_openOnly, opt_host) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - project, - statuses: opt_openOnly ? ['open'] : [], - host: opt_host, - }); - }, + /** + * @param {string} hashtag The name of the hashtag. + * @return {string} + */ + getUrlForHashtag(hashtag) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + hashtag, + statuses: ['open', 'merged'], + }); + }, - /** - * @param {string} branch The name of the branch. - * @param {string} project The name of the project. - * @param {string=} opt_status The status to search. - * @param {string=} opt_host The host in which to search. - * @return {string} - */ - getUrlForBranch(branch, project, opt_status, opt_host) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - branch, - project, - statuses: opt_status ? [opt_status] : undefined, - host: opt_host, - }); - }, + /** + * Navigate to a search for changes with the given status. + * + * @param {string} status + */ + navigateToStatusSearch(status) { + this._navigate(this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + statuses: [status], + })); + }, - /** - * @param {string} topic The name of the topic. - * @param {string=} opt_host The host in which to search. - * @return {string} - */ - getUrlForTopic(topic, opt_host) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - topic, - statuses: ['open', 'merged'], - host: opt_host, - }); - }, + /** + * Navigate to a search query + * + * @param {string} query + * @param {number=} opt_offset + */ + navigateToSearchQuery(query, opt_offset) { + return this._navigate(this.getUrlForSearchQuery(query, opt_offset)); + }, - /** - * @param {string} hashtag The name of the hashtag. - * @return {string} - */ - getUrlForHashtag(hashtag) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - hashtag, - statuses: ['open', 'merged'], - }); - }, + /** + * Navigate to the user's dashboard + */ + navigateToUserDashboard() { + return this._navigate(this.getUrlForUserDashboard('self')); + }, - /** - * Navigate to a search for changes with the given status. - * - * @param {string} status - */ - navigateToStatusSearch(status) { - this._navigate(this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - statuses: [status], - })); - }, + /** + * @param {!Object} change The change object. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {boolean=} opt_isEdit + * @param {string=} opt_messageHash + * @return {string} + */ + getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit, + opt_messageHash) { + if (opt_basePatchNum === PARENT_PATCHNUM) { + opt_basePatchNum = undefined; + } - /** - * Navigate to a search query - * - * @param {string} query - * @param {number=} opt_offset - */ - navigateToSearchQuery(query, opt_offset) { - return this._navigate(this.getUrlForSearchQuery(query, opt_offset)); - }, + this._checkPatchRange(opt_patchNum, opt_basePatchNum); + return this._getUrlFor({ + view: Gerrit.Nav.View.CHANGE, + changeNum: change._number, + project: change.project, + patchNum: opt_patchNum, + basePatchNum: opt_basePatchNum, + edit: opt_isEdit, + host: change.internalHost || undefined, + messageHash: opt_messageHash, + }); + }, - /** - * Navigate to the user's dashboard - */ - navigateToUserDashboard() { - return this._navigate(this.getUrlForUserDashboard('self')); - }, + /** + * @param {number} changeNum + * @param {string} project The name of the project. + * @param {number=} opt_patchNum + * @return {string} + */ + getUrlForChangeById(changeNum, project, opt_patchNum) { + return this._getUrlFor({ + view: Gerrit.Nav.View.CHANGE, + changeNum, + project, + patchNum: opt_patchNum, + }); + }, - /** - * @param {!Object} change The change object. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {boolean=} opt_isEdit - * @param {string=} opt_messageHash - * @return {string} - */ - getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit, - opt_messageHash) { - if (opt_basePatchNum === PARENT_PATCHNUM) { - opt_basePatchNum = undefined; - } + /** + * @param {!Object} change The change object. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {boolean=} opt_isEdit + */ + navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) { + this._navigate(this.getUrlForChange(change, opt_patchNum, + opt_basePatchNum, opt_isEdit)); + }, - this._checkPatchRange(opt_patchNum, opt_basePatchNum); - return this._getUrlFor({ - view: Gerrit.Nav.View.CHANGE, - changeNum: change._number, - project: change.project, - patchNum: opt_patchNum, - basePatchNum: opt_basePatchNum, - edit: opt_isEdit, - host: change.internalHost || undefined, - messageHash: opt_messageHash, - }); - }, + /** + * @param {{ _number: number, project: string }} change The change object. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {number|string=} opt_lineNum + * @return {string} + */ + getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) { + return this.getUrlForDiffById(change._number, change.project, path, + opt_patchNum, opt_basePatchNum, opt_lineNum); + }, - /** - * @param {number} changeNum - * @param {string} project The name of the project. - * @param {number=} opt_patchNum - * @return {string} - */ - getUrlForChangeById(changeNum, project, opt_patchNum) { - return this._getUrlFor({ - view: Gerrit.Nav.View.CHANGE, - changeNum, - project, - patchNum: opt_patchNum, - }); - }, + /** + * @param {number} changeNum + * @param {string} project The name of the project. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + * @param {number=} opt_lineNum + * @param {boolean=} opt_leftSide + * @return {string} + */ + getUrlForDiffById(changeNum, project, path, opt_patchNum, + opt_basePatchNum, opt_lineNum, opt_leftSide) { + if (opt_basePatchNum === PARENT_PATCHNUM) { + opt_basePatchNum = undefined; + } - /** - * @param {!Object} change The change object. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {boolean=} opt_isEdit - */ - navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) { - this._navigate(this.getUrlForChange(change, opt_patchNum, - opt_basePatchNum, opt_isEdit)); - }, + this._checkPatchRange(opt_patchNum, opt_basePatchNum); + return this._getUrlFor({ + view: Gerrit.Nav.View.DIFF, + changeNum, + project, + path, + patchNum: opt_patchNum, + basePatchNum: opt_basePatchNum, + lineNum: opt_lineNum, + leftSide: opt_leftSide, + }); + }, - /** - * @param {{ _number: number, project: string }} change The change object. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {number|string=} opt_lineNum - * @return {string} - */ - getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) { - return this.getUrlForDiffById(change._number, change.project, path, - opt_patchNum, opt_basePatchNum, opt_lineNum); - }, + /** + * @param {{ _number: number, project: string }} change The change object. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @return {string} + */ + getEditUrlForDiff(change, path, opt_patchNum) { + return this.getEditUrlForDiffById(change._number, change.project, path, + opt_patchNum); + }, - /** - * @param {number} changeNum - * @param {string} project The name of the project. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - * @param {number=} opt_lineNum - * @param {boolean=} opt_leftSide - * @return {string} - */ - getUrlForDiffById(changeNum, project, path, opt_patchNum, - opt_basePatchNum, opt_lineNum, opt_leftSide) { - if (opt_basePatchNum === PARENT_PATCHNUM) { - opt_basePatchNum = undefined; - } + /** + * @param {number} changeNum + * @param {string} project The name of the project. + * @param {string} path The file path. + * @param {number|string=} opt_patchNum The patchNum the file content + * should be based on, or ${EDIT_PATCHNUM} if left undefined. + * @return {string} + */ + getEditUrlForDiffById(changeNum, project, path, opt_patchNum) { + return this._getUrlFor({ + view: Gerrit.Nav.View.EDIT, + changeNum, + project, + path, + patchNum: opt_patchNum || EDIT_PATCHNUM, + }); + }, - this._checkPatchRange(opt_patchNum, opt_basePatchNum); - return this._getUrlFor({ - view: Gerrit.Nav.View.DIFF, - changeNum, - project, - path, - patchNum: opt_patchNum, - basePatchNum: opt_basePatchNum, - lineNum: opt_lineNum, - leftSide: opt_leftSide, - }); - }, + /** + * @param {!Object} change The change object. + * @param {string} path The file path. + * @param {number=} opt_patchNum + * @param {number|string=} opt_basePatchNum The string 'PARENT' can be + * used for none. + */ + navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) { + this._navigate(this.getUrlForDiff(change, path, opt_patchNum, + opt_basePatchNum)); + }, - /** - * @param {{ _number: number, project: string }} change The change object. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @return {string} - */ - getEditUrlForDiff(change, path, opt_patchNum) { - return this.getEditUrlForDiffById(change._number, change.project, path, - opt_patchNum); - }, + /** + * @param {string} owner The name of the owner. + * @return {string} + */ + getUrlForOwner(owner) { + return this._getUrlFor({ + view: Gerrit.Nav.View.SEARCH, + owner, + }); + }, - /** - * @param {number} changeNum - * @param {string} project The name of the project. - * @param {string} path The file path. - * @param {number|string=} opt_patchNum The patchNum the file content - * should be based on, or ${EDIT_PATCHNUM} if left undefined. - * @return {string} - */ - getEditUrlForDiffById(changeNum, project, path, opt_patchNum) { - return this._getUrlFor({ - view: Gerrit.Nav.View.EDIT, - changeNum, - project, - path, - patchNum: opt_patchNum || EDIT_PATCHNUM, - }); - }, + /** + * @param {string} user The name of the user. + * @return {string} + */ + getUrlForUserDashboard(user) { + return this._getUrlFor({ + view: Gerrit.Nav.View.DASHBOARD, + user, + }); + }, - /** - * @param {!Object} change The change object. - * @param {string} path The file path. - * @param {number=} opt_patchNum - * @param {number|string=} opt_basePatchNum The string 'PARENT' can be - * used for none. - */ - navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) { - this._navigate(this.getUrlForDiff(change, path, opt_patchNum, - opt_basePatchNum)); - }, + /** + * @return {string} + */ + getUrlForRoot() { + return this._getUrlFor({ + view: Gerrit.Nav.View.ROOT, + }); + }, - /** - * @param {string} owner The name of the owner. - * @return {string} - */ - getUrlForOwner(owner) { - return this._getUrlFor({ - view: Gerrit.Nav.View.SEARCH, - owner, - }); - }, + /** + * @param {string} repo The name of the repo. + * @param {string} dashboard The ID of the dashboard, in the form of + * '<ref>:<path>'. + * @return {string} + */ + getUrlForRepoDashboard(repo, dashboard) { + return this._getUrlFor({ + view: Gerrit.Nav.View.DASHBOARD, + repo, + dashboard, + }); + }, - /** - * @param {string} user The name of the user. - * @return {string} - */ - getUrlForUserDashboard(user) { - return this._getUrlFor({ - view: Gerrit.Nav.View.DASHBOARD, - user, - }); - }, + /** + * Navigate to an arbitrary relative URL. + * + * @param {string} relativeUrl + */ + navigateToRelativeUrl(relativeUrl) { + if (!relativeUrl.startsWith('/')) { + throw new Error('navigateToRelativeUrl with non-relative URL'); + } + this._navigate(relativeUrl); + }, - /** - * @return {string} - */ - getUrlForRoot() { - return this._getUrlFor({ - view: Gerrit.Nav.View.ROOT, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepo(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + }); + }, - /** - * @param {string} repo The name of the repo. - * @param {string} dashboard The ID of the dashboard, in the form of - * '<ref>:<path>'. - * @return {string} - */ - getUrlForRepoDashboard(repo, dashboard) { - return this._getUrlFor({ - view: Gerrit.Nav.View.DASHBOARD, - repo, - dashboard, - }); - }, + /** + * Navigate to a repo settings page. + * + * @param {string} repoName + */ + navigateToRepo(repoName) { + this._navigate(this.getUrlForRepo(repoName)); + }, - /** - * Navigate to an arbitrary relative URL. - * - * @param {string} relativeUrl - */ - navigateToRelativeUrl(relativeUrl) { - if (!relativeUrl.startsWith('/')) { - throw new Error('navigateToRelativeUrl with non-relative URL'); - } - this._navigate(relativeUrl); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoTags(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.TAGS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepo(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoBranches(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + }); + }, - /** - * Navigate to a repo settings page. - * - * @param {string} repoName - */ - navigateToRepo(repoName) { - this._navigate(this.getUrlForRepo(repoName)); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoAccess(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoTags(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.TAGS, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoCommands(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.COMMANDS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoBranches(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - }); - }, + /** + * @param {string} repoName + * @return {string} + */ + getUrlForRepoDashboards(repoName) { + return this._getUrlFor({ + view: Gerrit.Nav.View.REPO, + repoName, + detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoAccess(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - }); - }, + /** + * @param {string} groupId + * @return {string} + */ + getUrlForGroup(groupId) { + return this._getUrlFor({ + view: Gerrit.Nav.View.GROUP, + groupId, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoCommands(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.COMMANDS, - }); - }, + /** + * @param {string} groupId + * @return {string} + */ + getUrlForGroupLog(groupId) { + return this._getUrlFor({ + view: Gerrit.Nav.View.GROUP, + groupId, + detail: Gerrit.Nav.GroupDetailView.LOG, + }); + }, - /** - * @param {string} repoName - * @return {string} - */ - getUrlForRepoDashboards(repoName) { - return this._getUrlFor({ - view: Gerrit.Nav.View.REPO, - repoName, - detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, - }); - }, + /** + * @param {string} groupId + * @return {string} + */ + getUrlForGroupMembers(groupId) { + return this._getUrlFor({ + view: Gerrit.Nav.View.GROUP, + groupId, + detail: Gerrit.Nav.GroupDetailView.MEMBERS, + }); + }, - /** - * @param {string} groupId - * @return {string} - */ - getUrlForGroup(groupId) { - return this._getUrlFor({ - view: Gerrit.Nav.View.GROUP, - groupId, - }); - }, + getUrlForSettings() { + return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS}); + }, - /** - * @param {string} groupId - * @return {string} - */ - getUrlForGroupLog(groupId) { - return this._getUrlFor({ - view: Gerrit.Nav.View.GROUP, - groupId, - detail: Gerrit.Nav.GroupDetailView.LOG, - }); - }, + /** + * @param {string} repo + * @param {string} commit + * @param {string} file + * @param {Object=} opt_options + * @return { + * Array<{label: string, url: string}>| + * {label: string, url: string} + * } + */ + getFileWebLinks(repo, commit, file, opt_options) { + const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file}; + if (opt_options) { + params.options = opt_options; + } + return [].concat(this._generateWeblinks(params)); + }, - /** - * @param {string} groupId - * @return {string} - */ - getUrlForGroupMembers(groupId) { - return this._getUrlFor({ - view: Gerrit.Nav.View.GROUP, - groupId, - detail: Gerrit.Nav.GroupDetailView.MEMBERS, - }); - }, + /** + * @param {string} repo + * @param {string} commit + * @param {Object=} opt_options + * @return {{label: string, url: string}} + */ + getPatchSetWeblink(repo, commit, opt_options) { + const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit}; + if (opt_options) { + params.options = opt_options; + } + const result = this._generateWeblinks(params); + if (Array.isArray(result)) { + return result.pop(); + } else { + return result; + } + }, - getUrlForSettings() { - return this._getUrlFor({view: Gerrit.Nav.View.SETTINGS}); - }, + /** + * @param {string} repo + * @param {string} commit + * @param {Object=} opt_options + * @return { + * Array<{label: string, url: string}>| + * {label: string, url: string} + * } + */ + getChangeWeblinks(repo, commit, opt_options) { + const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit}; + if (opt_options) { + params.options = opt_options; + } + return [].concat(this._generateWeblinks(params)); + }, - /** - * @param {string} repo - * @param {string} commit - * @param {string} file - * @param {Object=} opt_options - * @return { - * Array<{label: string, url: string}>| - * {label: string, url: string} - * } - */ - getFileWebLinks(repo, commit, file, opt_options) { - const params = {type: Gerrit.Nav.WeblinkType.FILE, repo, commit, file}; - if (opt_options) { - params.options = opt_options; - } - return [].concat(this._generateWeblinks(params)); - }, - - /** - * @param {string} repo - * @param {string} commit - * @param {Object=} opt_options - * @return {{label: string, url: string}} - */ - getPatchSetWeblink(repo, commit, opt_options) { - const params = {type: Gerrit.Nav.WeblinkType.PATCHSET, repo, commit}; - if (opt_options) { - params.options = opt_options; - } - const result = this._generateWeblinks(params); - if (Array.isArray(result)) { - return result.pop(); - } else { - return result; - } - }, - - /** - * @param {string} repo - * @param {string} commit - * @param {Object=} opt_options - * @return { - * Array<{label: string, url: string}>| - * {label: string, url: string} - * } - */ - getChangeWeblinks(repo, commit, opt_options) { - const params = {type: Gerrit.Nav.WeblinkType.CHANGE, repo, commit}; - if (opt_options) { - params.options = opt_options; - } - return [].concat(this._generateWeblinks(params)); - }, - - getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS, - title = '') { - sections = sections - .filter(section => (user === 'self' || !section.selfOnly)) - .map(section => Object.assign({}, section, { - name: section.name, - query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), - })); - return {title, sections}; - }, - }; - })(window); -</script> + getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS, + title = '') { + sections = sections + .filter(section => (user === 'self' || !section.selfOnly)) + .map(section => Object.assign({}, section, { + name: section.name, + query: section.query.replace(USER_PLACEHOLDER_PATTERN, user), + })); + return {title, sections}; + }, + }; +})(window);
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html index f58780c..8f3c623 100644 --- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html +++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation_test.html
@@ -19,70 +19,71 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-navigation</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<script> - suite('gr-navigation tests', async () => { - await readyToTest(); - test('invalid patch ranges throw exceptions', () => { - assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12)); - assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12)); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +suite('gr-navigation tests', () => { + test('invalid patch ranges throw exceptions', () => { + assert.throw(() => Gerrit.Nav.getUrlForChange('123', undefined, 12)); + assert.throw(() => Gerrit.Nav.getUrlForDiff('123', 'x.c', undefined, 12)); + }); + + suite('_getUserDashboard', () => { + const sections = [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2 for ${user}'}, + {name: 'section 3', query: 'self only query', selfOnly: true}, + {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'}, + ]; + + test('dashboard for self', () => { + const dashboard = + Gerrit.Nav.getUserDashboard('self', sections, 'title'); + assert.deepEqual( + dashboard, + { + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2 for self'}, + { + name: 'section 3', + query: 'self only query', + selfOnly: true, + }, { + name: 'section 4', + query: 'query 4', + suffixForDashboard: 'suffix', + }, + ], + }); }); - suite('_getUserDashboard', () => { - const sections = [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2 for ${user}'}, - {name: 'section 3', query: 'self only query', selfOnly: true}, - {name: 'section 4', query: 'query 4', suffixForDashboard: 'suffix'}, - ]; - - test('dashboard for self', () => { - const dashboard = - Gerrit.Nav.getUserDashboard('self', sections, 'title'); - assert.deepEqual( - dashboard, - { - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2 for self'}, - { - name: 'section 3', - query: 'self only query', - selfOnly: true, - }, { - name: 'section 4', - query: 'query 4', - suffixForDashboard: 'suffix', - }, - ], - }); - }); - - test('dashboard for other user', () => { - const dashboard = - Gerrit.Nav.getUserDashboard('user', sections, 'title'); - assert.deepEqual( - dashboard, - { - title: 'title', - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2 for user'}, - { - name: 'section 4', - query: 'query 4', - suffixForDashboard: 'suffix', - }, - ], - }); - }); + test('dashboard for other user', () => { + const dashboard = + Gerrit.Nav.getUserDashboard('user', sections, 'title'); + assert.deepEqual( + dashboard, + { + title: 'title', + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2 for user'}, + { + name: 'section 4', + query: 'query 4', + suffixForDashboard: 'suffix', + }, + ], + }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html deleted file mode 100644 index 0ba8a22..0000000 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.html +++ /dev/null
@@ -1,22 +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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-reporting"> - <script src="gr-reporting.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js index 106508c..55f8abd 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -14,566 +14,566 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Latency reporting constants. - const TIMING = { - TYPE: 'timing-report', - CATEGORY_UI_LATENCY: 'UI Latency', - CATEGORY_RPC: 'RPC Timing', - // Reported events - alphabetize below. - APP_STARTED: 'App Started', - }; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; - // Plugin-related reporting constants. - const PLUGINS = { - TYPE: 'lifecycle', - // Reported events - alphabetize below. - INSTALLED: 'Plugins installed', - }; +// Latency reporting constants. +const TIMING = { + TYPE: 'timing-report', + CATEGORY_UI_LATENCY: 'UI Latency', + CATEGORY_RPC: 'RPC Timing', + // Reported events - alphabetize below. + APP_STARTED: 'App Started', +}; - // Chrome extension-related reporting constants. - const EXTENSION = { - TYPE: 'lifecycle', - // Reported events - alphabetize below. - DETECTED: 'Extension detected', - }; +// Plugin-related reporting constants. +const PLUGINS = { + TYPE: 'lifecycle', + // Reported events - alphabetize below. + INSTALLED: 'Plugins installed', +}; - // Navigation reporting constants. - const NAVIGATION = { - TYPE: 'nav-report', - CATEGORY: 'Location Changed', - PAGE: 'Page', - }; +// Chrome extension-related reporting constants. +const EXTENSION = { + TYPE: 'lifecycle', + // Reported events - alphabetize below. + DETECTED: 'Extension detected', +}; - const ERROR = { - TYPE: 'error', - CATEGORY: 'exception', - }; +// Navigation reporting constants. +const NAVIGATION = { + TYPE: 'nav-report', + CATEGORY: 'Location Changed', + PAGE: 'Page', +}; - const ERROR_DIALOG = { - TYPE: 'error', - CATEGORY: 'Error Dialog', - }; +const ERROR = { + TYPE: 'error', + CATEGORY: 'exception', +}; - const TIMER = { - CHANGE_DISPLAYED: 'ChangeDisplayed', - CHANGE_LOAD_FULL: 'ChangeFullyLoaded', - DASHBOARD_DISPLAYED: 'DashboardDisplayed', - DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent', - DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed', - DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded', - FILE_LIST_DISPLAYED: 'FileListDisplayed', - PLUGINS_LOADED: 'PluginsLoaded', - STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed', - STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded', - STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed', - STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent', - STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed', - STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded', - STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed', - WEB_COMPONENTS_READY: 'WebComponentsReady', - METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded', - }; +const ERROR_DIALOG = { + TYPE: 'error', + CATEGORY: 'Error Dialog', +}; - const STARTUP_TIMERS = {}; - STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0; - STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0; - STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0; - STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0; - STARTUP_TIMERS[TIMING.APP_STARTED] = 0; - // WebComponentsReady timer is triggered from gr-router. - STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0; +const TIMER = { + CHANGE_DISPLAYED: 'ChangeDisplayed', + CHANGE_LOAD_FULL: 'ChangeFullyLoaded', + DASHBOARD_DISPLAYED: 'DashboardDisplayed', + DIFF_VIEW_CONTENT_DISPLAYED: 'DiffViewOnlyContent', + DIFF_VIEW_DISPLAYED: 'DiffViewDisplayed', + DIFF_VIEW_LOAD_FULL: 'DiffViewFullyLoaded', + FILE_LIST_DISPLAYED: 'FileListDisplayed', + PLUGINS_LOADED: 'PluginsLoaded', + STARTUP_CHANGE_DISPLAYED: 'StartupChangeDisplayed', + STARTUP_CHANGE_LOAD_FULL: 'StartupChangeFullyLoaded', + STARTUP_DASHBOARD_DISPLAYED: 'StartupDashboardDisplayed', + STARTUP_DIFF_VIEW_CONTENT_DISPLAYED: 'StartupDiffViewOnlyContent', + STARTUP_DIFF_VIEW_DISPLAYED: 'StartupDiffViewDisplayed', + STARTUP_DIFF_VIEW_LOAD_FULL: 'StartupDiffViewFullyLoaded', + STARTUP_FILE_LIST_DISPLAYED: 'StartupFileListDisplayed', + WEB_COMPONENTS_READY: 'WebComponentsReady', + METRICS_PLUGIN_LOADED: 'MetricsPluginLoaded', +}; - const INTERACTION_TYPE = 'interaction'; +const STARTUP_TIMERS = {}; +STARTUP_TIMERS[TIMER.PLUGINS_LOADED] = 0; +STARTUP_TIMERS[TIMER.METRICS_PLUGIN_LOADED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_CHANGE_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_CHANGE_LOAD_FULL] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DASHBOARD_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_DISPLAYED] = 0; +STARTUP_TIMERS[TIMER.STARTUP_DIFF_VIEW_LOAD_FULL] = 0; +STARTUP_TIMERS[TIMER.STARTUP_FILE_LIST_DISPLAYED] = 0; +STARTUP_TIMERS[TIMING.APP_STARTED] = 0; +// WebComponentsReady timer is triggered from gr-router. +STARTUP_TIMERS[TIMER.WEB_COMPONENTS_READY] = 0; - const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions'; - const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes. +const INTERACTION_TYPE = 'interaction'; - let pending = []; - let slowRpcList = []; - const SLOW_RPC_THRESHOLD = 500; +const DRAFT_ACTION_TIMER = 'TimeBetweenDraftActions'; +const DRAFT_ACTION_TIMER_MAX = 2 * 60 * 1000; // 2 minutes. - // Variables that hold context info in global scope - let reportRepoName = undefined; +let pending = []; +let slowRpcList = []; +const SLOW_RPC_THRESHOLD = 500; - const onError = function(oldOnError, msg, url, line, column, error) { - if (oldOnError) { - oldOnError(msg, url, line, column, error); +// Variables that hold context info in global scope +let reportRepoName = undefined; + +const onError = function(oldOnError, msg, url, line, column, error) { + if (oldOnError) { + oldOnError(msg, url, line, column, error); + } + if (error) { + line = line || error.lineNumber; + column = column || error.columnNumber; + let shortenedErrorStack = msg; + if (error.stack) { + const errorStackLines = error.stack.split('\n'); + shortenedErrorStack = errorStackLines.slice(0, + Math.min(3, errorStackLines.length)).join('\n'); } - if (error) { - line = line || error.lineNumber; - column = column || error.columnNumber; - let shortenedErrorStack = msg; - if (error.stack) { - const errorStackLines = error.stack.split('\n'); - shortenedErrorStack = errorStackLines.slice(0, - Math.min(3, errorStackLines.length)).join('\n'); - } - msg = shortenedErrorStack || error.toString(); - } + msg = shortenedErrorStack || error.toString(); + } + const payload = { + url, + line, + column, + error, + }; + GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); + return true; +}; + +const catchErrors = function(opt_context) { + const context = opt_context || window; + context.onerror = onError.bind(null, context.onerror); + context.addEventListener('unhandledrejection', e => { + const msg = e.reason.message; const payload = { - url, - line, - column, - error, + error: e.reason, }; GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); - return true; - }; + }); +}; +catchErrors(); - const catchErrors = function(opt_context) { - const context = opt_context || window; - context.onerror = onError.bind(null, context.onerror); - context.addEventListener('unhandledrejection', e => { - const msg = e.reason.message; - const payload = { - error: e.reason, - }; - GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload); +// PerformanceObserver interface is a browser API. +if (window.PerformanceObserver) { + const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || []; + // Safari doesn't support longtask yet + if (supportedEntryTypes.includes('longtask')) { + const catchLongJsTasks = new PerformanceObserver(list => { + for (const task of list.getEntries()) { + // We are interested in longtask longer than 200 ms (default is 50 ms) + if (task.duration > 200) { + GrReporting.prototype.reporter(TIMING.TYPE, + TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`, + Math.round(task.duration), {}, false); + } + } }); - }; - catchErrors(); - - // PerformanceObserver interface is a browser API. - if (window.PerformanceObserver) { - const supportedEntryTypes = PerformanceObserver.supportedEntryTypes || []; - // Safari doesn't support longtask yet - if (supportedEntryTypes.includes('longtask')) { - const catchLongJsTasks = new PerformanceObserver(list => { - for (const task of list.getEntries()) { - // We are interested in longtask longer than 200 ms (default is 50 ms) - if (task.duration > 200) { - GrReporting.prototype.reporter(TIMING.TYPE, - TIMING.CATEGORY_UI_LATENCY, `Task ${task.name}`, - Math.round(task.duration), {}, false); - } - } - }); - catchLongJsTasks.observe({entryTypes: ['longtask']}); - } + catchLongJsTasks.observe({entryTypes: ['longtask']}); } +} - document.addEventListener('visibilitychange', () => { - const eventName = `Visibility changed to ${document.visibilityState}`; - GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName, - undefined, {}, true); - }); +document.addEventListener('visibilitychange', () => { + const eventName = `Visibility changed to ${document.visibilityState}`; + GrReporting.prototype.reporter(INTERACTION_TYPE, undefined, eventName, + undefined, {}, true); +}); - // The Polymer pass of JSCompiler requires this to be reassignable - // eslint-disable-next-line prefer-const - let GrReporting = Polymer({ - is: 'gr-reporting', +// The Polymer pass of JSCompiler requires this to be reassignable +// eslint-disable-next-line prefer-const +let GrReporting = Polymer({ + is: 'gr-reporting', - properties: { - category: String, + properties: { + category: String, - _baselines: { - type: Object, - value: STARTUP_TIMERS, // Shared across all instances. + _baselines: { + type: Object, + value: STARTUP_TIMERS, // Shared across all instances. + }, + + _timers: { + type: Object, + value: {timeBetweenDraftActions: null}, // Shared across all instances. + }, + }, + + get performanceTiming() { + return window.performance.timing; + }, + + get slowRpcSnapshot() { + return slowRpcList.slice(); + }, + + now() { + return Math.round(window.performance.now()); + }, + + _arePluginsLoaded() { + return this._baselines && + !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED); + }, + + _isMetricsPluginLoaded() { + return this._arePluginsLoaded() || this._baselines && + !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED); + }, + + /** + * Reporter reports events. Events will be queued if metrics plugin is not + * yet installed. + * + * @param {string} type + * @param {string} category + * @param {string} eventName + * @param {string|number} eventValue + * @param {Object} eventDetails + * @param {boolean|undefined} opt_noLog If true, the event will not be + * logged to the JS console. + */ + reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) { + const eventInfo = this._createEventInfo(type, category, + eventName, eventValue, eventDetails); + if (type === ERROR.TYPE && category === ERROR.CATEGORY) { + console.error(eventValue && eventValue.error || eventName); + } + + // We report events immediately when metrics plugin is loaded + if (this._isMetricsPluginLoaded() && !pending.length) { + this._reportEvent(eventInfo, opt_noLog); + } else { + // We cache until metrics plugin is loaded + pending.push([eventInfo, opt_noLog]); + if (this._isMetricsPluginLoaded()) { + pending.forEach(([eventInfo, opt_noLog]) => { + this._reportEvent(eventInfo, opt_noLog); + }); + pending = []; + } + } + }, + + _reportEvent(eventInfo, opt_noLog) { + const {type, value, name} = eventInfo; + document.dispatchEvent(new CustomEvent(type, {detail: eventInfo})); + if (opt_noLog) { return; } + if (type !== ERROR.TYPE) { + if (value !== undefined) { + console.log(`Reporting: ${name}: ${value}`); + } else { + console.log(`Reporting: ${name}`); + } + } + }, + + _createEventInfo(type, category, name, value, eventDetails) { + const eventInfo = { + type, + category, + name, + value, + eventStart: this.now(), + }; + + if (typeof(eventDetails) === 'object' && + Object.entries(eventDetails).length !== 0) { + eventInfo.eventDetails = JSON.stringify(eventDetails); + } + if (reportRepoName) { + eventInfo.repoName = reportRepoName; + } + const isInBackgroundTab = document.visibilityState === 'hidden'; + if (isInBackgroundTab !== undefined) { + eventInfo.inBackgroundTab = isInBackgroundTab; + } + + return eventInfo; + }, + + /** + * User-perceived app start time, should be reported when the app is ready. + */ + appStarted() { + this.timeEnd(TIMING.APP_STARTED); + this.pageLoaded(); + }, + + /** + * Page load time and other metrics, should be reported at any time + * after navigation. + */ + pageLoaded() { + if (this.performanceTiming.loadEventEnd === 0) { + console.error('pageLoaded should be called after window.onload'); + this.async(this.pageLoaded, 100); + } else { + const perfEvents = Object.keys(this.performanceTiming.toJSON()); + perfEvents.forEach( + eventName => this._reportPerformanceTiming(eventName) + ); + } + }, + + _reportPerformanceTiming(eventName, eventDetails) { + const eventTiming = this.performanceTiming[eventName]; + if (eventTiming > 0) { + const elapsedTime = eventTiming - + this.performanceTiming.navigationStart; + // NavResTime - Navigation and resource timings. + this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, + `NavResTime - ${eventName}`, elapsedTime, eventDetails, true); + } + }, + + beforeLocationChanged() { + for (const prop of Object.keys(this._baselines)) { + delete this._baselines[prop]; + } + this.time(TIMER.CHANGE_DISPLAYED); + this.time(TIMER.CHANGE_LOAD_FULL); + this.time(TIMER.DASHBOARD_DISPLAYED); + this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); + this.time(TIMER.DIFF_VIEW_DISPLAYED); + this.time(TIMER.DIFF_VIEW_LOAD_FULL); + this.time(TIMER.FILE_LIST_DISPLAYED); + reportRepoName = undefined; + // reset slow rpc list since here start page loads which report these rpcs + slowRpcList = []; + }, + + locationChanged(page) { + this.reporter( + NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page); + }, + + dashboardDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } else { + this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } + }, + + changeDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } else { + this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } + }, + + changeFullyLoaded() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) { + this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL); + } else { + this.timeEnd(TIMER.CHANGE_LOAD_FULL); + } + }, + + diffViewDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } else { + this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList: + this.slowRpcSnapshot}); + } + }, + + diffViewFullyLoaded() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) { + this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL); + } else { + this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL); + } + }, + + diffViewContentDisplayed() { + if (this._baselines.hasOwnProperty( + TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED); + } else { + this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); + } + }, + + fileListDisplayed() { + if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) { + this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED); + } else { + this.timeEnd(TIMER.FILE_LIST_DISPLAYED); + } + }, + + reportExtension(name) { + this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name); + }, + + pluginLoaded(name) { + if (name.startsWith('metrics-')) { + this.timeEnd(TIMER.METRICS_PLUGIN_LOADED); + } + }, + + pluginsLoaded(pluginsList) { + this.timeEnd(TIMER.PLUGINS_LOADED); + this.reporter( + PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined, + {pluginsList: pluginsList || []}, true); + }, + + /** + * Reset named timer. + */ + time(name) { + this._baselines[name] = this.now(); + window.performance.mark(`${name}-start`); + }, + + /** + * Finish named timer and report it to server. + */ + timeEnd(name, eventDetails) { + if (!this._baselines.hasOwnProperty(name)) { return; } + const baseTime = this._baselines[name]; + delete this._baselines[name]; + this._reportTiming(name, this.now() - baseTime, eventDetails); + + // Finalize the interval. Either from a registered start mark or + // the navigation start time (if baseTime is 0). + if (baseTime !== 0) { + window.performance.measure(name, `${name}-start`); + } else { + // Microsft Edge does not handle the 2nd param correctly + // (if undefined). + window.performance.measure(name); + } + }, + + /** + * Reports just line timeEnd, but additionally reports an average given a + * denominator and a separate reporiting name for the average. + * + * @param {string} name Timing name. + * @param {string} averageName Average timing name. + * @param {number} denominator Number by which to divide the total to + * compute the average. + */ + timeEndWithAverage(name, averageName, denominator) { + if (!this._baselines.hasOwnProperty(name)) { return; } + const baseTime = this._baselines[name]; + this.timeEnd(name); + + // Guard against division by zero. + if (!denominator) { return; } + const time = this.now() - baseTime; + this._reportTiming(averageName, time / denominator); + }, + + /** + * Send a timing report with an arbitrary time value. + * + * @param {string} name Timing name. + * @param {number} time The time to report as an integer of milliseconds. + * @param {Object} eventDetails non sensitive details + */ + _reportTiming(name, time, eventDetails) { + this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time, + eventDetails); + }, + + /** + * Get a timer object to for reporing a user timing. The start time will be + * the time that the object has been created, and the end time will be the + * time that the "end" method is called on the object. + * + * @param {string} name Timing name. + * @returns {!Object} The timer object. + */ + getTimer(name) { + let called = false; + let start; + let max = null; + + const timer = { + + // Clear the timer and reset the start time. + reset: () => { + called = false; + start = this.now(); + return timer; }, - _timers: { - type: Object, - value: {timeBetweenDraftActions: null}, // Shared across all instances. + // Stop the timer and report the intervening time. + end: () => { + if (called) { + throw new Error(`Timer for "${name}" already ended.`); + } + called = true; + const time = this.now() - start; + + // If a maximum is specified and the time exceeds it, do not report. + if (max && time > max) { return timer; } + + this._reportTiming(name, time); + return timer; }, - }, - get performanceTiming() { - return window.performance.timing; - }, + // Set a maximum reportable time. If a maximum is set and the timer is + // ended after the specified amount of time, the value is not reported. + withMaximum(maximum) { + max = maximum; + return timer; + }, + }; - get slowRpcSnapshot() { - return slowRpcList.slice(); - }, + // The timer is initialized to its creation time. + return timer.reset(); + }, - now() { - return Math.round(window.performance.now()); - }, + /** + * Log timing information for an RPC. + * + * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated. + * @param {number} elapsed The time elapsed of the RPC. + */ + reportRpcTiming(anonymizedUrl, elapsed) { + this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl, + elapsed, {}, true); + if (elapsed >= SLOW_RPC_THRESHOLD) { + slowRpcList.push({anonymizedUrl, elapsed}); + } + }, - _arePluginsLoaded() { - return this._baselines && - !this._baselines.hasOwnProperty(TIMER.PLUGINS_LOADED); - }, + reportInteraction(eventName, details) { + this.reporter(INTERACTION_TYPE, this.category, eventName, undefined, + details, true); + }, - _isMetricsPluginLoaded() { - return this._arePluginsLoaded() || this._baselines && - !this._baselines.hasOwnProperty(TIMER.METRICS_PLUGIN_LOADED); - }, + /** + * A draft interaction was started. Update the time-betweeen-draft-actions + * timer. + */ + recordDraftInteraction() { + // If there is no timer defined, then this is the first interaction. + // Set up the timer so that it's ready to record the intervening time when + // called again. + const timer = this._timers.timeBetweenDraftActions; + if (!timer) { + // Create a timer with a maximum length. + this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER) + .withMaximum(DRAFT_ACTION_TIMER_MAX); + return; + } - /** - * Reporter reports events. Events will be queued if metrics plugin is not - * yet installed. - * - * @param {string} type - * @param {string} category - * @param {string} eventName - * @param {string|number} eventValue - * @param {Object} eventDetails - * @param {boolean|undefined} opt_noLog If true, the event will not be - * logged to the JS console. - */ - reporter(type, category, eventName, eventValue, eventDetails, opt_noLog) { - const eventInfo = this._createEventInfo(type, category, - eventName, eventValue, eventDetails); - if (type === ERROR.TYPE && category === ERROR.CATEGORY) { - console.error(eventValue && eventValue.error || eventName); - } + // Mark the time and reinitialize the timer. + timer.end().reset(); + }, - // We report events immediately when metrics plugin is loaded - if (this._isMetricsPluginLoaded() && !pending.length) { - this._reportEvent(eventInfo, opt_noLog); - } else { - // We cache until metrics plugin is loaded - pending.push([eventInfo, opt_noLog]); - if (this._isMetricsPluginLoaded()) { - pending.forEach(([eventInfo, opt_noLog]) => { - this._reportEvent(eventInfo, opt_noLog); - }); - pending = []; - } - } - }, + reportErrorDialog(message) { + this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY, + 'ErrorDialog: ' + message, {error: new Error(message)}); + }, - _reportEvent(eventInfo, opt_noLog) { - const {type, value, name} = eventInfo; - document.dispatchEvent(new CustomEvent(type, {detail: eventInfo})); - if (opt_noLog) { return; } - if (type !== ERROR.TYPE) { - if (value !== undefined) { - console.log(`Reporting: ${name}: ${value}`); - } else { - console.log(`Reporting: ${name}`); - } - } - }, + setRepoName(repoName) { + reportRepoName = repoName; + }, +}); - _createEventInfo(type, category, name, value, eventDetails) { - const eventInfo = { - type, - category, - name, - value, - eventStart: this.now(), - }; - - if (typeof(eventDetails) === 'object' && - Object.entries(eventDetails).length !== 0) { - eventInfo.eventDetails = JSON.stringify(eventDetails); - } - if (reportRepoName) { - eventInfo.repoName = reportRepoName; - } - const isInBackgroundTab = document.visibilityState === 'hidden'; - if (isInBackgroundTab !== undefined) { - eventInfo.inBackgroundTab = isInBackgroundTab; - } - - return eventInfo; - }, - - /** - * User-perceived app start time, should be reported when the app is ready. - */ - appStarted() { - this.timeEnd(TIMING.APP_STARTED); - this.pageLoaded(); - }, - - /** - * Page load time and other metrics, should be reported at any time - * after navigation. - */ - pageLoaded() { - if (this.performanceTiming.loadEventEnd === 0) { - console.error('pageLoaded should be called after window.onload'); - this.async(this.pageLoaded, 100); - } else { - const perfEvents = Object.keys(this.performanceTiming.toJSON()); - perfEvents.forEach( - eventName => this._reportPerformanceTiming(eventName) - ); - } - }, - - _reportPerformanceTiming(eventName, eventDetails) { - const eventTiming = this.performanceTiming[eventName]; - if (eventTiming > 0) { - const elapsedTime = eventTiming - - this.performanceTiming.navigationStart; - // NavResTime - Navigation and resource timings. - this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, - `NavResTime - ${eventName}`, elapsedTime, eventDetails, true); - } - }, - - beforeLocationChanged() { - for (const prop of Object.keys(this._baselines)) { - delete this._baselines[prop]; - } - this.time(TIMER.CHANGE_DISPLAYED); - this.time(TIMER.CHANGE_LOAD_FULL); - this.time(TIMER.DASHBOARD_DISPLAYED); - this.time(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); - this.time(TIMER.DIFF_VIEW_DISPLAYED); - this.time(TIMER.DIFF_VIEW_LOAD_FULL); - this.time(TIMER.FILE_LIST_DISPLAYED); - reportRepoName = undefined; - // reset slow rpc list since here start page loads which report these rpcs - slowRpcList = []; - }, - - locationChanged(page) { - this.reporter( - NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page); - }, - - dashboardDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_DASHBOARD_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_DASHBOARD_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } else { - this.timeEnd(TIMER.DASHBOARD_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } - }, - - changeDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_CHANGE_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } else { - this.timeEnd(TIMER.CHANGE_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } - }, - - changeFullyLoaded() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_CHANGE_LOAD_FULL)) { - this.timeEnd(TIMER.STARTUP_CHANGE_LOAD_FULL); - } else { - this.timeEnd(TIMER.CHANGE_LOAD_FULL); - } - }, - - diffViewDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_DIFF_VIEW_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } else { - this.timeEnd(TIMER.DIFF_VIEW_DISPLAYED, {rpcList: - this.slowRpcSnapshot}); - } - }, - - diffViewFullyLoaded() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL)) { - this.timeEnd(TIMER.STARTUP_DIFF_VIEW_LOAD_FULL); - } else { - this.timeEnd(TIMER.DIFF_VIEW_LOAD_FULL); - } - }, - - diffViewContentDisplayed() { - if (this._baselines.hasOwnProperty( - TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_DIFF_VIEW_CONTENT_DISPLAYED); - } else { - this.timeEnd(TIMER.DIFF_VIEW_CONTENT_DISPLAYED); - } - }, - - fileListDisplayed() { - if (this._baselines.hasOwnProperty(TIMER.STARTUP_FILE_LIST_DISPLAYED)) { - this.timeEnd(TIMER.STARTUP_FILE_LIST_DISPLAYED); - } else { - this.timeEnd(TIMER.FILE_LIST_DISPLAYED); - } - }, - - reportExtension(name) { - this.reporter(EXTENSION.TYPE, EXTENSION.DETECTED, name); - }, - - pluginLoaded(name) { - if (name.startsWith('metrics-')) { - this.timeEnd(TIMER.METRICS_PLUGIN_LOADED); - } - }, - - pluginsLoaded(pluginsList) { - this.timeEnd(TIMER.PLUGINS_LOADED); - this.reporter( - PLUGINS.TYPE, PLUGINS.INSTALLED, PLUGINS.INSTALLED, undefined, - {pluginsList: pluginsList || []}, true); - }, - - /** - * Reset named timer. - */ - time(name) { - this._baselines[name] = this.now(); - window.performance.mark(`${name}-start`); - }, - - /** - * Finish named timer and report it to server. - */ - timeEnd(name, eventDetails) { - if (!this._baselines.hasOwnProperty(name)) { return; } - const baseTime = this._baselines[name]; - delete this._baselines[name]; - this._reportTiming(name, this.now() - baseTime, eventDetails); - - // Finalize the interval. Either from a registered start mark or - // the navigation start time (if baseTime is 0). - if (baseTime !== 0) { - window.performance.measure(name, `${name}-start`); - } else { - // Microsft Edge does not handle the 2nd param correctly - // (if undefined). - window.performance.measure(name); - } - }, - - /** - * Reports just line timeEnd, but additionally reports an average given a - * denominator and a separate reporiting name for the average. - * - * @param {string} name Timing name. - * @param {string} averageName Average timing name. - * @param {number} denominator Number by which to divide the total to - * compute the average. - */ - timeEndWithAverage(name, averageName, denominator) { - if (!this._baselines.hasOwnProperty(name)) { return; } - const baseTime = this._baselines[name]; - this.timeEnd(name); - - // Guard against division by zero. - if (!denominator) { return; } - const time = this.now() - baseTime; - this._reportTiming(averageName, time / denominator); - }, - - /** - * Send a timing report with an arbitrary time value. - * - * @param {string} name Timing name. - * @param {number} time The time to report as an integer of milliseconds. - * @param {Object} eventDetails non sensitive details - */ - _reportTiming(name, time, eventDetails) { - this.reporter(TIMING.TYPE, TIMING.CATEGORY_UI_LATENCY, name, time, - eventDetails); - }, - - /** - * Get a timer object to for reporing a user timing. The start time will be - * the time that the object has been created, and the end time will be the - * time that the "end" method is called on the object. - * - * @param {string} name Timing name. - * @returns {!Object} The timer object. - */ - getTimer(name) { - let called = false; - let start; - let max = null; - - const timer = { - - // Clear the timer and reset the start time. - reset: () => { - called = false; - start = this.now(); - return timer; - }, - - // Stop the timer and report the intervening time. - end: () => { - if (called) { - throw new Error(`Timer for "${name}" already ended.`); - } - called = true; - const time = this.now() - start; - - // If a maximum is specified and the time exceeds it, do not report. - if (max && time > max) { return timer; } - - this._reportTiming(name, time); - return timer; - }, - - // Set a maximum reportable time. If a maximum is set and the timer is - // ended after the specified amount of time, the value is not reported. - withMaximum(maximum) { - max = maximum; - return timer; - }, - }; - - // The timer is initialized to its creation time. - return timer.reset(); - }, - - /** - * Log timing information for an RPC. - * - * @param {string} anonymizedUrl The URL of the RPC with tokens obfuscated. - * @param {number} elapsed The time elapsed of the RPC. - */ - reportRpcTiming(anonymizedUrl, elapsed) { - this.reporter(TIMING.TYPE, TIMING.CATEGORY_RPC, 'RPC-' + anonymizedUrl, - elapsed, {}, true); - if (elapsed >= SLOW_RPC_THRESHOLD) { - slowRpcList.push({anonymizedUrl, elapsed}); - } - }, - - reportInteraction(eventName, details) { - this.reporter(INTERACTION_TYPE, this.category, eventName, undefined, - details, true); - }, - - /** - * A draft interaction was started. Update the time-betweeen-draft-actions - * timer. - */ - recordDraftInteraction() { - // If there is no timer defined, then this is the first interaction. - // Set up the timer so that it's ready to record the intervening time when - // called again. - const timer = this._timers.timeBetweenDraftActions; - if (!timer) { - // Create a timer with a maximum length. - this._timers.timeBetweenDraftActions = this.getTimer(DRAFT_ACTION_TIMER) - .withMaximum(DRAFT_ACTION_TIMER_MAX); - return; - } - - // Mark the time and reinitialize the timer. - timer.end().reset(); - }, - - reportErrorDialog(message) { - this.reporter(ERROR_DIALOG.TYPE, ERROR_DIALOG.CATEGORY, - 'ErrorDialog: ' + message, {error: new Error(message)}); - }, - - setRepoName(repoName) { - reportRepoName = repoName; - }, - }); - - window.GrReporting = GrReporting; - // Expose onerror installation so it would be accessible from tests. - window.GrReporting._catchErrors = catchErrors; - window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS); -})(); +window.GrReporting = GrReporting; +// Expose onerror installation so it would be accessible from tests. +window.GrReporting._catchErrors = catchErrors; +window.GrReporting.STARTUP_TIMERS = Object.assign({}, STARTUP_TIMERS);
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html index 19e4b74..ad9903e 100644 --- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html +++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reporting</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-reporting.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-reporting.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reporting.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,407 +40,409 @@ </template> </test-fixture> -<script> - suite('gr-reporting tests', async () => { - await readyToTest(); - let element; - let sandbox; - let clock; - let fakePerformance; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-reporting.js'; +suite('gr-reporting tests', () => { + let element; + let sandbox; + let clock; + let fakePerformance; - const NOW_TIME = 100; + const NOW_TIME = 100; + setup(() => { + sandbox = sinon.sandbox.create(); + clock = sinon.useFakeTimers(NOW_TIME); + element = fixture('basic'); + element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS); + fakePerformance = { + navigationStart: 1, + loadEventEnd: 2, + }; + fakePerformance.toJSON = () => fakePerformance; + sinon.stub(element, 'performanceTiming', + {get() { return fakePerformance; }}); + sandbox.stub(element, 'reporter'); + }); + + teardown(() => { + sandbox.restore(); + clock.restore(); + }); + + test('appStarted', () => { + sandbox.stub(element, 'now').returns(42); + element.appStarted(); + assert.isTrue( + element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'App Started', 42 + )); + }); + + test('WebComponentsReady', () => { + sandbox.stub(element, 'now').returns(42); + element.timeEnd('WebComponentsReady'); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'WebComponentsReady', 42 + )); + }); + + test('pageLoaded', () => { + element.pageLoaded(); + assert.isTrue( + element.reporter.calledWithExactly( + 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd', + fakePerformance.loadEventEnd - fakePerformance.navigationStart, + undefined, true) + ); + }); + + test('beforeLocationChanged', () => { + element._baselines['garbage'] = 'monster'; + sandbox.stub(element, 'time'); + element.beforeLocationChanged(); + assert.isTrue(element.time.calledWithExactly('DashboardDisplayed')); + assert.isTrue(element.time.calledWithExactly('ChangeDisplayed')); + assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded')); + assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed')); + assert.isTrue(element.time.calledWithExactly('FileListDisplayed')); + assert.isFalse(element._baselines.hasOwnProperty('garbage')); + }); + + test('changeDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.changeDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []})); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupChangeDisplayed', + {rpcList: []})); + element.changeDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed', + {rpcList: []})); + }); + + test('changeFullyLoaded', () => { + sandbox.spy(element, 'timeEnd'); + element.changeFullyLoaded(); + assert.isFalse( + element.timeEnd.calledWithExactly('ChangeFullyLoaded')); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupChangeFullyLoaded')); + element.changeFullyLoaded(); + assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded')); + }); + + test('diffViewDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.diffViewDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('DiffViewDisplayed', + {rpcList: []})); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupDiffViewDisplayed', + {rpcList: []})); + element.diffViewDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed', + {rpcList: []})); + }); + + test('fileListDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.fileListDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('FileListDisplayed')); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupFileListDisplayed')); + element.fileListDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed')); + }); + + test('dashboardDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.dashboardDisplayed(); + assert.isFalse( + element.timeEnd.calledWithExactly('DashboardDisplayed', + {rpcList: []})); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupDashboardDisplayed', + {rpcList: []})); + element.dashboardDisplayed(); + assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed', + {rpcList: []})); + }); + + test('dashboardDisplayed', () => { + sandbox.spy(element, 'timeEnd'); + element.reportRpcTiming('/changes/*~*/comments', 500); + element.dashboardDisplayed(); + assert.isTrue( + element.timeEnd.calledWithExactly('StartupDashboardDisplayed', + {rpcList: [ + { + anonymizedUrl: '/changes/*~*/comments', + elapsed: 500, + }, + ]} + )); + }); + + test('time and timeEnd', () => { + const nowStub = sandbox.stub(element, 'now').returns(0); + element.time('foo'); + nowStub.returns(1); + element.time('bar'); + nowStub.returns(2); + element.timeEnd('bar'); + nowStub.returns(3); + element.timeEnd('foo'); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'foo', 3 + )); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'bar', 1 + )); + }); + + test('timer object', () => { + const nowStub = sandbox.stub(element, 'now').returns(100); + const timer = element.getTimer('foo-bar'); + nowStub.returns(150); + timer.end(); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'foo-bar', 50)); + }); + + test('timer object double call', () => { + const timer = element.getTimer('foo-bar'); + timer.end(); + assert.isTrue(element.reporter.calledOnce); + assert.throws(() => { + timer.end(); + }, 'Timer for "foo-bar" already ended.'); + }); + + test('timer object maximum', () => { + const nowStub = sandbox.stub(element, 'now').returns(100); + const timer = element.getTimer('foo-bar').withMaximum(100); + nowStub.returns(150); + timer.end(); + assert.isTrue(element.reporter.calledOnce); + + timer.reset(); + nowStub.returns(260); + timer.end(); + assert.isTrue(element.reporter.calledOnce); + }); + + test('recordDraftInteraction', () => { + const key = 'TimeBetweenDraftActions'; + const nowStub = sandbox.stub(element, 'now').returns(100); + const timingStub = sandbox.stub(element, '_reportTiming'); + element.recordDraftInteraction(); + assert.isFalse(timingStub.called); + + nowStub.returns(200); + element.recordDraftInteraction(); + assert.isTrue(timingStub.calledOnce); + assert.equal(timingStub.lastCall.args[0], key); + assert.equal(timingStub.lastCall.args[1], 100); + + nowStub.returns(350); + element.recordDraftInteraction(); + assert.isTrue(timingStub.calledTwice); + assert.equal(timingStub.lastCall.args[0], key); + assert.equal(timingStub.lastCall.args[1], 150); + + nowStub.returns(370 + 2 * 60 * 1000); + element.recordDraftInteraction(); + assert.isFalse(timingStub.calledThrice); + }); + + test('timeEndWithAverage', () => { + const nowStub = sandbox.stub(element, 'now').returns(0); + nowStub.returns(1000); + element.time('foo'); + nowStub.returns(1100); + element.timeEndWithAverage('foo', 'bar', 10); + assert.isTrue(element.reporter.calledTwice); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'foo', 100)); + assert.isTrue(element.reporter.calledWithMatch( + 'timing-report', 'UI Latency', 'bar', 10)); + }); + + test('reportExtension', () => { + element.reportExtension('foo'); + assert.isTrue(element.reporter.calledWithExactly( + 'lifecycle', 'Extension detected', 'foo' + )); + }); + + test('reportInteraction', () => { + element.reporter.restore(); + sandbox.spy(element, '_reportEvent'); + element.pluginsLoaded(); // so we don't cache + element.reportInteraction('button-click', {name: 'sendReply'}); + assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + { + type: 'interaction', + name: 'button-click', + eventDetails: JSON.stringify({name: 'sendReply'}), + } + )); + }); + + test('report start time', () => { + element.reporter.restore(); + sandbox.stub(element, 'now').returns(42); + sandbox.spy(element, '_reportEvent'); + const dispatchStub = sandbox.spy(document, 'dispatchEvent'); + element.pluginsLoaded(); + element.time('timeAction'); + element.timeEnd('timeAction'); + assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + { + type: 'timing-report', + category: 'UI Latency', + name: 'timeAction', + value: 0, + eventStart: 42, + } + )); + assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42); + }); + + suite('plugins', () => { setup(() => { - sandbox = sinon.sandbox.create(); - clock = sinon.useFakeTimers(NOW_TIME); - element = fixture('basic'); - element._baselines = Object.assign({}, GrReporting.STARTUP_TIMERS); - fakePerformance = { - navigationStart: 1, - loadEventEnd: 2, - }; - fakePerformance.toJSON = () => fakePerformance; - sinon.stub(element, 'performanceTiming', - {get() { return fakePerformance; }}); - sandbox.stub(element, 'reporter'); - }); - - teardown(() => { - sandbox.restore(); - clock.restore(); - }); - - test('appStarted', () => { - sandbox.stub(element, 'now').returns(42); - element.appStarted(); - assert.isTrue( - element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'App Started', 42 - )); - }); - - test('WebComponentsReady', () => { - sandbox.stub(element, 'now').returns(42); - element.timeEnd('WebComponentsReady'); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'WebComponentsReady', 42 - )); - }); - - test('pageLoaded', () => { - element.pageLoaded(); - assert.isTrue( - element.reporter.calledWithExactly( - 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd', - fakePerformance.loadEventEnd - fakePerformance.navigationStart, - undefined, true) - ); - }); - - test('beforeLocationChanged', () => { - element._baselines['garbage'] = 'monster'; - sandbox.stub(element, 'time'); - element.beforeLocationChanged(); - assert.isTrue(element.time.calledWithExactly('DashboardDisplayed')); - assert.isTrue(element.time.calledWithExactly('ChangeDisplayed')); - assert.isTrue(element.time.calledWithExactly('ChangeFullyLoaded')); - assert.isTrue(element.time.calledWithExactly('DiffViewDisplayed')); - assert.isTrue(element.time.calledWithExactly('FileListDisplayed')); - assert.isFalse(element._baselines.hasOwnProperty('garbage')); - }); - - test('changeDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.changeDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('ChangeDisplayed', {rpcList: []})); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupChangeDisplayed', - {rpcList: []})); - element.changeDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('ChangeDisplayed', - {rpcList: []})); - }); - - test('changeFullyLoaded', () => { - sandbox.spy(element, 'timeEnd'); - element.changeFullyLoaded(); - assert.isFalse( - element.timeEnd.calledWithExactly('ChangeFullyLoaded')); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupChangeFullyLoaded')); - element.changeFullyLoaded(); - assert.isTrue(element.timeEnd.calledWithExactly('ChangeFullyLoaded')); - }); - - test('diffViewDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.diffViewDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('DiffViewDisplayed', - {rpcList: []})); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupDiffViewDisplayed', - {rpcList: []})); - element.diffViewDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('DiffViewDisplayed', - {rpcList: []})); - }); - - test('fileListDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.fileListDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('FileListDisplayed')); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupFileListDisplayed')); - element.fileListDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('FileListDisplayed')); - }); - - test('dashboardDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.dashboardDisplayed(); - assert.isFalse( - element.timeEnd.calledWithExactly('DashboardDisplayed', - {rpcList: []})); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupDashboardDisplayed', - {rpcList: []})); - element.dashboardDisplayed(); - assert.isTrue(element.timeEnd.calledWithExactly('DashboardDisplayed', - {rpcList: []})); - }); - - test('dashboardDisplayed', () => { - sandbox.spy(element, 'timeEnd'); - element.reportRpcTiming('/changes/*~*/comments', 500); - element.dashboardDisplayed(); - assert.isTrue( - element.timeEnd.calledWithExactly('StartupDashboardDisplayed', - {rpcList: [ - { - anonymizedUrl: '/changes/*~*/comments', - elapsed: 500, - }, - ]} - )); - }); - - test('time and timeEnd', () => { - const nowStub = sandbox.stub(element, 'now').returns(0); - element.time('foo'); - nowStub.returns(1); - element.time('bar'); - nowStub.returns(2); - element.timeEnd('bar'); - nowStub.returns(3); - element.timeEnd('foo'); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'foo', 3 - )); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'bar', 1 - )); - }); - - test('timer object', () => { - const nowStub = sandbox.stub(element, 'now').returns(100); - const timer = element.getTimer('foo-bar'); - nowStub.returns(150); - timer.end(); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'foo-bar', 50)); - }); - - test('timer object double call', () => { - const timer = element.getTimer('foo-bar'); - timer.end(); - assert.isTrue(element.reporter.calledOnce); - assert.throws(() => { - timer.end(); - }, 'Timer for "foo-bar" already ended.'); - }); - - test('timer object maximum', () => { - const nowStub = sandbox.stub(element, 'now').returns(100); - const timer = element.getTimer('foo-bar').withMaximum(100); - nowStub.returns(150); - timer.end(); - assert.isTrue(element.reporter.calledOnce); - - timer.reset(); - nowStub.returns(260); - timer.end(); - assert.isTrue(element.reporter.calledOnce); - }); - - test('recordDraftInteraction', () => { - const key = 'TimeBetweenDraftActions'; - const nowStub = sandbox.stub(element, 'now').returns(100); - const timingStub = sandbox.stub(element, '_reportTiming'); - element.recordDraftInteraction(); - assert.isFalse(timingStub.called); - - nowStub.returns(200); - element.recordDraftInteraction(); - assert.isTrue(timingStub.calledOnce); - assert.equal(timingStub.lastCall.args[0], key); - assert.equal(timingStub.lastCall.args[1], 100); - - nowStub.returns(350); - element.recordDraftInteraction(); - assert.isTrue(timingStub.calledTwice); - assert.equal(timingStub.lastCall.args[0], key); - assert.equal(timingStub.lastCall.args[1], 150); - - nowStub.returns(370 + 2 * 60 * 1000); - element.recordDraftInteraction(); - assert.isFalse(timingStub.calledThrice); - }); - - test('timeEndWithAverage', () => { - const nowStub = sandbox.stub(element, 'now').returns(0); - nowStub.returns(1000); - element.time('foo'); - nowStub.returns(1100); - element.timeEndWithAverage('foo', 'bar', 10); - assert.isTrue(element.reporter.calledTwice); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'foo', 100)); - assert.isTrue(element.reporter.calledWithMatch( - 'timing-report', 'UI Latency', 'bar', 10)); - }); - - test('reportExtension', () => { - element.reportExtension('foo'); - assert.isTrue(element.reporter.calledWithExactly( - 'lifecycle', 'Extension detected', 'foo' - )); - }); - - test('reportInteraction', () => { element.reporter.restore(); - sandbox.spy(element, '_reportEvent'); - element.pluginsLoaded(); // so we don't cache - element.reportInteraction('button-click', {name: 'sendReply'}); - assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( - { - type: 'interaction', - name: 'button-click', - eventDetails: JSON.stringify({name: 'sendReply'}), - } - )); + sandbox.stub(element, '_reportEvent'); }); - test('report start time', () => { - element.reporter.restore(); + test('pluginsLoaded reports time', () => { sandbox.stub(element, 'now').returns(42); - sandbox.spy(element, '_reportEvent'); - const dispatchStub = sandbox.spy(document, 'dispatchEvent'); element.pluginsLoaded(); - element.time('timeAction'); - element.timeEnd('timeAction'); - assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + assert.isTrue(element._reportEvent.calledWithMatch( { type: 'timing-report', category: 'UI Latency', - name: 'timeAction', - value: 0, - eventStart: 42, + name: 'PluginsLoaded', + value: 42, } )); - assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42); }); - suite('plugins', () => { - setup(() => { - element.reporter.restore(); - sandbox.stub(element, '_reportEvent'); - }); - - test('pluginsLoaded reports time', () => { - sandbox.stub(element, 'now').returns(42); - element.pluginsLoaded(); - assert.isTrue(element._reportEvent.calledWithMatch( - { - type: 'timing-report', - category: 'UI Latency', - name: 'PluginsLoaded', - value: 42, - } - )); - }); - - test('pluginsLoaded reports plugins', () => { - element.pluginsLoaded(['foo', 'bar']); - assert.isTrue(element._reportEvent.calledWithMatch( - { - type: 'lifecycle', - category: 'Plugins installed', - eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}), - } - )); - }); - - test('caches reports if plugins are not loaded', () => { - element.timeEnd('foo'); - assert.isFalse(element._reportEvent.called); - }); - - test('reports if plugins are loaded', () => { - element.pluginsLoaded(); - assert.isTrue(element._reportEvent.called); - }); - - test('reports if metrics plugin xyz is loaded', () => { - element.pluginLoaded('metrics-xyz'); - assert.isTrue(element._reportEvent.called); - }); - - test('reports cached events preserving order', () => { - element.time('foo'); - element.time('bar'); - element.timeEnd('foo'); - element.pluginsLoaded(); - element.timeEnd('bar'); - assert.isTrue(element._reportEvent.getCall(0).calledWithMatch( - {type: 'timing-report', category: 'UI Latency', name: 'foo'} - )); - assert.isTrue(element._reportEvent.getCall(1).calledWithMatch( - {type: 'timing-report', category: 'UI Latency', - name: 'PluginsLoaded'} - )); - assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( - {type: 'lifecycle', category: 'Plugins installed'} - )); - assert.isTrue(element._reportEvent.getCall(3).calledWithMatch( - {type: 'timing-report', category: 'UI Latency', name: 'bar'} - )); - }); + test('pluginsLoaded reports plugins', () => { + element.pluginsLoaded(['foo', 'bar']); + assert.isTrue(element._reportEvent.calledWithMatch( + { + type: 'lifecycle', + category: 'Plugins installed', + eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}), + } + )); }); - test('search', () => { - element.locationChanged('_handleSomeRoute'); - assert.isTrue(element.reporter.calledWithExactly( - 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute')); + test('caches reports if plugins are not loaded', () => { + element.timeEnd('foo'); + assert.isFalse(element._reportEvent.called); }); - suite('exception logging', () => { - let fakeWindow; - let reporter; + test('reports if plugins are loaded', () => { + element.pluginsLoaded(); + assert.isTrue(element._reportEvent.called); + }); - const emulateThrow = function(msg, url, line, column, error) { - return fakeWindow.onerror(msg, url, line, column, error); - }; + test('reports if metrics plugin xyz is loaded', () => { + element.pluginLoaded('metrics-xyz'); + assert.isTrue(element._reportEvent.called); + }); - setup(() => { - reporter = sandbox.stub(GrReporting.prototype, 'reporter'); - fakeWindow = { - handlers: {}, - addEventListener(type, handler) { - this.handlers[type] = handler; - }, - }; - sandbox.stub(console, 'error'); - window.GrReporting._catchErrors(fakeWindow); - }); - - test('is reported', () => { - const error = new Error('bar'); - error.stack = undefined; - emulateThrow('bar', 'http://url', 4, 2, error); - assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); - const payload = reporter.lastCall.args[3]; - assert.deepEqual(payload, { - url: 'http://url', - line: 4, - column: 2, - error, - }); - }); - - test('is reported with 3 lines of stack', () => { - const error = new Error('bar'); - emulateThrow('bar', 'http://url', 4, 2, error); - const expectedStack = error.stack.split('\n').slice(0, 3) - .join('\n'); - assert.isTrue(reporter.calledWith('error', 'exception', - expectedStack)); - }); - - test('prevent default event handler', () => { - assert.isTrue(emulateThrow()); - }); - - test('unhandled rejection', () => { - fakeWindow.handlers['unhandledrejection']({ - reason: { - message: 'bar', - }, - }); - assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); - }); + test('reports cached events preserving order', () => { + element.time('foo'); + element.time('bar'); + element.timeEnd('foo'); + element.pluginsLoaded(); + element.timeEnd('bar'); + assert.isTrue(element._reportEvent.getCall(0).calledWithMatch( + {type: 'timing-report', category: 'UI Latency', name: 'foo'} + )); + assert.isTrue(element._reportEvent.getCall(1).calledWithMatch( + {type: 'timing-report', category: 'UI Latency', + name: 'PluginsLoaded'} + )); + assert.isTrue(element._reportEvent.getCall(2).calledWithMatch( + {type: 'lifecycle', category: 'Plugins installed'} + )); + assert.isTrue(element._reportEvent.getCall(3).calledWithMatch( + {type: 'timing-report', category: 'UI Latency', name: 'bar'} + )); }); }); + + test('search', () => { + element.locationChanged('_handleSomeRoute'); + assert.isTrue(element.reporter.calledWithExactly( + 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute')); + }); + + suite('exception logging', () => { + let fakeWindow; + let reporter; + + const emulateThrow = function(msg, url, line, column, error) { + return fakeWindow.onerror(msg, url, line, column, error); + }; + + setup(() => { + reporter = sandbox.stub(GrReporting.prototype, 'reporter'); + fakeWindow = { + handlers: {}, + addEventListener(type, handler) { + this.handlers[type] = handler; + }, + }; + sandbox.stub(console, 'error'); + window.GrReporting._catchErrors(fakeWindow); + }); + + test('is reported', () => { + const error = new Error('bar'); + error.stack = undefined; + emulateThrow('bar', 'http://url', 4, 2, error); + assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); + const payload = reporter.lastCall.args[3]; + assert.deepEqual(payload, { + url: 'http://url', + line: 4, + column: 2, + error, + }); + }); + + test('is reported with 3 lines of stack', () => { + const error = new Error('bar'); + emulateThrow('bar', 'http://url', 4, 2, error); + const expectedStack = error.stack.split('\n').slice(0, 3) + .join('\n'); + assert.isTrue(reporter.calledWith('error', 'exception', + expectedStack)); + }); + + test('prevent default event handler', () => { + assert.isTrue(emulateThrow()); + }); + + test('unhandled rejection', () => { + fakeWindow.handlers['unhandledrejection']({ + reason: { + message: 'bar', + }, + }); + assert.isTrue(reporter.calledWith('error', 'exception', 'bar')); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js index ebac1e1..e461d1d 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -14,1520 +14,1535 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const RoutePattern = { - ROOT: '/', +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-reporting/gr-reporting.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import page from 'page/page.mjs'; +self.page = page; +import {htmlTemplate} from './gr-router_html.js'; - DASHBOARD: /^\/dashboard\/(.+)$/, - CUSTOM_DASHBOARD: /^\/dashboard\/?$/, - PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/, +const RoutePattern = { + ROOT: '/', - AGREEMENTS: /^\/settings\/agreements\/?/, - NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/, - REGISTER: /^\/register(\/.*)?$/, + DASHBOARD: /^\/dashboard\/(.+)$/, + CUSTOM_DASHBOARD: /^\/dashboard\/?$/, + PROJECT_DASHBOARD: /^\/p\/(.+)\/\+\/dashboard\/(.+)/, - // Pattern for login and logout URLs intended to be passed-through. May - // include a return URL. - LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/, + AGREEMENTS: /^\/settings\/agreements\/?/, + NEW_AGREEMENTS: /^\/settings\/new-agreement\/?/, + REGISTER: /^\/register(\/.*)?$/, - // Pattern for a catchall route when no other pattern is matched. - DEFAULT: /.*/, + // Pattern for login and logout URLs intended to be passed-through. May + // include a return URL. + LOG_IN_OR_OUT: /\/log(in|out)(\/(.+))?$/, - // Matches /admin/groups/[uuid-]<group> - GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/, + // Pattern for a catchall route when no other pattern is matched. + DEFAULT: /.*/, - // Redirects /groups/self to /settings/#Groups for GWT compatibility - GROUP_SELF: /^\/groups\/self/, + // Matches /admin/groups/[uuid-]<group> + GROUP: /^\/admin\/groups\/(?:uuid-)?([^,]+)$/, - // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui) - // Redirects to /admin/groups/[uuid-]<group> - GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/, + // Redirects /groups/self to /settings/#Groups for GWT compatibility + GROUP_SELF: /^\/groups\/self/, - // Matches /admin/groups/<group>,audit-log - GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/, + // Matches /admin/groups/[uuid-]<group>,info (backwords compat with gwtui) + // Redirects to /admin/groups/[uuid-]<group> + GROUP_INFO: /^\/admin\/groups\/(?:uuid-)?(.+),info$/, - // Matches /admin/groups/[uuid-]<group>,members - GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/, + // Matches /admin/groups/<group>,audit-log + GROUP_AUDIT_LOG: /^\/admin\/groups\/(?:uuid-)?(.+),audit-log$/, - // Matches /admin/groups[,<offset>][/]. - GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/, - GROUP_LIST_FILTER: '/admin/groups/q/filter::filter', - GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset', + // Matches /admin/groups/[uuid-]<group>,members + GROUP_MEMBERS: /^\/admin\/groups\/(?:uuid-)?(.+),members$/, - // Matches /admin/create-project - LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/, + // Matches /admin/groups[,<offset>][/]. + GROUP_LIST_OFFSET: /^\/admin\/groups(,(\d+))?(\/)?$/, + GROUP_LIST_FILTER: '/admin/groups/q/filter::filter', + GROUP_LIST_FILTER_OFFSET: '/admin/groups/q/filter::filter,:offset', - // Matches /admin/create-project - LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/, + // Matches /admin/create-project + LEGACY_CREATE_PROJECT: /^\/admin\/create-project\/?$/, - PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/, + // Matches /admin/create-project + LEGACY_CREATE_GROUP: /^\/admin\/create-group\/?$/, - // Matches /admin/repos/<repo> - REPO: /^\/admin\/repos\/([^,]+)$/, + PROJECT_OLD: /^\/admin\/(projects)\/?(.+)?$/, - // Matches /admin/repos/<repo>,commands. - REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/, + // Matches /admin/repos/<repo> + REPO: /^\/admin\/repos\/([^,]+)$/, - // Matches /admin/repos/<repos>,access. - REPO_ACCESS: /^\/admin\/repos\/(.+),access$/, + // Matches /admin/repos/<repo>,commands. + REPO_COMMANDS: /^\/admin\/repos\/(.+),commands$/, - // Matches /admin/repos/<repos>,access. - REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/, + // Matches /admin/repos/<repos>,access. + REPO_ACCESS: /^\/admin\/repos\/(.+),access$/, - // Matches /admin/repos[,<offset>][/]. - REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/, - REPO_LIST_FILTER: '/admin/repos/q/filter::filter', - REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset', + // Matches /admin/repos/<repos>,access. + REPO_DASHBOARDS: /^\/admin\/repos\/(.+),dashboards$/, - // Matches /admin/repos/<repo>,branches[,<offset>]. - BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/, - BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter', - BRANCH_LIST_FILTER_OFFSET: - '/admin/repos/:repo,branches/q/filter::filter,:offset', + // Matches /admin/repos[,<offset>][/]. + REPO_LIST_OFFSET: /^\/admin\/repos(,(\d+))?(\/)?$/, + REPO_LIST_FILTER: '/admin/repos/q/filter::filter', + REPO_LIST_FILTER_OFFSET: '/admin/repos/q/filter::filter,:offset', - // Matches /admin/repos/<repo>,tags[,<offset>]. - TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/, - TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter', - TAG_LIST_FILTER_OFFSET: - '/admin/repos/:repo,tags/q/filter::filter,:offset', + // Matches /admin/repos/<repo>,branches[,<offset>]. + BRANCH_LIST_OFFSET: /^\/admin\/repos\/(.+),branches(,(.+))?$/, + BRANCH_LIST_FILTER: '/admin/repos/:repo,branches/q/filter::filter', + BRANCH_LIST_FILTER_OFFSET: + '/admin/repos/:repo,branches/q/filter::filter,:offset', - PLUGINS: /^\/plugins\/(.+)$/, + // Matches /admin/repos/<repo>,tags[,<offset>]. + TAG_LIST_OFFSET: /^\/admin\/repos\/(.+),tags(,(.+))?$/, + TAG_LIST_FILTER: '/admin/repos/:repo,tags/q/filter::filter', + TAG_LIST_FILTER_OFFSET: + '/admin/repos/:repo,tags/q/filter::filter,:offset', - PLUGIN_LIST: /^\/admin\/plugins(\/)?$/, + PLUGINS: /^\/plugins\/(.+)$/, - // Matches /admin/plugins[,<offset>][/]. - PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/, - PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter', - PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset', + PLUGIN_LIST: /^\/admin\/plugins(\/)?$/, - QUERY: /^\/q\/([^,]+)(,(\d+))?$/, + // Matches /admin/plugins[,<offset>][/]. + PLUGIN_LIST_OFFSET: /^\/admin\/plugins(,(\d+))?(\/)?$/, + PLUGIN_LIST_FILTER: '/admin/plugins/q/filter::filter', + PLUGIN_LIST_FILTER_OFFSET: '/admin/plugins/q/filter::filter,:offset', - /** - * Support vestigial params from GWT UI. - * - * @see Issue 7673. - * @type {!RegExp} - */ - QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/, - - // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/]. - CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, - CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/, - - // Matches - // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>]. - // TODO(kaspern): Migrate completely to project based URLs, with backwards - // compatibility for change-only. - CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, - - // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit - CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/, - - // Matches - // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>. - // TODO(kaspern): Migrate completely to project based URLs, with backwards - // compatibility for change-only. - // eslint-disable-next-line max-len - DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/, - - // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum] - DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/, - - // Matches non-project-relative - // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>. - DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/, - - // Matches diff routes using @\d+ to specify a file name (whether or not - // the project name is included). - // eslint-disable-next-line max-len - DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/, - - SETTINGS: /^\/settings\/?/, - SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/, - - // Matches /c/<changeNum>/ /<URL tail> - // Catches improperly encoded URLs (context: Issue 7100) - IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/, - - PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/, - - DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter', - DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/, - DOCUMENTATION: /^\/Documentation(\/)?(.+)?/, - }; + QUERY: /^\/q\/([^,]+)(,(\d+))?$/, /** - * Pattern to recognize and parse the diff line locations as they appear in - * the hash of diff URLs. In this format, a number on its own indicates that - * line number in the revision of the diff. A number prefixed by either an 'a' - * or a 'b' indicates that line number of the base of the diff. + * Support vestigial params from GWT UI. * - * @type {RegExp} + * @see Issue 7673. + * @type {!RegExp} */ - const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/; + QUERY_LEGACY_SUFFIX: /^\/q\/.+,n,z$/, - /** - * Pattern to recognize '+' in url-encoded strings for replacement with ' '. - */ - const PLUS_PATTERN = /\+/g; + // Matches /c/<changeNum>/[<basePatchNum>..][<patchNum>][/]. + CHANGE_LEGACY: /^\/c\/(\d+)\/?(((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, + CHANGE_NUMBER_LEGACY: /^\/(\d+)\/?/, - /** - * Pattern to recognize leading '?' in window.location.search, for stripping. - */ - const QUESTION_PATTERN = /^\?*/; + // Matches + // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..][<patchNum|edit>]. + // TODO(kaspern): Migrate completely to project based URLs, with backwards + // compatibility for change-only. + CHANGE: /^\/c\/(.+)\/\+\/(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?))?\/?$/, - /** - * GWT UI would use @\d+ at the end of a path to indicate linenum. - */ - const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/; + // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>],edit + CHANGE_EDIT: /^\/c\/(.+)\/\+\/(\d+)(\/(\d+))?,edit\/?$/, - const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/; + // Matches + // /c/<project>/+/<changeNum>/[<basePatchNum|edit>..]<patchNum|edit>/<path>. + // TODO(kaspern): Migrate completely to project based URLs, with backwards + // compatibility for change-only. + // eslint-disable-next-line max-len + DIFF: /^\/c\/(.+)\/\+\/(\d+)(\/((-?\d+|edit)(\.\.(\d+|edit))?(\/(.+))))\/?$/, - const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g; + // Matches /c/<project>/+/<changeNum>/[<patchNum|edit>]/<path>,edit[#lineNum] + DIFF_EDIT: /^\/c\/(.+)\/\+\/(\d+)\/(\d+|edit)\/(.+),edit(\#\d+)?$/, - // Polymer makes `app` intrinsically defined on the window by virtue of the - // custom element having the id "app", but it is made explicit here. - const app = document.querySelector('#app'); - if (!app) { - console.log('No gr-app found (running tests)'); + // Matches non-project-relative + // /c/<changeNum>/[<basePatchNum>..]<patchNum>/<path>. + DIFF_LEGACY: /^\/c\/(\d+)\/((-?\d+|edit)(\.\.(\d+|edit))?)\/(.+)/, + + // Matches diff routes using @\d+ to specify a file name (whether or not + // the project name is included). + // eslint-disable-next-line max-len + DIFF_LEGACY_LINENUM: /^\/c\/((.+)\/\+\/)?(\d+)(\/?((-?\d+|edit)(\.\.(\d+|edit))?\/(.+))?)@[ab]?\d+$/, + + SETTINGS: /^\/settings\/?/, + SETTINGS_LEGACY: /^\/settings\/VE\/(\S+)/, + + // Matches /c/<changeNum>/ /<URL tail> + // Catches improperly encoded URLs (context: Issue 7100) + IMPROPERLY_ENCODED_PLUS: /^\/c\/(.+)\/\ \/(.+)$/, + + PLUGIN_SCREEN: /^\/x\/([\w-]+)\/([\w-]+)\/?/, + + DOCUMENTATION_SEARCH_FILTER: '/Documentation/q/filter::filter', + DOCUMENTATION_SEARCH: /^\/Documentation\/q\/(.*)$/, + DOCUMENTATION: /^\/Documentation(\/)?(.+)?/, +}; + +/** + * Pattern to recognize and parse the diff line locations as they appear in + * the hash of diff URLs. In this format, a number on its own indicates that + * line number in the revision of the diff. A number prefixed by either an 'a' + * or a 'b' indicates that line number of the base of the diff. + * + * @type {RegExp} + */ +const LINE_ADDRESS_PATTERN = /^([ab]?)(\d+)$/; + +/** + * Pattern to recognize '+' in url-encoded strings for replacement with ' '. + */ +const PLUS_PATTERN = /\+/g; + +/** + * Pattern to recognize leading '?' in window.location.search, for stripping. + */ +const QUESTION_PATTERN = /^\?*/; + +/** + * GWT UI would use @\d+ at the end of a path to indicate linenum. + */ +const LEGACY_LINENUM_PATTERN = /@([ab]?\d+)$/; + +const LEGACY_QUERY_SUFFIX_PATTERN = /,n,z$/; + +const REPO_TOKEN_PATTERN = /\$\{(project|repo)\}/g; + +// Polymer makes `app` intrinsically defined on the window by virtue of the +// custom element having the id "app", but it is made explicit here. +const app = document.querySelector('#app'); +if (!app) { + console.log('No gr-app found (running tests)'); +} + +// Setup listeners outside of the router component initialization. +(function() { + const reporting = document.createElement('gr-reporting'); + + window.addEventListener('WebComponentsReady', () => { + reporting.timeEnd('WebComponentsReady'); + }); +})(); + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrRouter extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-router'; } + + static get properties() { + return { + _app: { + type: Object, + value: app, + }, + _isRedirecting: Boolean, + // This variable is to differentiate between internal navigation (false) + // and for first navigation in app after loaded from server (true). + _isInitialLoad: { + type: Boolean, + value: true, + }, + }; } - // Setup listeners outside of the router component initialization. - (function() { - const reporting = document.createElement('gr-reporting'); + start() { + if (!this._app) { return; } + this._startRouter(); + } - window.addEventListener('WebComponentsReady', () => { - reporting.timeEnd('WebComponentsReady'); - }); - })(); + _setParams(params) { + this._appElement().params = params; + } + + _appElement() { + // In Polymer2 you have to reach through the shadow root of the app + // element. This obviously breaks encapsulation. + // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element + // explicitly in app, or by delegating to it. + return document.getElementById('app-element') || + document.getElementById('app').shadowRoot.getElementById( + 'app-element'); + } + + _redirect(url) { + this._isRedirecting = true; + page.redirect(url); + } /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * @param {!Object} params + * @return {string} */ - class GrRouter extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-router'; } + _generateUrl(params) { + const base = this.getBaseUrl(); + let url = ''; + const Views = Gerrit.Nav.View; - static get properties() { - return { - _app: { - type: Object, - value: app, - }, - _isRedirecting: Boolean, - // This variable is to differentiate between internal navigation (false) - // and for first navigation in app after loaded from server (true). - _isInitialLoad: { - type: Boolean, - value: true, - }, - }; + if (params.view === Views.SEARCH) { + url = this._generateSearchUrl(params); + } else if (params.view === Views.CHANGE) { + url = this._generateChangeUrl(params); + } else if (params.view === Views.DASHBOARD) { + url = this._generateDashboardUrl(params); + } else if (params.view === Views.DIFF || params.view === Views.EDIT) { + url = this._generateDiffOrEditUrl(params); + } else if (params.view === Views.GROUP) { + url = this._generateGroupUrl(params); + } else if (params.view === Views.REPO) { + url = this._generateRepoUrl(params); + } else if (params.view === Views.ROOT) { + url = '/'; + } else if (params.view === Views.SETTINGS) { + url = this._generateSettingsUrl(params); + } else { + throw new Error('Can\'t generate'); } - start() { - if (!this._app) { return; } - this._startRouter(); + return base + url; + } + + _generateWeblinks(params) { + const type = params.type; + switch (type) { + case Gerrit.Nav.WeblinkType.FILE: + return this._getFileWebLinks(params); + case Gerrit.Nav.WeblinkType.CHANGE: + return this._getChangeWeblinks(params); + case Gerrit.Nav.WeblinkType.PATCHSET: + return this._getPatchSetWeblink(params); + default: + console.warn(`Unsupported weblink ${type}!`); + } + } + + _getPatchSetWeblink(params) { + const {commit, options} = params; + const {weblinks, config} = options || {}; + const name = commit && commit.slice(0, 7); + const weblink = this._getBrowseCommitWeblink(weblinks, config); + if (!weblink || !weblink.url) { + return {name}; + } else { + return {name, url: weblink.url}; + } + } + + _firstCodeBrowserWeblink(weblinks) { + // This is an ordered whitelist of web link types that provide direct + // links to the commit in the url property. + const codeBrowserLinks = ['gitiles', 'browse', 'gitweb']; + for (let i = 0; i < codeBrowserLinks.length; i++) { + const weblink = + weblinks.find(weblink => weblink.name === codeBrowserLinks[i]); + if (weblink) { return weblink; } + } + return null; + } + + _getBrowseCommitWeblink(weblinks, config) { + if (!weblinks) { return null; } + let weblink; + // Use primary weblink if configured and exists. + if (config && config.gerrit && config.gerrit.primary_weblink_name) { + weblink = weblinks.find( + weblink => weblink.name === config.gerrit.primary_weblink_name + ); + } + if (!weblink) { + weblink = this._firstCodeBrowserWeblink(weblinks); + } + if (!weblink) { return null; } + return weblink; + } + + _getChangeWeblinks({repo, commit, options: {weblinks, config}}) { + if (!weblinks || !weblinks.length) return []; + const commitWeblink = this._getBrowseCommitWeblink(weblinks, config); + return weblinks.filter(weblink => + !commitWeblink || + !commitWeblink.name || + weblink.name !== commitWeblink.name); + } + + _getFileWebLinks({repo, commit, file, options: {weblinks}}) { + return weblinks; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateSearchUrl(params) { + let offsetExpr = ''; + if (params.offset && params.offset > 0) { + offsetExpr = ',' + params.offset; } - _setParams(params) { - this._appElement().params = params; + if (params.query) { + return '/q/' + this.encodeURL(params.query, true) + offsetExpr; } - _appElement() { - // In Polymer2 you have to reach through the shadow root of the app - // element. This obviously breaks encapsulation. - // TODO(brohlfs): Make this more elegant, e.g. by exposing app-element - // explicitly in app, or by delegating to it. - return document.getElementById('app-element') || - document.getElementById('app').shadowRoot.getElementById( - 'app-element'); + const operators = []; + if (params.owner) { + operators.push('owner:' + this.encodeURL(params.owner, false)); + } + if (params.project) { + operators.push('project:' + this.encodeURL(params.project, false)); + } + if (params.branch) { + operators.push('branch:' + this.encodeURL(params.branch, false)); + } + if (params.topic) { + operators.push('topic:"' + this.encodeURL(params.topic, false) + '"'); + } + if (params.hashtag) { + operators.push('hashtag:"' + + this.encodeURL(params.hashtag.toLowerCase(), false) + '"'); + } + if (params.statuses) { + if (params.statuses.length === 1) { + operators.push( + 'status:' + this.encodeURL(params.statuses[0], false)); + } else if (params.statuses.length > 1) { + operators.push( + '(' + + params.statuses.map(s => `status:${this.encodeURL(s, false)}`) + .join(' OR ') + + ')'); + } } - _redirect(url) { - this._isRedirecting = true; - page.redirect(url); + return '/q/' + operators.join('+') + offsetExpr; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateChangeUrl(params) { + let range = this._getPatchRangeExpression(params); + if (range.length) { range = '/' + range; } + let suffix = `${range}`; + if (params.querystring) { + suffix += '?' + params.querystring; + } else if (params.edit) { + suffix += ',edit'; + } + if (params.messageHash) { + suffix += params.messageHash; + } + if (params.project) { + const encodedProject = this.encodeURL(params.project, true); + return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; + } else { + return `/c/${params.changeNum}${suffix}`; + } + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateDashboardUrl(params) { + const repoName = params.repo || params.project || null; + if (params.sections) { + // Custom dashboard. + const queryParams = this._sectionsToEncodedParams(params.sections, + repoName); + if (params.title) { + queryParams.push('title=' + encodeURIComponent(params.title)); + } + const user = params.user ? params.user : ''; + return `/dashboard/${user}?${queryParams.join('&')}`; + } else if (repoName) { + // Project dashboard. + const encodedRepo = this.encodeURL(repoName, true); + return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`; + } else { + // User dashboard. + return `/dashboard/${params.user || 'self'}`; + } + } + + /** + * @param {!Array<!{name: string, query: string}>} sections + * @param {string=} opt_repoName + * @return {!Array<string>} + */ + _sectionsToEncodedParams(sections, opt_repoName) { + return sections.map(section => { + // If there is a repo name provided, make sure to substitute it into the + // ${repo} (or legacy ${project}) query tokens. + const query = opt_repoName ? + section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) : + section.query; + return encodeURIComponent(section.name) + '=' + + encodeURIComponent(query); + }); + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateDiffOrEditUrl(params) { + let range = this._getPatchRangeExpression(params); + if (range.length) { range = '/' + range; } + + let suffix = `${range}/${this.encodeURL(params.path, true)}`; + + if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; } + + if (params.lineNum) { + suffix += '#'; + if (params.leftSide) { suffix += 'b'; } + suffix += params.lineNum; } - /** - * @param {!Object} params - * @return {string} - */ - _generateUrl(params) { - const base = this.getBaseUrl(); - let url = ''; - const Views = Gerrit.Nav.View; + if (params.project) { + const encodedProject = this.encodeURL(params.project, true); + return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; + } else { + return `/c/${params.changeNum}${suffix}`; + } + } - if (params.view === Views.SEARCH) { - url = this._generateSearchUrl(params); - } else if (params.view === Views.CHANGE) { - url = this._generateChangeUrl(params); - } else if (params.view === Views.DASHBOARD) { - url = this._generateDashboardUrl(params); - } else if (params.view === Views.DIFF || params.view === Views.EDIT) { - url = this._generateDiffOrEditUrl(params); - } else if (params.view === Views.GROUP) { - url = this._generateGroupUrl(params); - } else if (params.view === Views.REPO) { - url = this._generateRepoUrl(params); - } else if (params.view === Views.ROOT) { - url = '/'; - } else if (params.view === Views.SETTINGS) { - url = this._generateSettingsUrl(params); + /** + * @param {!Object} params + * @return {string} + */ + _generateGroupUrl(params) { + let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`; + if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) { + url += ',members'; + } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) { + url += ',audit-log'; + } + return url; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateRepoUrl(params) { + let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`; + if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) { + url += ',access'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) { + url += ',branches'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) { + url += ',tags'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) { + url += ',commands'; + } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) { + url += ',dashboards'; + } + return url; + } + + /** + * @param {!Object} params + * @return {string} + */ + _generateSettingsUrl(params) { + return '/settings'; + } + + /** + * Given an object of parameters, potentially including a `patchNum` or a + * `basePatchNum` or both, return a string representation of that range. If + * no range is indicated in the params, the empty string is returned. + * + * @param {!Object} params + * @return {string} + */ + _getPatchRangeExpression(params) { + let range = ''; + if (params.patchNum) { range = '' + params.patchNum; } + if (params.basePatchNum) { range = params.basePatchNum + '..' + range; } + return range; + } + + /** + * Given a set of params without a project, gets the project from the rest + * API project lookup and then sets the app params. + * + * @param {?Object} params + */ + _normalizeLegacyRouteParams(params) { + if (!params.changeNum) { return Promise.resolve(); } + + return this.$.restAPI.getFromProjectLookup(params.changeNum) + .then(project => { + // Show a 404 and terminate if the lookup request failed. Attempting + // to redirect after failing to get the project loops infinitely. + if (!project) { + this._show404(); + return; + } + + params.project = project; + this._normalizePatchRangeParams(params); + this._redirect(this._generateUrl(params)); + }); + } + + /** + * Normalizes the params object, and determines if the URL needs to be + * modified to fit the proper schema. + * + * @param {*} params + * @return {boolean} whether or not the URL needs to be upgraded. + */ + _normalizePatchRangeParams(params) { + const hasBasePatchNum = params.basePatchNum !== null && + params.basePatchNum !== undefined; + const hasPatchNum = params.patchNum !== null && + params.patchNum !== undefined; + let needsRedirect = false; + + // Diffing a patch against itself is invalid, so if the base and revision + // patches are equal clear the base. + if (hasBasePatchNum && + this.patchNumEquals(params.basePatchNum, params.patchNum)) { + needsRedirect = true; + params.basePatchNum = null; + } else if (hasBasePatchNum && !hasPatchNum) { + // Regexes set basePatchNum instead of patchNum when only one is + // specified. Redirect is not needed in this case. + params.patchNum = params.basePatchNum; + params.basePatchNum = null; + } + return needsRedirect; + } + + /** + * Redirect the user to login using the given return-URL for redirection + * after authentication success. + * + * @param {string} returnUrl + */ + _redirectToLogin(returnUrl) { + const basePath = this.getBaseUrl() || ''; + page( + '/login/' + encodeURIComponent(returnUrl.substring(basePath.length))); + } + + /** + * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c" + * is parsed to have a hash of "b" rather than "b#c". Instead, this method + * parses hashes correctly. Will return an empty string if there is no hash. + * + * @param {!string} canonicalPath + * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c"). + */ + _getHashFromCanonicalPath(canonicalPath) { + return canonicalPath.split('#').slice(1) + .join('#'); + } + + _parseLineAddress(hash) { + const match = hash.match(LINE_ADDRESS_PATTERN); + if (!match) { return null; } + return { + leftSide: !!match[1], + lineNum: parseInt(match[2], 10), + }; + } + + /** + * Check to see if the user is logged in and return a promise that only + * resolves if the user is logged in. If the user us not logged in, the + * promise is rejected and the page is redirected to the login flow. + * + * @param {!Object} data The parsed route data. + * @return {!Promise<!Object>} A promise yielding the original route data + * (if it resolves). + */ + _redirectIfNotLoggedIn(data) { + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + return Promise.resolve(); } else { - throw new Error('Can\'t generate'); + this._redirectToLogin(data.canonicalPath); + return Promise.reject(new Error()); } + }); + } - return base + url; + /** Page.js middleware that warms the REST API's logged-in cache line. */ + _loadUserMiddleware(ctx, next) { + this.$.restAPI.getLoggedIn().then(() => { next(); }); + } + + /** + * Map a route to a method on the router. + * + * @param {!string|!RegExp} pattern The page.js pattern for the route. + * @param {!string} handlerName The method name for the handler. If the + * route is matched, the handler will be executed with `this` referring + * to the component. Its return value will be discarded so that it does + * not interfere with page.js. + * @param {?boolean=} opt_authRedirect If true, then auth is checked before + * executing the handler. If the user is not logged in, it will redirect + * to the login flow and the handler will not be executed. The login + * redirect specifies the matched URL to be used after successfull auth. + */ + _mapRoute(pattern, handlerName, opt_authRedirect) { + if (!this[handlerName]) { + console.error('Attempted to map route to unknown method: ', + handlerName); + return; + } + page(pattern, this._loadUserMiddleware.bind(this), data => { + this.$.reporting.locationChanged(handlerName); + const promise = opt_authRedirect ? + this._redirectIfNotLoggedIn(data) : Promise.resolve(); + promise.then(() => { this[handlerName](data); }); + }); + } + + _startRouter() { + const base = this.getBaseUrl(); + if (base) { + page.base(base); } - _generateWeblinks(params) { - const type = params.type; - switch (type) { - case Gerrit.Nav.WeblinkType.FILE: - return this._getFileWebLinks(params); - case Gerrit.Nav.WeblinkType.CHANGE: - return this._getChangeWeblinks(params); - case Gerrit.Nav.WeblinkType.PATCHSET: - return this._getPatchSetWeblink(params); - default: - console.warn(`Unsupported weblink ${type}!`); + Gerrit.Nav.setup( + url => { page.show(url); }, + this._generateUrl.bind(this), + params => this._generateWeblinks(params), + x => x + ); + + page.exit('*', (ctx, next) => { + if (!this._isRedirecting) { + this.$.reporting.beforeLocationChanged(); } - } + this._isRedirecting = false; + this._isInitialLoad = false; + next(); + }); - _getPatchSetWeblink(params) { - const {commit, options} = params; - const {weblinks, config} = options || {}; - const name = commit && commit.slice(0, 7); - const weblink = this._getBrowseCommitWeblink(weblinks, config); - if (!weblink || !weblink.url) { - return {name}; - } else { - return {name, url: weblink.url}; - } - } + // Middleware + page((ctx, next) => { + document.body.scrollTop = 0; - _firstCodeBrowserWeblink(weblinks) { - // This is an ordered whitelist of web link types that provide direct - // links to the commit in the url property. - const codeBrowserLinks = ['gitiles', 'browse', 'gitweb']; - for (let i = 0; i < codeBrowserLinks.length; i++) { - const weblink = - weblinks.find(weblink => weblink.name === codeBrowserLinks[i]); - if (weblink) { return weblink; } - } - return null; - } - - _getBrowseCommitWeblink(weblinks, config) { - if (!weblinks) { return null; } - let weblink; - // Use primary weblink if configured and exists. - if (config && config.gerrit && config.gerrit.primary_weblink_name) { - weblink = weblinks.find( - weblink => weblink.name === config.gerrit.primary_weblink_name - ); - } - if (!weblink) { - weblink = this._firstCodeBrowserWeblink(weblinks); - } - if (!weblink) { return null; } - return weblink; - } - - _getChangeWeblinks({repo, commit, options: {weblinks, config}}) { - if (!weblinks || !weblinks.length) return []; - const commitWeblink = this._getBrowseCommitWeblink(weblinks, config); - return weblinks.filter(weblink => - !commitWeblink || - !commitWeblink.name || - weblink.name !== commitWeblink.name); - } - - _getFileWebLinks({repo, commit, file, options: {weblinks}}) { - return weblinks; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateSearchUrl(params) { - let offsetExpr = ''; - if (params.offset && params.offset > 0) { - offsetExpr = ',' + params.offset; - } - - if (params.query) { - return '/q/' + this.encodeURL(params.query, true) + offsetExpr; - } - - const operators = []; - if (params.owner) { - operators.push('owner:' + this.encodeURL(params.owner, false)); - } - if (params.project) { - operators.push('project:' + this.encodeURL(params.project, false)); - } - if (params.branch) { - operators.push('branch:' + this.encodeURL(params.branch, false)); - } - if (params.topic) { - operators.push('topic:"' + this.encodeURL(params.topic, false) + '"'); - } - if (params.hashtag) { - operators.push('hashtag:"' + - this.encodeURL(params.hashtag.toLowerCase(), false) + '"'); - } - if (params.statuses) { - if (params.statuses.length === 1) { - operators.push( - 'status:' + this.encodeURL(params.statuses[0], false)); - } else if (params.statuses.length > 1) { - operators.push( - '(' + - params.statuses.map(s => `status:${this.encodeURL(s, false)}`) - .join(' OR ') + - ')'); - } - } - - return '/q/' + operators.join('+') + offsetExpr; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateChangeUrl(params) { - let range = this._getPatchRangeExpression(params); - if (range.length) { range = '/' + range; } - let suffix = `${range}`; - if (params.querystring) { - suffix += '?' + params.querystring; - } else if (params.edit) { - suffix += ',edit'; - } - if (params.messageHash) { - suffix += params.messageHash; - } - if (params.project) { - const encodedProject = this.encodeURL(params.project, true); - return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; - } else { - return `/c/${params.changeNum}${suffix}`; - } - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateDashboardUrl(params) { - const repoName = params.repo || params.project || null; - if (params.sections) { - // Custom dashboard. - const queryParams = this._sectionsToEncodedParams(params.sections, - repoName); - if (params.title) { - queryParams.push('title=' + encodeURIComponent(params.title)); - } - const user = params.user ? params.user : ''; - return `/dashboard/${user}?${queryParams.join('&')}`; - } else if (repoName) { - // Project dashboard. - const encodedRepo = this.encodeURL(repoName, true); - return `/p/${encodedRepo}/+/dashboard/${params.dashboard}`; - } else { - // User dashboard. - return `/dashboard/${params.user || 'self'}`; - } - } - - /** - * @param {!Array<!{name: string, query: string}>} sections - * @param {string=} opt_repoName - * @return {!Array<string>} - */ - _sectionsToEncodedParams(sections, opt_repoName) { - return sections.map(section => { - // If there is a repo name provided, make sure to substitute it into the - // ${repo} (or legacy ${project}) query tokens. - const query = opt_repoName ? - section.query.replace(REPO_TOKEN_PATTERN, opt_repoName) : - section.query; - return encodeURIComponent(section.name) + '=' + - encodeURIComponent(query); - }); - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateDiffOrEditUrl(params) { - let range = this._getPatchRangeExpression(params); - if (range.length) { range = '/' + range; } - - let suffix = `${range}/${this.encodeURL(params.path, true)}`; - - if (params.view === Gerrit.Nav.View.EDIT) { suffix += ',edit'; } - - if (params.lineNum) { - suffix += '#'; - if (params.leftSide) { suffix += 'b'; } - suffix += params.lineNum; - } - - if (params.project) { - const encodedProject = this.encodeURL(params.project, true); - return `/c/${encodedProject}/+/${params.changeNum}${suffix}`; - } else { - return `/c/${params.changeNum}${suffix}`; - } - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateGroupUrl(params) { - let url = `/admin/groups/${this.encodeURL(params.groupId + '', true)}`; - if (params.detail === Gerrit.Nav.GroupDetailView.MEMBERS) { - url += ',members'; - } else if (params.detail === Gerrit.Nav.GroupDetailView.LOG) { - url += ',audit-log'; - } - return url; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateRepoUrl(params) { - let url = `/admin/repos/${this.encodeURL(params.repoName + '', true)}`; - if (params.detail === Gerrit.Nav.RepoDetailView.ACCESS) { - url += ',access'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.BRANCHES) { - url += ',branches'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.TAGS) { - url += ',tags'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.COMMANDS) { - url += ',commands'; - } else if (params.detail === Gerrit.Nav.RepoDetailView.DASHBOARDS) { - url += ',dashboards'; - } - return url; - } - - /** - * @param {!Object} params - * @return {string} - */ - _generateSettingsUrl(params) { - return '/settings'; - } - - /** - * Given an object of parameters, potentially including a `patchNum` or a - * `basePatchNum` or both, return a string representation of that range. If - * no range is indicated in the params, the empty string is returned. - * - * @param {!Object} params - * @return {string} - */ - _getPatchRangeExpression(params) { - let range = ''; - if (params.patchNum) { range = '' + params.patchNum; } - if (params.basePatchNum) { range = params.basePatchNum + '..' + range; } - return range; - } - - /** - * Given a set of params without a project, gets the project from the rest - * API project lookup and then sets the app params. - * - * @param {?Object} params - */ - _normalizeLegacyRouteParams(params) { - if (!params.changeNum) { return Promise.resolve(); } - - return this.$.restAPI.getFromProjectLookup(params.changeNum) - .then(project => { - // Show a 404 and terminate if the lookup request failed. Attempting - // to redirect after failing to get the project loops infinitely. - if (!project) { - this._show404(); - return; - } - - params.project = project; - this._normalizePatchRangeParams(params); - this._redirect(this._generateUrl(params)); - }); - } - - /** - * Normalizes the params object, and determines if the URL needs to be - * modified to fit the proper schema. - * - * @param {*} params - * @return {boolean} whether or not the URL needs to be upgraded. - */ - _normalizePatchRangeParams(params) { - const hasBasePatchNum = params.basePatchNum !== null && - params.basePatchNum !== undefined; - const hasPatchNum = params.patchNum !== null && - params.patchNum !== undefined; - let needsRedirect = false; - - // Diffing a patch against itself is invalid, so if the base and revision - // patches are equal clear the base. - if (hasBasePatchNum && - this.patchNumEquals(params.basePatchNum, params.patchNum)) { - needsRedirect = true; - params.basePatchNum = null; - } else if (hasBasePatchNum && !hasPatchNum) { - // Regexes set basePatchNum instead of patchNum when only one is - // specified. Redirect is not needed in this case. - params.patchNum = params.basePatchNum; - params.basePatchNum = null; - } - return needsRedirect; - } - - /** - * Redirect the user to login using the given return-URL for redirection - * after authentication success. - * - * @param {string} returnUrl - */ - _redirectToLogin(returnUrl) { - const basePath = this.getBaseUrl() || ''; - page( - '/login/' + encodeURIComponent(returnUrl.substring(basePath.length))); - } - - /** - * Hashes parsed by page.js exclude "inner" hashes, so a URL like "/a#b#c" - * is parsed to have a hash of "b" rather than "b#c". Instead, this method - * parses hashes correctly. Will return an empty string if there is no hash. - * - * @param {!string} canonicalPath - * @return {!string} Everything after the first '#' ("a#b#c" -> "b#c"). - */ - _getHashFromCanonicalPath(canonicalPath) { - return canonicalPath.split('#').slice(1) - .join('#'); - } - - _parseLineAddress(hash) { - const match = hash.match(LINE_ADDRESS_PATTERN); - if (!match) { return null; } - return { - leftSide: !!match[1], - lineNum: parseInt(match[2], 10), - }; - } - - /** - * Check to see if the user is logged in and return a promise that only - * resolves if the user is logged in. If the user us not logged in, the - * promise is rejected and the page is redirected to the login flow. - * - * @param {!Object} data The parsed route data. - * @return {!Promise<!Object>} A promise yielding the original route data - * (if it resolves). - */ - _redirectIfNotLoggedIn(data) { - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (loggedIn) { - return Promise.resolve(); - } else { - this._redirectToLogin(data.canonicalPath); - return Promise.reject(new Error()); - } - }); - } - - /** Page.js middleware that warms the REST API's logged-in cache line. */ - _loadUserMiddleware(ctx, next) { - this.$.restAPI.getLoggedIn().then(() => { next(); }); - } - - /** - * Map a route to a method on the router. - * - * @param {!string|!RegExp} pattern The page.js pattern for the route. - * @param {!string} handlerName The method name for the handler. If the - * route is matched, the handler will be executed with `this` referring - * to the component. Its return value will be discarded so that it does - * not interfere with page.js. - * @param {?boolean=} opt_authRedirect If true, then auth is checked before - * executing the handler. If the user is not logged in, it will redirect - * to the login flow and the handler will not be executed. The login - * redirect specifies the matched URL to be used after successfull auth. - */ - _mapRoute(pattern, handlerName, opt_authRedirect) { - if (!this[handlerName]) { - console.error('Attempted to map route to unknown method: ', - handlerName); + if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) { + // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen + // This is needed to allow plugins to add basic #/x/ screen links to + // any location. + this._redirect(ctx.hash); return; } - page(pattern, this._loadUserMiddleware.bind(this), data => { - this.$.reporting.locationChanged(handlerName); - const promise = opt_authRedirect ? - this._redirectIfNotLoggedIn(data) : Promise.resolve(); - promise.then(() => { this[handlerName](data); }); - }); - } - _startRouter() { + // Fire asynchronously so that the URL is changed by the time the event + // is processed. + this.async(() => { + this.fire('location-change', { + hash: window.location.hash, + pathname: window.location.pathname, + }); + }, 1); + next(); + }); + + this._mapRoute(RoutePattern.ROOT, '_handleRootRoute'); + + this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute'); + + this._mapRoute(RoutePattern.CUSTOM_DASHBOARD, + '_handleCustomDashboardRoute'); + + this._mapRoute(RoutePattern.PROJECT_DASHBOARD, + '_handleProjectDashboardRoute'); + + this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true); + + this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute', + true); + + this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute', + true); + + this._mapRoute(RoutePattern.GROUP_LIST_OFFSET, + '_handleGroupListOffsetRoute', true); + + this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET, + '_handleGroupListFilterOffsetRoute', true); + + this._mapRoute(RoutePattern.GROUP_LIST_FILTER, + '_handleGroupListFilterRoute', true); + + this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute', + true); + + this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true); + + this._mapRoute(RoutePattern.PROJECT_OLD, + '_handleProjectsOldRoute'); + + this._mapRoute(RoutePattern.REPO_COMMANDS, + '_handleRepoCommandsRoute', true); + + this._mapRoute(RoutePattern.REPO_ACCESS, + '_handleRepoAccessRoute'); + + this._mapRoute(RoutePattern.REPO_DASHBOARDS, + '_handleRepoDashboardsRoute'); + + this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET, + '_handleBranchListOffsetRoute'); + + this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET, + '_handleBranchListFilterOffsetRoute'); + + this._mapRoute(RoutePattern.BRANCH_LIST_FILTER, + '_handleBranchListFilterRoute'); + + this._mapRoute(RoutePattern.TAG_LIST_OFFSET, + '_handleTagListOffsetRoute'); + + this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET, + '_handleTagListFilterOffsetRoute'); + + this._mapRoute(RoutePattern.TAG_LIST_FILTER, + '_handleTagListFilterRoute'); + + this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP, + '_handleCreateGroupRoute', true); + + this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT, + '_handleCreateProjectRoute', true); + + this._mapRoute(RoutePattern.REPO_LIST_OFFSET, + '_handleRepoListOffsetRoute'); + + this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET, + '_handleRepoListFilterOffsetRoute'); + + this._mapRoute(RoutePattern.REPO_LIST_FILTER, + '_handleRepoListFilterRoute'); + + this._mapRoute(RoutePattern.REPO, '_handleRepoRoute'); + + this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute'); + + this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET, + '_handlePluginListOffsetRoute', true); + + this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET, + '_handlePluginListFilterOffsetRoute', true); + + this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER, + '_handlePluginListFilterRoute', true); + + this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true); + + this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX, + '_handleQueryLegacySuffixRoute'); + + this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute'); + + this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum'); + + this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY, + '_handleChangeNumberLegacyRoute'); + + this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true); + + this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true); + + this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute'); + + this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute'); + + this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute'); + + this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute'); + + this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true); + + this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute', + true); + + this._mapRoute(RoutePattern.SETTINGS_LEGACY, + '_handleSettingsLegacyRoute', true); + + this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true); + + this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute'); + + this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute'); + + this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS, + '_handleImproperlyEncodedPlusRoute'); + + this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen'); + + this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER, + '_handleDocumentationSearchRoute'); + + // redirects /Documentation/q/* to /Documentation/q/filter:* + this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH, + '_handleDocumentationSearchRedirectRoute'); + + // Makes sure /Documentation/* links work (doin't return 404) + this._mapRoute(RoutePattern.DOCUMENTATION, + '_handleDocumentationRedirectRoute'); + + // Note: this route should appear last so it only catches URLs unmatched + // by other patterns. + this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute'); + + page.start(); + } + + /** + * @param {!Object} data + * @return {Promise|null} if handling the route involves asynchrony, then a + * promise is returned. Otherwise, synchronous handling returns null. + */ + _handleRootRoute(data) { + if (data.querystring.match(/^closeAfterLogin/)) { + // Close child window on redirect after login. + window.close(); + return null; + } + let hash = this._getHashFromCanonicalPath(data.canonicalPath); + // For backward compatibility with GWT links. + if (hash) { + // In certain login flows the server may redirect to a hash without + // a leading slash, which page.js doesn't handle correctly. + if (hash[0] !== '/') { + hash = '/' + hash; + } + if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) { + // Path decodes all '+' to ' ' -- this breaks project-based URLs. + // See Issue 6888. + hash = hash.replace('/ /', '/+/'); + } const base = this.getBaseUrl(); - if (base) { - page.base(base); + let newUrl = base + hash; + if (hash.startsWith('/VE/')) { + newUrl = base + '/settings' + hash; } - - Gerrit.Nav.setup( - url => { page.show(url); }, - this._generateUrl.bind(this), - params => this._generateWeblinks(params), - x => x - ); - - page.exit('*', (ctx, next) => { - if (!this._isRedirecting) { - this.$.reporting.beforeLocationChanged(); - } - this._isRedirecting = false; - this._isInitialLoad = false; - next(); - }); - - // Middleware - page((ctx, next) => { - document.body.scrollTop = 0; - - if (ctx.hash.match(RoutePattern.PLUGIN_SCREEN)) { - // Redirect all urls using hash #/x/plugin/screen to /x/plugin/screen - // This is needed to allow plugins to add basic #/x/ screen links to - // any location. - this._redirect(ctx.hash); - return; - } - - // Fire asynchronously so that the URL is changed by the time the event - // is processed. - this.async(() => { - this.fire('location-change', { - hash: window.location.hash, - pathname: window.location.pathname, - }); - }, 1); - next(); - }); - - this._mapRoute(RoutePattern.ROOT, '_handleRootRoute'); - - this._mapRoute(RoutePattern.DASHBOARD, '_handleDashboardRoute'); - - this._mapRoute(RoutePattern.CUSTOM_DASHBOARD, - '_handleCustomDashboardRoute'); - - this._mapRoute(RoutePattern.PROJECT_DASHBOARD, - '_handleProjectDashboardRoute'); - - this._mapRoute(RoutePattern.GROUP_INFO, '_handleGroupInfoRoute', true); - - this._mapRoute(RoutePattern.GROUP_AUDIT_LOG, '_handleGroupAuditLogRoute', - true); - - this._mapRoute(RoutePattern.GROUP_MEMBERS, '_handleGroupMembersRoute', - true); - - this._mapRoute(RoutePattern.GROUP_LIST_OFFSET, - '_handleGroupListOffsetRoute', true); - - this._mapRoute(RoutePattern.GROUP_LIST_FILTER_OFFSET, - '_handleGroupListFilterOffsetRoute', true); - - this._mapRoute(RoutePattern.GROUP_LIST_FILTER, - '_handleGroupListFilterRoute', true); - - this._mapRoute(RoutePattern.GROUP_SELF, '_handleGroupSelfRedirectRoute', - true); - - this._mapRoute(RoutePattern.GROUP, '_handleGroupRoute', true); - - this._mapRoute(RoutePattern.PROJECT_OLD, - '_handleProjectsOldRoute'); - - this._mapRoute(RoutePattern.REPO_COMMANDS, - '_handleRepoCommandsRoute', true); - - this._mapRoute(RoutePattern.REPO_ACCESS, - '_handleRepoAccessRoute'); - - this._mapRoute(RoutePattern.REPO_DASHBOARDS, - '_handleRepoDashboardsRoute'); - - this._mapRoute(RoutePattern.BRANCH_LIST_OFFSET, - '_handleBranchListOffsetRoute'); - - this._mapRoute(RoutePattern.BRANCH_LIST_FILTER_OFFSET, - '_handleBranchListFilterOffsetRoute'); - - this._mapRoute(RoutePattern.BRANCH_LIST_FILTER, - '_handleBranchListFilterRoute'); - - this._mapRoute(RoutePattern.TAG_LIST_OFFSET, - '_handleTagListOffsetRoute'); - - this._mapRoute(RoutePattern.TAG_LIST_FILTER_OFFSET, - '_handleTagListFilterOffsetRoute'); - - this._mapRoute(RoutePattern.TAG_LIST_FILTER, - '_handleTagListFilterRoute'); - - this._mapRoute(RoutePattern.LEGACY_CREATE_GROUP, - '_handleCreateGroupRoute', true); - - this._mapRoute(RoutePattern.LEGACY_CREATE_PROJECT, - '_handleCreateProjectRoute', true); - - this._mapRoute(RoutePattern.REPO_LIST_OFFSET, - '_handleRepoListOffsetRoute'); - - this._mapRoute(RoutePattern.REPO_LIST_FILTER_OFFSET, - '_handleRepoListFilterOffsetRoute'); - - this._mapRoute(RoutePattern.REPO_LIST_FILTER, - '_handleRepoListFilterRoute'); - - this._mapRoute(RoutePattern.REPO, '_handleRepoRoute'); - - this._mapRoute(RoutePattern.PLUGINS, '_handlePassThroughRoute'); - - this._mapRoute(RoutePattern.PLUGIN_LIST_OFFSET, - '_handlePluginListOffsetRoute', true); - - this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER_OFFSET, - '_handlePluginListFilterOffsetRoute', true); - - this._mapRoute(RoutePattern.PLUGIN_LIST_FILTER, - '_handlePluginListFilterRoute', true); - - this._mapRoute(RoutePattern.PLUGIN_LIST, '_handlePluginListRoute', true); - - this._mapRoute(RoutePattern.QUERY_LEGACY_SUFFIX, - '_handleQueryLegacySuffixRoute'); - - this._mapRoute(RoutePattern.QUERY, '_handleQueryRoute'); - - this._mapRoute(RoutePattern.DIFF_LEGACY_LINENUM, '_handleLegacyLinenum'); - - this._mapRoute(RoutePattern.CHANGE_NUMBER_LEGACY, - '_handleChangeNumberLegacyRoute'); - - this._mapRoute(RoutePattern.DIFF_EDIT, '_handleDiffEditRoute', true); - - this._mapRoute(RoutePattern.CHANGE_EDIT, '_handleChangeEditRoute', true); - - this._mapRoute(RoutePattern.DIFF, '_handleDiffRoute'); - - this._mapRoute(RoutePattern.CHANGE, '_handleChangeRoute'); - - this._mapRoute(RoutePattern.CHANGE_LEGACY, '_handleChangeLegacyRoute'); - - this._mapRoute(RoutePattern.DIFF_LEGACY, '_handleDiffLegacyRoute'); - - this._mapRoute(RoutePattern.AGREEMENTS, '_handleAgreementsRoute', true); - - this._mapRoute(RoutePattern.NEW_AGREEMENTS, '_handleNewAgreementsRoute', - true); - - this._mapRoute(RoutePattern.SETTINGS_LEGACY, - '_handleSettingsLegacyRoute', true); - - this._mapRoute(RoutePattern.SETTINGS, '_handleSettingsRoute', true); - - this._mapRoute(RoutePattern.REGISTER, '_handleRegisterRoute'); - - this._mapRoute(RoutePattern.LOG_IN_OR_OUT, '_handlePassThroughRoute'); - - this._mapRoute(RoutePattern.IMPROPERLY_ENCODED_PLUS, - '_handleImproperlyEncodedPlusRoute'); - - this._mapRoute(RoutePattern.PLUGIN_SCREEN, '_handlePluginScreen'); - - this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH_FILTER, - '_handleDocumentationSearchRoute'); - - // redirects /Documentation/q/* to /Documentation/q/filter:* - this._mapRoute(RoutePattern.DOCUMENTATION_SEARCH, - '_handleDocumentationSearchRedirectRoute'); - - // Makes sure /Documentation/* links work (doin't return 404) - this._mapRoute(RoutePattern.DOCUMENTATION, - '_handleDocumentationRedirectRoute'); - - // Note: this route should appear last so it only catches URLs unmatched - // by other patterns. - this._mapRoute(RoutePattern.DEFAULT, '_handleDefaultRoute'); - - page.start(); + this._redirect(newUrl); + return null; } + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (loggedIn) { + this._redirect('/dashboard/self'); + } else { + this._redirect('/q/status:open'); + } + }); + } - /** - * @param {!Object} data - * @return {Promise|null} if handling the route involves asynchrony, then a - * promise is returned. Otherwise, synchronous handling returns null. - */ - _handleRootRoute(data) { - if (data.querystring.match(/^closeAfterLogin/)) { - // Close child window on redirect after login. - window.close(); - return null; + /** + * Decode an application/x-www-form-urlencoded string. + * + * @param {string} qs The application/x-www-form-urlencoded string. + * @return {string} The decoded string. + */ + _decodeQueryString(qs) { + return decodeURIComponent(qs.replace(PLUS_PATTERN, ' ')); + } + + /** + * Parse a query string (e.g. window.location.search) into an array of + * name/value pairs. + * + * @param {string} qs The application/x-www-form-urlencoded query string. + * @return {!Array<!Array<string>>} An array of name/value pairs, where each + * element is a 2-element array. + */ + _parseQueryString(qs) { + qs = qs.replace(QUESTION_PATTERN, ''); + if (!qs) { + return []; + } + const params = []; + qs.split('&').forEach(param => { + const idx = param.indexOf('='); + let name; + let value; + if (idx < 0) { + name = this._decodeQueryString(param); + value = ''; + } else { + name = this._decodeQueryString(param.substring(0, idx)); + value = this._decodeQueryString(param.substring(idx + 1)); } - let hash = this._getHashFromCanonicalPath(data.canonicalPath); - // For backward compatibility with GWT links. - if (hash) { - // In certain login flows the server may redirect to a hash without - // a leading slash, which page.js doesn't handle correctly. - if (hash[0] !== '/') { - hash = '/' + hash; - } - if (hash.includes('/ /') && data.canonicalPath.includes('/+/')) { - // Path decodes all '+' to ' ' -- this breaks project-based URLs. - // See Issue 6888. - hash = hash.replace('/ /', '/+/'); - } - const base = this.getBaseUrl(); - let newUrl = base + hash; - if (hash.startsWith('/VE/')) { - newUrl = base + '/settings' + hash; - } - this._redirect(newUrl); - return null; + if (name) { + params.push([name, value]); } - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (loggedIn) { - this._redirect('/dashboard/self'); + }); + return params; + } + + /** + * Handle dashboard routes. These may be user, or project dashboards. + * + * @param {!Object} data The parsed route data. + */ + _handleDashboardRoute(data) { + // User dashboard. We require viewing user to be logged in, else we + // redirect to login for self dashboard or simple owner search for + // other user dashboard. + return this.$.restAPI.getLoggedIn().then(loggedIn => { + if (!loggedIn) { + if (data.params[0].toLowerCase() === 'self') { + this._redirectToLogin(data.canonicalPath); } else { - this._redirect('/q/status:open'); + this._redirect('/q/owner:' + encodeURIComponent(data.params[0])); } - }); - } - - /** - * Decode an application/x-www-form-urlencoded string. - * - * @param {string} qs The application/x-www-form-urlencoded string. - * @return {string} The decoded string. - */ - _decodeQueryString(qs) { - return decodeURIComponent(qs.replace(PLUS_PATTERN, ' ')); - } - - /** - * Parse a query string (e.g. window.location.search) into an array of - * name/value pairs. - * - * @param {string} qs The application/x-www-form-urlencoded query string. - * @return {!Array<!Array<string>>} An array of name/value pairs, where each - * element is a 2-element array. - */ - _parseQueryString(qs) { - qs = qs.replace(QUESTION_PATTERN, ''); - if (!qs) { - return []; - } - const params = []; - qs.split('&').forEach(param => { - const idx = param.indexOf('='); - let name; - let value; - if (idx < 0) { - name = this._decodeQueryString(param); - value = ''; - } else { - name = this._decodeQueryString(param.substring(0, idx)); - value = this._decodeQueryString(param.substring(idx + 1)); - } - if (name) { - params.push([name, value]); - } - }); - return params; - } - - /** - * Handle dashboard routes. These may be user, or project dashboards. - * - * @param {!Object} data The parsed route data. - */ - _handleDashboardRoute(data) { - // User dashboard. We require viewing user to be logged in, else we - // redirect to login for self dashboard or simple owner search for - // other user dashboard. - return this.$.restAPI.getLoggedIn().then(loggedIn => { - if (!loggedIn) { - if (data.params[0].toLowerCase() === 'self') { - this._redirectToLogin(data.canonicalPath); - } else { - this._redirect('/q/owner:' + encodeURIComponent(data.params[0])); - } - } else { - this._setParams({ - view: Gerrit.Nav.View.DASHBOARD, - user: data.params[0], - }); - } - }); - } - - /** - * Handle custom dashboard routes. - * - * @param {!Object} data The parsed route data. - * @param {string=} opt_qs Optional query string associated with the route. - * If not given, window.location.search is used. (Used by tests). - */ - _handleCustomDashboardRoute(data, opt_qs) { - // opt_qs may be provided by a test, and it may have a falsy value - const qs = opt_qs !== undefined ? opt_qs : window.location.search; - const queryParams = this._parseQueryString(qs); - let title = 'Custom Dashboard'; - const titleParam = queryParams.find( - elem => elem[0].toLowerCase() === 'title'); - if (titleParam) { - title = titleParam[1]; - } - // Dashboards support a foreach param which adds a base query to any - // additional query. - const forEachParam = queryParams.find( - elem => elem[0].toLowerCase() === 'foreach'); - let forEachQuery = null; - if (forEachParam) { - forEachQuery = forEachParam[1]; - } - const sectionParams = queryParams.filter( - elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' && - elem[0].toLowerCase() !== 'foreach'); - const sections = sectionParams.map(elem => { - const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1]; - return { - name: elem[0], - query, - }; - }); - - if (sections.length > 0) { - // Custom dashboard view. + } else { this._setParams({ view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections, - title, + user: data.params[0], }); - return Promise.resolve(); } + }); + } - // Redirect /dashboard/ -> /dashboard/self. - this._redirect('/dashboard/self'); + /** + * Handle custom dashboard routes. + * + * @param {!Object} data The parsed route data. + * @param {string=} opt_qs Optional query string associated with the route. + * If not given, window.location.search is used. (Used by tests). + */ + _handleCustomDashboardRoute(data, opt_qs) { + // opt_qs may be provided by a test, and it may have a falsy value + const qs = opt_qs !== undefined ? opt_qs : window.location.search; + const queryParams = this._parseQueryString(qs); + let title = 'Custom Dashboard'; + const titleParam = queryParams.find( + elem => elem[0].toLowerCase() === 'title'); + if (titleParam) { + title = titleParam[1]; + } + // Dashboards support a foreach param which adds a base query to any + // additional query. + const forEachParam = queryParams.find( + elem => elem[0].toLowerCase() === 'foreach'); + let forEachQuery = null; + if (forEachParam) { + forEachQuery = forEachParam[1]; + } + const sectionParams = queryParams.filter( + elem => elem[0] && elem[1] && elem[0].toLowerCase() !== 'title' && + elem[0].toLowerCase() !== 'foreach'); + const sections = sectionParams.map(elem => { + const query = forEachQuery ? `${forEachQuery} ${elem[1]}` : elem[1]; + return { + name: elem[0], + query, + }; + }); + + if (sections.length > 0) { + // Custom dashboard view. + this._setParams({ + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections, + title, + }); return Promise.resolve(); } - _handleProjectDashboardRoute(data) { - const project = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.DASHBOARD, - project, - dashboard: decodeURIComponent(data.params[1]), - }); - this.$.reporting.setRepoName(project); - } + // Redirect /dashboard/ -> /dashboard/self. + this._redirect('/dashboard/self'); + return Promise.resolve(); + } - _handleGroupInfoRoute(data) { - this._redirect('/admin/groups/' + encodeURIComponent(data.params[0])); - } + _handleProjectDashboardRoute(data) { + const project = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.DASHBOARD, + project, + dashboard: decodeURIComponent(data.params[1]), + }); + this.$.reporting.setRepoName(project); + } - _handleGroupSelfRedirectRoute(data) { - this._redirect('/settings/#Groups'); - } + _handleGroupInfoRoute(data) { + this._redirect('/admin/groups/' + encodeURIComponent(data.params[0])); + } - _handleGroupRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.GROUP, - groupId: data.params[0], - }); - } + _handleGroupSelfRedirectRoute(data) { + this._redirect('/settings/#Groups'); + } - _handleGroupAuditLogRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.GROUP, - detail: Gerrit.Nav.GroupDetailView.LOG, - groupId: data.params[0], - }); - } + _handleGroupRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.GROUP, + groupId: data.params[0], + }); + } - _handleGroupMembersRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.GROUP, - detail: Gerrit.Nav.GroupDetailView.MEMBERS, - groupId: data.params[0], - }); - } + _handleGroupAuditLogRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.GROUP, + detail: Gerrit.Nav.GroupDetailView.LOG, + groupId: data.params[0], + }); + } - _handleGroupListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - offset: data.params[1] || 0, - filter: null, - openCreateModal: data.hash === 'create', - }); - } + _handleGroupMembersRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.GROUP, + detail: Gerrit.Nav.GroupDetailView.MEMBERS, + groupId: data.params[0], + }); + } - _handleGroupListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - offset: data.params.offset, - filter: data.params.filter, - }); - } + _handleGroupListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: data.params[1] || 0, + filter: null, + openCreateModal: data.hash === 'create', + }); + } - _handleGroupListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - filter: data.params.filter || null, - }); - } + _handleGroupListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: data.params.offset, + filter: data.params.filter, + }); + } - _handleProjectsOldRoute(data) { - let params = ''; - if (data.params[1]) { - params = encodeURIComponent(data.params[1]); - if (data.params[1].includes(',')) { - params = - encodeURIComponent(data.params[1]).replace('%2C', ','); - } - } + _handleGroupListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + filter: data.params.filter || null, + }); + } - this._redirect(`/admin/repos/${params}`); - } - - _handleRepoCommandsRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.COMMANDS, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handleRepoAccessRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handleRepoDashboardsRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handleBranchListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: data.params[0], - offset: data.params[2] || 0, - filter: null, - }); - } - - _handleBranchListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: data.params.repo, - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handleBranchListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: data.params.repo, - filter: data.params.filter || null, - }); - } - - _handleTagListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: data.params[0], - offset: data.params[2] || 0, - filter: null, - }); - } - - _handleTagListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: data.params.repo, - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handleTagListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: data.params.repo, - filter: data.params.filter || null, - }); - } - - _handleRepoListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: data.params[1] || 0, - filter: null, - openCreateModal: data.hash === 'create', - }); - } - - _handleRepoListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handleRepoListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - filter: data.params.filter || null, - }); - } - - _handleCreateProjectRoute(data) { - // Redirects the legacy route to the new route, which displays the project - // list with a hash 'create'. - this._redirect('/admin/repos#create'); - } - - _handleCreateGroupRoute(data) { - // Redirects the legacy route to the new route, which displays the group - // list with a hash 'create'. - this._redirect('/admin/groups#create'); - } - - _handleRepoRoute(data) { - const repo = data.params[0]; - this._setParams({ - view: Gerrit.Nav.View.REPO, - repo, - }); - this.$.reporting.setRepoName(repo); - } - - _handlePluginListOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: data.params[1] || 0, - filter: null, - }); - } - - _handlePluginListFilterOffsetRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: data.params.offset, - filter: data.params.filter, - }); - } - - _handlePluginListFilterRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - filter: data.params.filter || null, - }); - } - - _handlePluginListRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - }); - } - - _handleQueryRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.SEARCH, - query: data.params[0], - offset: data.params[2], - }); - } - - _handleQueryLegacySuffixRoute(ctx) { - this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, '')); - } - - _handleChangeNumberLegacyRoute(ctx) { - this._redirect('/c/' + encodeURIComponent(ctx.params[0])); - } - - _handleChangeRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - project: ctx.params[0], - changeNum: ctx.params[1], - basePatchNum: ctx.params[4], - patchNum: ctx.params[6], - view: Gerrit.Nav.View.CHANGE, - }; - - this.$.reporting.setRepoName(params.project); - this._redirectOrNavigate(params); - } - - _handleDiffRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - project: ctx.params[0], - changeNum: ctx.params[1], - basePatchNum: ctx.params[4], - patchNum: ctx.params[6], - path: ctx.params[8], - view: Gerrit.Nav.View.DIFF, - }; - - const address = this._parseLineAddress(ctx.hash); - if (address) { - params.leftSide = address.leftSide; - params.lineNum = address.lineNum; - } - this.$.reporting.setRepoName(params.project); - this._redirectOrNavigate(params); - } - - _handleChangeLegacyRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - changeNum: ctx.params[0], - basePatchNum: ctx.params[3], - patchNum: ctx.params[5], - view: Gerrit.Nav.View.CHANGE, - querystring: ctx.querystring, - }; - - this._normalizeLegacyRouteParams(params); - } - - _handleLegacyLinenum(ctx) { - this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1')); - } - - _handleDiffLegacyRoute(ctx) { - // Parameter order is based on the regex group number matched. - const params = { - changeNum: ctx.params[0], - basePatchNum: ctx.params[2], - patchNum: ctx.params[4], - path: ctx.params[5], - view: Gerrit.Nav.View.DIFF, - }; - - const address = this._parseLineAddress(ctx.hash); - if (address) { - params.leftSide = address.leftSide; - params.lineNum = address.lineNum; - } - - this._normalizeLegacyRouteParams(params); - } - - _handleDiffEditRoute(ctx) { - // Parameter order is based on the regex group number matched. - const project = ctx.params[0]; - this._redirectOrNavigate({ - project, - changeNum: ctx.params[1], - patchNum: ctx.params[2], - path: ctx.params[3], - lineNum: ctx.hash, - view: Gerrit.Nav.View.EDIT, - }); - this.$.reporting.setRepoName(project); - } - - _handleChangeEditRoute(ctx) { - // Parameter order is based on the regex group number matched. - const project = ctx.params[0]; - this._redirectOrNavigate({ - project, - changeNum: ctx.params[1], - patchNum: ctx.params[3], - view: Gerrit.Nav.View.CHANGE, - edit: true, - }); - this.$.reporting.setRepoName(project); - } - - /** - * Normalize the patch range params for a the change or diff view and - * redirect if URL upgrade is needed. - */ - _redirectOrNavigate(params) { - const needsRedirect = this._normalizePatchRangeParams(params); - if (needsRedirect) { - this._redirect(this._generateUrl(params)); - } else { - this._setParams(params); + _handleProjectsOldRoute(data) { + let params = ''; + if (data.params[1]) { + params = encodeURIComponent(data.params[1]); + if (data.params[1].includes(',')) { + params = + encodeURIComponent(data.params[1]).replace('%2C', ','); } } - _handleAgreementsRoute() { - this._redirect('/settings/#Agreements'); + this._redirect(`/admin/repos/${params}`); + } + + _handleRepoCommandsRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.COMMANDS, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handleRepoAccessRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handleRepoDashboardsRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.DASHBOARDS, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handleBranchListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: data.params[0], + offset: data.params[2] || 0, + filter: null, + }); + } + + _handleBranchListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: data.params.repo, + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handleBranchListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: data.params.repo, + filter: data.params.filter || null, + }); + } + + _handleTagListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: data.params[0], + offset: data.params[2] || 0, + filter: null, + }); + } + + _handleTagListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: data.params.repo, + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handleTagListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: data.params.repo, + filter: data.params.filter || null, + }); + } + + _handleRepoListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + offset: data.params[1] || 0, + filter: null, + openCreateModal: data.hash === 'create', + }); + } + + _handleRepoListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handleRepoListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-repo-list', + filter: data.params.filter || null, + }); + } + + _handleCreateProjectRoute(data) { + // Redirects the legacy route to the new route, which displays the project + // list with a hash 'create'. + this._redirect('/admin/repos#create'); + } + + _handleCreateGroupRoute(data) { + // Redirects the legacy route to the new route, which displays the group + // list with a hash 'create'. + this._redirect('/admin/groups#create'); + } + + _handleRepoRoute(data) { + const repo = data.params[0]; + this._setParams({ + view: Gerrit.Nav.View.REPO, + repo, + }); + this.$.reporting.setRepoName(repo); + } + + _handlePluginListOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: data.params[1] || 0, + filter: null, + }); + } + + _handlePluginListFilterOffsetRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: data.params.offset, + filter: data.params.filter, + }); + } + + _handlePluginListFilterRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + filter: data.params.filter || null, + }); + } + + _handlePluginListRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + }); + } + + _handleQueryRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.SEARCH, + query: data.params[0], + offset: data.params[2], + }); + } + + _handleQueryLegacySuffixRoute(ctx) { + this._redirect(ctx.path.replace(LEGACY_QUERY_SUFFIX_PATTERN, '')); + } + + _handleChangeNumberLegacyRoute(ctx) { + this._redirect('/c/' + encodeURIComponent(ctx.params[0])); + } + + _handleChangeRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + project: ctx.params[0], + changeNum: ctx.params[1], + basePatchNum: ctx.params[4], + patchNum: ctx.params[6], + view: Gerrit.Nav.View.CHANGE, + }; + + this.$.reporting.setRepoName(params.project); + this._redirectOrNavigate(params); + } + + _handleDiffRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + project: ctx.params[0], + changeNum: ctx.params[1], + basePatchNum: ctx.params[4], + patchNum: ctx.params[6], + path: ctx.params[8], + view: Gerrit.Nav.View.DIFF, + }; + + const address = this._parseLineAddress(ctx.hash); + if (address) { + params.leftSide = address.leftSide; + params.lineNum = address.lineNum; + } + this.$.reporting.setRepoName(params.project); + this._redirectOrNavigate(params); + } + + _handleChangeLegacyRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + changeNum: ctx.params[0], + basePatchNum: ctx.params[3], + patchNum: ctx.params[5], + view: Gerrit.Nav.View.CHANGE, + querystring: ctx.querystring, + }; + + this._normalizeLegacyRouteParams(params); + } + + _handleLegacyLinenum(ctx) { + this._redirect(ctx.path.replace(LEGACY_LINENUM_PATTERN, '#$1')); + } + + _handleDiffLegacyRoute(ctx) { + // Parameter order is based on the regex group number matched. + const params = { + changeNum: ctx.params[0], + basePatchNum: ctx.params[2], + patchNum: ctx.params[4], + path: ctx.params[5], + view: Gerrit.Nav.View.DIFF, + }; + + const address = this._parseLineAddress(ctx.hash); + if (address) { + params.leftSide = address.leftSide; + params.lineNum = address.lineNum; } - _handleNewAgreementsRoute(data) { - data.params.view = Gerrit.Nav.View.AGREEMENTS; - this._setParams(data.params); - } + this._normalizeLegacyRouteParams(params); + } - _handleSettingsLegacyRoute(data) { - // email tokens may contain '+' but no space. - // The parameter parsing replaces all '+' with a space, - // undo that to have valid tokens. - const token = data.params[0].replace(/ /g, '+'); - this._setParams({ - view: Gerrit.Nav.View.SETTINGS, - emailToken: token, - }); - } + _handleDiffEditRoute(ctx) { + // Parameter order is based on the regex group number matched. + const project = ctx.params[0]; + this._redirectOrNavigate({ + project, + changeNum: ctx.params[1], + patchNum: ctx.params[2], + path: ctx.params[3], + lineNum: ctx.hash, + view: Gerrit.Nav.View.EDIT, + }); + this.$.reporting.setRepoName(project); + } - _handleSettingsRoute(data) { - this._setParams({view: Gerrit.Nav.View.SETTINGS}); - } + _handleChangeEditRoute(ctx) { + // Parameter order is based on the regex group number matched. + const project = ctx.params[0]; + this._redirectOrNavigate({ + project, + changeNum: ctx.params[1], + patchNum: ctx.params[3], + view: Gerrit.Nav.View.CHANGE, + edit: true, + }); + this.$.reporting.setRepoName(project); + } - _handleRegisterRoute(ctx) { - this._setParams({justRegistered: true}); - let path = ctx.params[0] || '/'; - - // Prevent redirect looping. - if (path.startsWith('/register')) { path = '/'; } - - if (path[0] !== '/') { return; } - this._redirect(this.getBaseUrl() + path); - } - - /** - * Handler for routes that should pass through the router and not be caught - * by the catchall _handleDefaultRoute handler. - */ - _handlePassThroughRoute() { - location.reload(); - } - - /** - * URL may sometimes have /+/ encoded to / /. - * Context: Issue 6888, Issue 7100 - */ - _handleImproperlyEncodedPlusRoute(ctx) { - let hash = this._getHashFromCanonicalPath(ctx.canonicalPath); - if (hash.length) { hash = '#' + hash; } - this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`); - } - - _handlePluginScreen(ctx) { - const view = Gerrit.Nav.View.PLUGIN_SCREEN; - const plugin = ctx.params[0]; - const screen = ctx.params[1]; - this._setParams({view, plugin, screen}); - } - - _handleDocumentationSearchRoute(data) { - this._setParams({ - view: Gerrit.Nav.View.DOCUMENTATION_SEARCH, - filter: data.params.filter || null, - }); - } - - _handleDocumentationSearchRedirectRoute(data) { - this._redirect('/Documentation/q/filter:' + - encodeURIComponent(data.params[0])); - } - - _handleDocumentationRedirectRoute(data) { - if (data.params[1]) { - location.reload(); - } else { - // Redirect /Documentation to /Documentation/index.html - this._redirect('/Documentation/index.html'); - } - } - - /** - * Catchall route for when no other route is matched. - */ - _handleDefaultRoute() { - if (this._isInitialLoad) { - // Server recognized this route as polygerrit, so we show 404. - this._show404(); - } else { - // Route can be recognized by server, so we pass it to server. - this._handlePassThroughRoute(); - } - } - - _show404() { - // Note: the app's 404 display is tightly-coupled with catching 404 - // network responses, so we simulate a 404 response status to display it. - // TODO: Decouple the gr-app error view from network responses. - this._appElement().dispatchEvent(new CustomEvent('page-error', - {detail: {response: {status: 404}}})); + /** + * Normalize the patch range params for a the change or diff view and + * redirect if URL upgrade is needed. + */ + _redirectOrNavigate(params) { + const needsRedirect = this._normalizePatchRangeParams(params); + if (needsRedirect) { + this._redirect(this._generateUrl(params)); + } else { + this._setParams(params); } } - customElements.define(GrRouter.is, GrRouter); -})(); + _handleAgreementsRoute() { + this._redirect('/settings/#Agreements'); + } + + _handleNewAgreementsRoute(data) { + data.params.view = Gerrit.Nav.View.AGREEMENTS; + this._setParams(data.params); + } + + _handleSettingsLegacyRoute(data) { + // email tokens may contain '+' but no space. + // The parameter parsing replaces all '+' with a space, + // undo that to have valid tokens. + const token = data.params[0].replace(/ /g, '+'); + this._setParams({ + view: Gerrit.Nav.View.SETTINGS, + emailToken: token, + }); + } + + _handleSettingsRoute(data) { + this._setParams({view: Gerrit.Nav.View.SETTINGS}); + } + + _handleRegisterRoute(ctx) { + this._setParams({justRegistered: true}); + let path = ctx.params[0] || '/'; + + // Prevent redirect looping. + if (path.startsWith('/register')) { path = '/'; } + + if (path[0] !== '/') { return; } + this._redirect(this.getBaseUrl() + path); + } + + /** + * Handler for routes that should pass through the router and not be caught + * by the catchall _handleDefaultRoute handler. + */ + _handlePassThroughRoute() { + location.reload(); + } + + /** + * URL may sometimes have /+/ encoded to / /. + * Context: Issue 6888, Issue 7100 + */ + _handleImproperlyEncodedPlusRoute(ctx) { + let hash = this._getHashFromCanonicalPath(ctx.canonicalPath); + if (hash.length) { hash = '#' + hash; } + this._redirect(`/c/${ctx.params[0]}/+/${ctx.params[1]}${hash}`); + } + + _handlePluginScreen(ctx) { + const view = Gerrit.Nav.View.PLUGIN_SCREEN; + const plugin = ctx.params[0]; + const screen = ctx.params[1]; + this._setParams({view, plugin, screen}); + } + + _handleDocumentationSearchRoute(data) { + this._setParams({ + view: Gerrit.Nav.View.DOCUMENTATION_SEARCH, + filter: data.params.filter || null, + }); + } + + _handleDocumentationSearchRedirectRoute(data) { + this._redirect('/Documentation/q/filter:' + + encodeURIComponent(data.params[0])); + } + + _handleDocumentationRedirectRoute(data) { + if (data.params[1]) { + location.reload(); + } else { + // Redirect /Documentation to /Documentation/index.html + this._redirect('/Documentation/index.html'); + } + } + + /** + * Catchall route for when no other route is matched. + */ + _handleDefaultRoute() { + if (this._isInitialLoad) { + // Server recognized this route as polygerrit, so we show 404. + this._show404(); + } else { + // Route can be recognized by server, so we pass it to server. + this._handlePassThroughRoute(); + } + } + + _show404() { + // Note: the app's 404 display is tightly-coupled with catching 404 + // network responses, so we simulate a 404 response status to display it. + // TODO: Decouple the gr-app error view from network responses. + this._appElement().dispatchEvent(new CustomEvent('page-error', + {detail: {response: {status: 404}}})); + } +} + +customElements.define(GrRouter.is, GrRouter);
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js index 5d2531e..01acaa3 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-reporting/gr-reporting.html"> -<script src="/bower_components/page/page.js"></script> - -<dom-module id="gr-router"> - <template> +export const htmlTemplate = html` <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-router.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html index f127a91..6ea07a5 100644 --- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html +++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-router</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-router.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-router.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-router.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,1617 +40,1619 @@ </template> </test-fixture> -<script> - suite('gr-router tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-router.js'; +suite('gr-router tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + + test('_firstCodeBrowserWeblink', () => { + assert.deepEqual(element._firstCodeBrowserWeblink([ + {name: 'gitweb'}, + {name: 'gitiles'}, + {name: 'browse'}, + {name: 'test'}]), {name: 'gitiles'}); + + assert.deepEqual(element._firstCodeBrowserWeblink([ + {name: 'gitweb'}, + {name: 'test'}]), {name: 'gitweb'}); + }); + + test('_getBrowseCommitWeblink', () => { + const browserLink = {name: 'browser', url: 'browser/url'}; + const link = {name: 'test', url: 'test/url'}; + const weblinks = [browserLink, link]; + const config = {gerrit: {primary_weblink_name: browserLink.name}}; + sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link); + + assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config), + browserLink); + + assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link); + }); + + test('_getChangeWeblinks', () => { + const link = {name: 'test', url: 'test/url'}; + const browserLink = {name: 'browser', url: 'browser/url'}; + const mapLinksToConfig = weblinks => { return {options: {weblinks}}; }; + sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink); + + assert.deepEqual( + element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0], + {name: 'test', url: 'test/url'}); + + assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], + {name: 'test', url: 'test/url'}); + + link.url = 'https://' + link.url; + assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], + {name: 'test', url: 'https://test/url'}); + }); + + test('_getHashFromCanonicalPath', () => { + let url = '/foo/bar'; + let hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, ''); + + url = ''; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, ''); + + url = '/foo#bar'; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, 'bar'); + + url = '/foo#bar#baz'; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, 'bar#baz'); + + url = '#foo#bar#baz'; + hash = element._getHashFromCanonicalPath(url); + assert.equal(hash, 'foo#bar#baz'); + }); + + suite('_parseLineAddress', () => { + test('returns null for empty and invalid hashes', () => { + let actual = element._parseLineAddress(''); + assert.isNull(actual); + + actual = element._parseLineAddress('foobar'); + assert.isNull(actual); + + actual = element._parseLineAddress('foo123'); + assert.isNull(actual); + + actual = element._parseLineAddress('123bar'); + assert.isNull(actual); + }); + + test('parses correctly', () => { + let actual = element._parseLineAddress('1234'); + assert.isOk(actual); + assert.equal(actual.lineNum, 1234); + assert.isFalse(actual.leftSide); + + actual = element._parseLineAddress('a4'); + assert.isOk(actual); + assert.equal(actual.lineNum, 4); + assert.isTrue(actual.leftSide); + + actual = element._parseLineAddress('b77'); + assert.isOk(actual); + assert.equal(actual.lineNum, 77); + assert.isTrue(actual.leftSide); + }); + }); + + test('_startRouter requires auth for the right handlers', () => { + // This test encodes the lists of route handler methods that gr-router + // automatically checks for authentication before triggering. + + const requiresAuth = {}; + const doesNotRequireAuth = {}; + sandbox.stub(Gerrit.Nav, 'setup'); + sandbox.stub(window.page, 'start'); + sandbox.stub(window.page, 'base'); + sandbox.stub(window, 'page'); + sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => { + if (usesAuth) { + requiresAuth[methodName] = true; + } else { + doesNotRequireAuth[methodName] = true; + } + }); + element._startRouter(); + + const actualRequiresAuth = Object.keys(requiresAuth); + actualRequiresAuth.sort(); + const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth); + actualDoesNotRequireAuth.sort(); + + const shouldRequireAutoAuth = [ + '_handleAgreementsRoute', + '_handleChangeEditRoute', + '_handleCreateGroupRoute', + '_handleCreateProjectRoute', + '_handleDiffEditRoute', + '_handleGroupAuditLogRoute', + '_handleGroupInfoRoute', + '_handleGroupListFilterOffsetRoute', + '_handleGroupListFilterRoute', + '_handleGroupListOffsetRoute', + '_handleGroupMembersRoute', + '_handleGroupRoute', + '_handleGroupSelfRedirectRoute', + '_handleNewAgreementsRoute', + '_handlePluginListFilterOffsetRoute', + '_handlePluginListFilterRoute', + '_handlePluginListOffsetRoute', + '_handlePluginListRoute', + '_handleRepoCommandsRoute', + '_handleSettingsLegacyRoute', + '_handleSettingsRoute', + ]; + assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth); + + const unauthenticatedHandlers = [ + '_handleBranchListFilterOffsetRoute', + '_handleBranchListFilterRoute', + '_handleBranchListOffsetRoute', + '_handleChangeNumberLegacyRoute', + '_handleChangeRoute', + '_handleDiffRoute', + '_handleDefaultRoute', + '_handleChangeLegacyRoute', + '_handleDiffLegacyRoute', + '_handleDocumentationRedirectRoute', + '_handleDocumentationSearchRoute', + '_handleDocumentationSearchRedirectRoute', + '_handleLegacyLinenum', + '_handleImproperlyEncodedPlusRoute', + '_handlePassThroughRoute', + '_handleProjectDashboardRoute', + '_handleProjectsOldRoute', + '_handleRepoAccessRoute', + '_handleRepoDashboardsRoute', + '_handleRepoListFilterOffsetRoute', + '_handleRepoListFilterRoute', + '_handleRepoListOffsetRoute', + '_handleRepoRoute', + '_handleQueryLegacySuffixRoute', + '_handleQueryRoute', + '_handleRegisterRoute', + '_handleTagListFilterOffsetRoute', + '_handleTagListFilterRoute', + '_handleTagListOffsetRoute', + '_handlePluginScreen', + ]; + + // Handler names that check authentication themselves, and thus don't need + // it performed for them. + const selfAuthenticatingHandlers = [ + '_handleDashboardRoute', + '_handleCustomDashboardRoute', + '_handleRootRoute', + ]; + + const shouldNotRequireAuth = unauthenticatedHandlers + .concat(selfAuthenticatingHandlers); + shouldNotRequireAuth.sort(); + assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth); + }); + + test('_redirectIfNotLoggedIn while logged in', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + const data = {canonicalPath: ''}; + const redirectStub = sandbox.stub(element, '_redirectToLogin'); + return element._redirectIfNotLoggedIn(data).then(() => { + assert.isFalse(redirectStub.called); + }); + }); + + test('_redirectIfNotLoggedIn while logged out', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const redirectStub = sandbox.stub(element, '_redirectToLogin'); + const data = {canonicalPath: ''}; + return new Promise(resolve => { + element._redirectIfNotLoggedIn(data) + .then(() => { + assert.isTrue(false, 'Should never execute'); + }) + .catch(() => { + assert.isTrue(redirectStub.calledOnce); + resolve(); + }); + }); + }); + + suite('generateUrl', () => { + test('search', () => { + let params = { + view: Gerrit.Nav.View.SEARCH, + owner: 'a%b', + project: 'c%d', + branch: 'e%f', + topic: 'g%h', + statuses: ['op%en'], + }; + assert.equal(element._generateUrl(params), + '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + + 'topic:"g%2525h"+status:op%2525en'); + + params.offset = 100; + assert.equal(element._generateUrl(params), + '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + + 'topic:"g%2525h"+status:op%2525en,100'); + delete params.offset; + + // The presence of the query param overrides other params. + params.query = 'foo$bar'; + assert.equal(element._generateUrl(params), '/q/foo%2524bar'); + + params.offset = 100; + assert.equal(element._generateUrl(params), '/q/foo%2524bar,100'); + + params = { + view: Gerrit.Nav.View.SEARCH, + statuses: ['a', 'b', 'c'], + }; + assert.equal(element._generateUrl(params), + '/q/(status:a OR status:b OR status:c)'); + }); + + test('change', () => { + const params = { + view: Gerrit.Nav.View.CHANGE, + changeNum: '1234', + project: 'test', + }; + const paramsWithQuery = { + view: Gerrit.Nav.View.CHANGE, + changeNum: '1234', + project: 'test', + querystring: 'revert&foo=bar', + }; + + assert.equal(element._generateUrl(params), '/c/test/+/1234'); + assert.equal(element._generateUrl(paramsWithQuery), + '/c/test/+/1234?revert&foo=bar'); + + params.patchNum = 10; + assert.equal(element._generateUrl(params), '/c/test/+/1234/10'); + paramsWithQuery.patchNum = 10; + assert.equal(element._generateUrl(paramsWithQuery), + '/c/test/+/1234/10?revert&foo=bar'); + + params.basePatchNum = 5; + assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10'); + paramsWithQuery.basePatchNum = 5; + assert.equal(element._generateUrl(paramsWithQuery), + '/c/test/+/1234/5..10?revert&foo=bar'); + + params.messageHash = '#123'; + assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123'); + }); + + test('change with repo name encoding', () => { + const params = { + view: Gerrit.Nav.View.CHANGE, + changeNum: '1234', + project: 'x+/y+/z+/w', + }; + assert.equal(element._generateUrl(params), + '/c/x%252B/y%252B/z%252B/w/+/1234'); + }); + + test('diff', () => { + const params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + path: 'x+y/path.cpp', + patchNum: 12, + }; + assert.equal(element._generateUrl(params), + '/c/42/12/x%252By/path.cpp'); + + params.project = 'test'; + assert.equal(element._generateUrl(params), + '/c/test/+/42/12/x%252By/path.cpp'); + + params.basePatchNum = 6; + assert.equal(element._generateUrl(params), + '/c/test/+/42/6..12/x%252By/path.cpp'); + + params.path = 'foo bar/my+file.txt%'; + params.patchNum = 2; + delete params.basePatchNum; + assert.equal(element._generateUrl(params), + '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'); + + params.path = 'file.cpp'; + params.lineNum = 123; + assert.equal(element._generateUrl(params), + '/c/test/+/42/2/file.cpp#123'); + + params.leftSide = true; + assert.equal(element._generateUrl(params), + '/c/test/+/42/2/file.cpp#b123'); + }); + + test('diff with repo name encoding', () => { + const params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + path: 'x+y/path.cpp', + patchNum: 12, + project: 'x+/y', + }; + assert.equal(element._generateUrl(params), + '/c/x%252B/y/+/42/12/x%252By/path.cpp'); + }); + + test('edit', () => { + const params = { + view: Gerrit.Nav.View.EDIT, + changeNum: '42', + project: 'test', + path: 'x+y/path.cpp', + }; + assert.equal(element._generateUrl(params), + '/c/test/+/42/x%252By/path.cpp,edit'); + }); + + test('_getPatchRangeExpression', () => { + const params = {}; + let actual = element._getPatchRangeExpression(params); + assert.equal(actual, ''); + + params.patchNum = 4; + actual = element._getPatchRangeExpression(params); + assert.equal(actual, '4'); + + params.basePatchNum = 2; + actual = element._getPatchRangeExpression(params); + assert.equal(actual, '2..4'); + + delete params.patchNum; + actual = element._getPatchRangeExpression(params); + assert.equal(actual, '2..'); + }); + + suite('dashboard', () => { + test('self dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + }; + assert.equal(element._generateUrl(params), '/dashboard/self'); + }); + + test('user dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + user: 'user', + }; + assert.equal(element._generateUrl(params), '/dashboard/user'); + }); + + test('custom self dashboard, no title', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + sections: [ + {name: 'section 1', query: 'query 1'}, + {name: 'section 2', query: 'query 2'}, + ], + }; + assert.equal( + element._generateUrl(params), + '/dashboard/?section%201=query%201§ion%202=query%202'); + }); + + test('custom repo dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + sections: [ + {name: 'section 1', query: 'query 1 ${project}'}, + {name: 'section 2', query: 'query 2 ${repo}'}, + ], + repo: 'repo-name', + }; + assert.equal( + element._generateUrl(params), + '/dashboard/?section%201=query%201%20repo-name&' + + 'section%202=query%202%20repo-name'); + }); + + test('custom user dashboard, with title', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + user: 'user', + sections: [{name: 'name', query: 'query'}], + title: 'custom dashboard', + }; + assert.equal( + element._generateUrl(params), + '/dashboard/user?name=query&title=custom%20dashboard'); + }); + + test('repo dashboard', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + repo: 'gerrit/repo', + dashboard: 'default:main', + }; + assert.equal( + element._generateUrl(params), + '/p/gerrit/repo/+/dashboard/default:main'); + }); + + test('project dashboard (legacy)', () => { + const params = { + view: Gerrit.Nav.View.DASHBOARD, + project: 'gerrit/project', + dashboard: 'default:main', + }; + assert.equal( + element._generateUrl(params), + '/p/gerrit/project/+/dashboard/default:main'); + }); + }); + + suite('groups', () => { + test('group info', () => { + const params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + }; + assert.equal(element._generateUrl(params), '/admin/groups/1234'); + }); + + test('group members', () => { + const params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + detail: 'members', + }; + assert.equal(element._generateUrl(params), + '/admin/groups/1234,members'); + }); + + test('group audit log', () => { + const params = { + view: Gerrit.Nav.View.GROUP, + groupId: 1234, + detail: 'log', + }; + assert.equal(element._generateUrl(params), + '/admin/groups/1234,audit-log'); + }); + }); + }); + + suite('param normalization', () => { + let projectLookupStub; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + projectLookupStub = sandbox + .stub(element.$.restAPI, 'getFromProjectLookup'); + sandbox.stub(element, '_generateUrl'); }); - teardown(() => { sandbox.restore(); }); + suite('_normalizeLegacyRouteParams', () => { + let rangeStub; + let redirectStub; + let show404Stub; - test('_firstCodeBrowserWeblink', () => { - assert.deepEqual(element._firstCodeBrowserWeblink([ - {name: 'gitweb'}, - {name: 'gitiles'}, - {name: 'browse'}, - {name: 'test'}]), {name: 'gitiles'}); - - assert.deepEqual(element._firstCodeBrowserWeblink([ - {name: 'gitweb'}, - {name: 'test'}]), {name: 'gitweb'}); - }); - - test('_getBrowseCommitWeblink', () => { - const browserLink = {name: 'browser', url: 'browser/url'}; - const link = {name: 'test', url: 'test/url'}; - const weblinks = [browserLink, link]; - const config = {gerrit: {primary_weblink_name: browserLink.name}}; - sandbox.stub(element, '_firstCodeBrowserWeblink').returns(link); - - assert.deepEqual(element._getBrowseCommitWeblink(weblinks, config), - browserLink); - - assert.deepEqual(element._getBrowseCommitWeblink(weblinks, {}), link); - }); - - test('_getChangeWeblinks', () => { - const link = {name: 'test', url: 'test/url'}; - const browserLink = {name: 'browser', url: 'browser/url'}; - const mapLinksToConfig = weblinks => { return {options: {weblinks}}; }; - sandbox.stub(element, '_getBrowseCommitWeblink').returns(browserLink); - - assert.deepEqual( - element._getChangeWeblinks(mapLinksToConfig([link, browserLink]))[0], - {name: 'test', url: 'test/url'}); - - assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], - {name: 'test', url: 'test/url'}); - - link.url = 'https://' + link.url; - assert.deepEqual(element._getChangeWeblinks(mapLinksToConfig([link]))[0], - {name: 'test', url: 'https://test/url'}); - }); - - test('_getHashFromCanonicalPath', () => { - let url = '/foo/bar'; - let hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, ''); - - url = ''; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, ''); - - url = '/foo#bar'; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, 'bar'); - - url = '/foo#bar#baz'; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, 'bar#baz'); - - url = '#foo#bar#baz'; - hash = element._getHashFromCanonicalPath(url); - assert.equal(hash, 'foo#bar#baz'); - }); - - suite('_parseLineAddress', () => { - test('returns null for empty and invalid hashes', () => { - let actual = element._parseLineAddress(''); - assert.isNull(actual); - - actual = element._parseLineAddress('foobar'); - assert.isNull(actual); - - actual = element._parseLineAddress('foo123'); - assert.isNull(actual); - - actual = element._parseLineAddress('123bar'); - assert.isNull(actual); + setup(() => { + rangeStub = sandbox.stub(element, '_normalizePatchRangeParams') + .returns(Promise.resolve()); + redirectStub = sandbox.stub(element, '_redirect'); + show404Stub = sandbox.stub(element, '_show404'); }); - test('parses correctly', () => { - let actual = element._parseLineAddress('1234'); - assert.isOk(actual); - assert.equal(actual.lineNum, 1234); - assert.isFalse(actual.leftSide); + test('w/o changeNum', () => { + projectLookupStub.returns(Promise.resolve('foo/bar')); + const params = {}; + return element._normalizeLegacyRouteParams(params).then(() => { + assert.isFalse(projectLookupStub.called); + assert.isFalse(rangeStub.called); + assert.isNotOk(params.project); + assert.isFalse(redirectStub.called); + assert.isFalse(show404Stub.called); + }); + }); - actual = element._parseLineAddress('a4'); - assert.isOk(actual); - assert.equal(actual.lineNum, 4); - assert.isTrue(actual.leftSide); + test('w/ changeNum', () => { + projectLookupStub.returns(Promise.resolve('foo/bar')); + const params = {changeNum: 1234}; + return element._normalizeLegacyRouteParams(params).then(() => { + assert.isTrue(projectLookupStub.called); + assert.isTrue(rangeStub.called); + assert.equal(params.project, 'foo/bar'); + assert.isTrue(redirectStub.calledOnce); + assert.isFalse(show404Stub.called); + }); + }); - actual = element._parseLineAddress('b77'); - assert.isOk(actual); - assert.equal(actual.lineNum, 77); - assert.isTrue(actual.leftSide); + test('halts on project lookup failure', () => { + projectLookupStub.returns(Promise.resolve(undefined)); + const params = {changeNum: 1234}; + return element._normalizeLegacyRouteParams(params).then(() => { + assert.isTrue(projectLookupStub.called); + assert.isFalse(rangeStub.called); + assert.isUndefined(params.project); + assert.isFalse(redirectStub.called); + assert.isTrue(show404Stub.calledOnce); + }); }); }); - test('_startRouter requires auth for the right handlers', () => { - // This test encodes the lists of route handler methods that gr-router - // automatically checks for authentication before triggering. + suite('_normalizePatchRangeParams', () => { + test('range n..n normalizes to n', () => { + const params = {basePatchNum: 4, patchNum: 4}; + const needsRedirect = element._normalizePatchRangeParams(params); + assert.isTrue(needsRedirect); + assert.isNotOk(params.basePatchNum); + assert.equal(params.patchNum, 4); + }); - const requiresAuth = {}; - const doesNotRequireAuth = {}; + test('range n.. normalizes to n', () => { + const params = {basePatchNum: 4}; + const needsRedirect = element._normalizePatchRangeParams(params); + assert.isFalse(needsRedirect); + assert.isNotOk(params.basePatchNum); + assert.equal(params.patchNum, 4); + }); + }); + }); + + suite('route handlers', () => { + let redirectStub; + let setParamsStub; + let handlePassThroughRoute; + + // Simple route handlers are direct mappings from parsed route data to a + // new set of app.params. This test helper asserts that passing `data` + // into `methodName` results in setting the params specified in `params`. + function assertDataToParams(data, methodName, params) { + element[methodName](data); + assert.deepEqual(setParamsStub.lastCall.args[0], params); + } + + setup(() => { + redirectStub = sandbox.stub(element, '_redirect'); + setParamsStub = sandbox.stub(element, '_setParams'); + handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute'); + }); + + test('_handleAgreementsRoute', () => { + const data = {params: {}}; + element._handleAgreementsRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements'); + }); + + test('_handleNewAgreementsRoute', () => { + element._handleNewAgreementsRoute({params: {}}); + assert.isTrue(setParamsStub.calledOnce); + assert.equal(setParamsStub.lastCall.args[0].view, + Gerrit.Nav.View.AGREEMENTS); + }); + + test('_handleSettingsLegacyRoute', () => { + const data = {params: {0: 'my-token'}}; + assertDataToParams(data, '_handleSettingsLegacyRoute', { + view: Gerrit.Nav.View.SETTINGS, + emailToken: 'my-token', + }); + }); + + test('_handleSettingsLegacyRoute with +', () => { + const data = {params: {0: 'my-token test'}}; + assertDataToParams(data, '_handleSettingsLegacyRoute', { + view: Gerrit.Nav.View.SETTINGS, + emailToken: 'my-token+test', + }); + }); + + test('_handleSettingsRoute', () => { + const data = {}; + assertDataToParams(data, '_handleSettingsRoute', { + view: Gerrit.Nav.View.SETTINGS, + }); + }); + + test('_handleDefaultRoute on first load', () => { + const appElementStub = {dispatchEvent: sinon.stub()}; + element._appElement = () => appElementStub; + element._handleDefaultRoute(); + assert.isTrue(appElementStub.dispatchEvent.calledOnce); + assert.equal( + appElementStub.dispatchEvent.lastCall.args[0].detail.response.status, + 404); + }); + + test('_handleDefaultRoute after internal navigation', () => { + let onExit = null; + const onRegisteringExit = (match, _onExit) => { + onExit = _onExit; + }; + sandbox.stub(window.page, 'exit', onRegisteringExit); sandbox.stub(Gerrit.Nav, 'setup'); sandbox.stub(window.page, 'start'); sandbox.stub(window.page, 'base'); sandbox.stub(window, 'page'); - sandbox.stub(element, '_mapRoute', (pattern, methodName, usesAuth) => { - if (usesAuth) { - requiresAuth[methodName] = true; - } else { - doesNotRequireAuth[methodName] = true; - } - }); element._startRouter(); - const actualRequiresAuth = Object.keys(requiresAuth); - actualRequiresAuth.sort(); - const actualDoesNotRequireAuth = Object.keys(doesNotRequireAuth); - actualDoesNotRequireAuth.sort(); + const appElementStub = {dispatchEvent: sinon.stub()}; + element._appElement = () => appElementStub; + element._handleDefaultRoute(); - const shouldRequireAutoAuth = [ - '_handleAgreementsRoute', - '_handleChangeEditRoute', - '_handleCreateGroupRoute', - '_handleCreateProjectRoute', - '_handleDiffEditRoute', - '_handleGroupAuditLogRoute', - '_handleGroupInfoRoute', - '_handleGroupListFilterOffsetRoute', - '_handleGroupListFilterRoute', - '_handleGroupListOffsetRoute', - '_handleGroupMembersRoute', - '_handleGroupRoute', - '_handleGroupSelfRedirectRoute', - '_handleNewAgreementsRoute', - '_handlePluginListFilterOffsetRoute', - '_handlePluginListFilterRoute', - '_handlePluginListOffsetRoute', - '_handlePluginListRoute', - '_handleRepoCommandsRoute', - '_handleSettingsLegacyRoute', - '_handleSettingsRoute', - ]; - assert.deepEqual(actualRequiresAuth, shouldRequireAutoAuth); + onExit('', () => {}); // we left page; - const unauthenticatedHandlers = [ - '_handleBranchListFilterOffsetRoute', - '_handleBranchListFilterRoute', - '_handleBranchListOffsetRoute', - '_handleChangeNumberLegacyRoute', - '_handleChangeRoute', - '_handleDiffRoute', - '_handleDefaultRoute', - '_handleChangeLegacyRoute', - '_handleDiffLegacyRoute', - '_handleDocumentationRedirectRoute', - '_handleDocumentationSearchRoute', - '_handleDocumentationSearchRedirectRoute', - '_handleLegacyLinenum', - '_handleImproperlyEncodedPlusRoute', - '_handlePassThroughRoute', - '_handleProjectDashboardRoute', - '_handleProjectsOldRoute', - '_handleRepoAccessRoute', - '_handleRepoDashboardsRoute', - '_handleRepoListFilterOffsetRoute', - '_handleRepoListFilterRoute', - '_handleRepoListOffsetRoute', - '_handleRepoRoute', - '_handleQueryLegacySuffixRoute', - '_handleQueryRoute', - '_handleRegisterRoute', - '_handleTagListFilterOffsetRoute', - '_handleTagListFilterRoute', - '_handleTagListOffsetRoute', - '_handlePluginScreen', - ]; - - // Handler names that check authentication themselves, and thus don't need - // it performed for them. - const selfAuthenticatingHandlers = [ - '_handleDashboardRoute', - '_handleCustomDashboardRoute', - '_handleRootRoute', - ]; - - const shouldNotRequireAuth = unauthenticatedHandlers - .concat(selfAuthenticatingHandlers); - shouldNotRequireAuth.sort(); - assert.deepEqual(actualDoesNotRequireAuth, shouldNotRequireAuth); + element._handleDefaultRoute(); + assert.isTrue(handlePassThroughRoute.calledOnce); }); - test('_redirectIfNotLoggedIn while logged in', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - const data = {canonicalPath: ''}; - const redirectStub = sandbox.stub(element, '_redirectToLogin'); - return element._redirectIfNotLoggedIn(data).then(() => { + test('_handleImproperlyEncodedPlusRoute', () => { + // Regression test for Issue 7100. + element._handleImproperlyEncodedPlusRoute( + {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); + assert.isTrue(redirectStub.calledOnce); + assert.equal( + redirectStub.lastCall.args[0], + '/c/test/+/42'); + + sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo'); + element._handleImproperlyEncodedPlusRoute( + {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); + assert.equal( + redirectStub.lastCall.args[0], + '/c/test/+/42#foo'); + }); + + test('_handleQueryRoute', () => { + const data = {params: ['project:foo/bar/baz']}; + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: undefined, + }); + + data.params.push(',123', '123'); + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: '123', + }); + }); + + test('_handleQueryLegacySuffixRoute', () => { + element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'}); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar'); + }); + + test('_handleQueryRoute', () => { + const data = {params: ['project:foo/bar/baz']}; + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: undefined, + }); + + data.params.push(',123', '123'); + assertDataToParams(data, '_handleQueryRoute', { + view: Gerrit.Nav.View.SEARCH, + query: 'project:foo/bar/baz', + offset: '123', + }); + }); + + suite('_handleRegisterRoute', () => { + test('happy path', () => { + const ctx = {params: ['/foo/bar']}; + element._handleRegisterRoute(ctx); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar')); + assert.isTrue(setParamsStub.calledOnce); + assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); + }); + + test('no param', () => { + const ctx = {params: ['']}; + element._handleRegisterRoute(ctx); + assert.isTrue(redirectStub.calledWithExactly('/')); + assert.isTrue(setParamsStub.calledOnce); + assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); + }); + + test('prevent redirect', () => { + const ctx = {params: ['/register']}; + element._handleRegisterRoute(ctx); + assert.isTrue(redirectStub.calledWithExactly('/')); + assert.isTrue(setParamsStub.calledOnce); + assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); + }); + }); + + suite('_handleRootRoute', () => { + test('closes for closeAfterLogin', () => { + const data = {querystring: 'closeAfterLogin', canonicalPath: ''}; + const closeStub = sandbox.stub(window, 'close'); + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(closeStub.called); assert.isFalse(redirectStub.called); }); - }); - test('_redirectIfNotLoggedIn while logged out', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const redirectStub = sandbox.stub(element, '_redirectToLogin'); - const data = {canonicalPath: ''}; - return new Promise(resolve => { - element._redirectIfNotLoggedIn(data) - .then(() => { - assert.isTrue(false, 'Should never execute'); - }) - .catch(() => { - assert.isTrue(redirectStub.calledOnce); - resolve(); - }); - }); - }); - - suite('generateUrl', () => { - test('search', () => { - let params = { - view: Gerrit.Nav.View.SEARCH, - owner: 'a%b', - project: 'c%d', - branch: 'e%f', - topic: 'g%h', - statuses: ['op%en'], + test('redirects to dashboard if logged in', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + const data = { + canonicalPath: '/', path: '/', querystring: '', hash: '', }; - assert.equal(element._generateUrl(params), - '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + - 'topic:"g%2525h"+status:op%2525en'); + const result = element._handleRootRoute(data); + assert.isOk(result); + return result.then(() => { + assert.isTrue(redirectStub.calledWithExactly('/dashboard/self')); + }); + }); - params.offset = 100; - assert.equal(element._generateUrl(params), - '/q/owner:a%2525b+project:c%2525d+branch:e%2525f+' + - 'topic:"g%2525h"+status:op%2525en,100'); - delete params.offset; - - // The presence of the query param overrides other params. - params.query = 'foo$bar'; - assert.equal(element._generateUrl(params), '/q/foo%2524bar'); - - params.offset = 100; - assert.equal(element._generateUrl(params), '/q/foo%2524bar,100'); - - params = { - view: Gerrit.Nav.View.SEARCH, - statuses: ['a', 'b', 'c'], + test('redirects to open changes if not logged in', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const data = { + canonicalPath: '/', path: '/', querystring: '', hash: '', }; - assert.equal(element._generateUrl(params), - '/q/(status:a OR status:b OR status:c)'); + const result = element._handleRootRoute(data); + assert.isOk(result); + return result.then(() => { + assert.isTrue(redirectStub.calledWithExactly('/q/status:open')); + }); }); - test('change', () => { - const params = { - view: Gerrit.Nav.View.CHANGE, - changeNum: '1234', - project: 'test', - }; - const paramsWithQuery = { - view: Gerrit.Nav.View.CHANGE, - changeNum: '1234', - project: 'test', - querystring: 'revert&foo=bar', - }; - - assert.equal(element._generateUrl(params), '/c/test/+/1234'); - assert.equal(element._generateUrl(paramsWithQuery), - '/c/test/+/1234?revert&foo=bar'); - - params.patchNum = 10; - assert.equal(element._generateUrl(params), '/c/test/+/1234/10'); - paramsWithQuery.patchNum = 10; - assert.equal(element._generateUrl(paramsWithQuery), - '/c/test/+/1234/10?revert&foo=bar'); - - params.basePatchNum = 5; - assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10'); - paramsWithQuery.basePatchNum = 5; - assert.equal(element._generateUrl(paramsWithQuery), - '/c/test/+/1234/5..10?revert&foo=bar'); - - params.messageHash = '#123'; - assert.equal(element._generateUrl(params), '/c/test/+/1234/5..10#123'); - }); - - test('change with repo name encoding', () => { - const params = { - view: Gerrit.Nav.View.CHANGE, - changeNum: '1234', - project: 'x+/y+/z+/w', - }; - assert.equal(element._generateUrl(params), - '/c/x%252B/y%252B/z%252B/w/+/1234'); - }); - - test('diff', () => { - const params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - path: 'x+y/path.cpp', - patchNum: 12, - }; - assert.equal(element._generateUrl(params), - '/c/42/12/x%252By/path.cpp'); - - params.project = 'test'; - assert.equal(element._generateUrl(params), - '/c/test/+/42/12/x%252By/path.cpp'); - - params.basePatchNum = 6; - assert.equal(element._generateUrl(params), - '/c/test/+/42/6..12/x%252By/path.cpp'); - - params.path = 'foo bar/my+file.txt%'; - params.patchNum = 2; - delete params.basePatchNum; - assert.equal(element._generateUrl(params), - '/c/test/+/42/2/foo+bar/my%252Bfile.txt%2525'); - - params.path = 'file.cpp'; - params.lineNum = 123; - assert.equal(element._generateUrl(params), - '/c/test/+/42/2/file.cpp#123'); - - params.leftSide = true; - assert.equal(element._generateUrl(params), - '/c/test/+/42/2/file.cpp#b123'); - }); - - test('diff with repo name encoding', () => { - const params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - path: 'x+y/path.cpp', - patchNum: 12, - project: 'x+/y', - }; - assert.equal(element._generateUrl(params), - '/c/x%252B/y/+/42/12/x%252By/path.cpp'); - }); - - test('edit', () => { - const params = { - view: Gerrit.Nav.View.EDIT, - changeNum: '42', - project: 'test', - path: 'x+y/path.cpp', - }; - assert.equal(element._generateUrl(params), - '/c/test/+/42/x%252By/path.cpp,edit'); - }); - - test('_getPatchRangeExpression', () => { - const params = {}; - let actual = element._getPatchRangeExpression(params); - assert.equal(actual, ''); - - params.patchNum = 4; - actual = element._getPatchRangeExpression(params); - assert.equal(actual, '4'); - - params.basePatchNum = 2; - actual = element._getPatchRangeExpression(params); - assert.equal(actual, '2..4'); - - delete params.patchNum; - actual = element._getPatchRangeExpression(params); - assert.equal(actual, '2..'); - }); - - suite('dashboard', () => { - test('self dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, + suite('GWT hash-path URLs', () => { + test('redirects hash-path URLs', () => { + const data = { + canonicalPath: '/#/foo/bar/baz', + hash: '/foo/bar/baz', + querystring: '', }; - assert.equal(element._generateUrl(params), '/dashboard/self'); - }); - - test('user dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - user: 'user', - }; - assert.equal(element._generateUrl(params), '/dashboard/user'); - }); - - test('custom self dashboard, no title', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - sections: [ - {name: 'section 1', query: 'query 1'}, - {name: 'section 2', query: 'query 2'}, - ], - }; - assert.equal( - element._generateUrl(params), - '/dashboard/?section%201=query%201§ion%202=query%202'); - }); - - test('custom repo dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - sections: [ - {name: 'section 1', query: 'query 1 ${project}'}, - {name: 'section 2', query: 'query 2 ${repo}'}, - ], - repo: 'repo-name', - }; - assert.equal( - element._generateUrl(params), - '/dashboard/?section%201=query%201%20repo-name&' + - 'section%202=query%202%20repo-name'); - }); - - test('custom user dashboard, with title', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - user: 'user', - sections: [{name: 'name', query: 'query'}], - title: 'custom dashboard', - }; - assert.equal( - element._generateUrl(params), - '/dashboard/user?name=query&title=custom%20dashboard'); - }); - - test('repo dashboard', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - repo: 'gerrit/repo', - dashboard: 'default:main', - }; - assert.equal( - element._generateUrl(params), - '/p/gerrit/repo/+/dashboard/default:main'); - }); - - test('project dashboard (legacy)', () => { - const params = { - view: Gerrit.Nav.View.DASHBOARD, - project: 'gerrit/project', - dashboard: 'default:main', - }; - assert.equal( - element._generateUrl(params), - '/p/gerrit/project/+/dashboard/default:main'); - }); - }); - - suite('groups', () => { - test('group info', () => { - const params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - }; - assert.equal(element._generateUrl(params), '/admin/groups/1234'); - }); - - test('group members', () => { - const params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - detail: 'members', - }; - assert.equal(element._generateUrl(params), - '/admin/groups/1234,members'); - }); - - test('group audit log', () => { - const params = { - view: Gerrit.Nav.View.GROUP, - groupId: 1234, - detail: 'log', - }; - assert.equal(element._generateUrl(params), - '/admin/groups/1234,audit-log'); - }); - }); - }); - - suite('param normalization', () => { - let projectLookupStub; - - setup(() => { - projectLookupStub = sandbox - .stub(element.$.restAPI, 'getFromProjectLookup'); - sandbox.stub(element, '_generateUrl'); - }); - - suite('_normalizeLegacyRouteParams', () => { - let rangeStub; - let redirectStub; - let show404Stub; - - setup(() => { - rangeStub = sandbox.stub(element, '_normalizePatchRangeParams') - .returns(Promise.resolve()); - redirectStub = sandbox.stub(element, '_redirect'); - show404Stub = sandbox.stub(element, '_show404'); - }); - - test('w/o changeNum', () => { - projectLookupStub.returns(Promise.resolve('foo/bar')); - const params = {}; - return element._normalizeLegacyRouteParams(params).then(() => { - assert.isFalse(projectLookupStub.called); - assert.isFalse(rangeStub.called); - assert.isNotOk(params.project); - assert.isFalse(redirectStub.called); - assert.isFalse(show404Stub.called); - }); - }); - - test('w/ changeNum', () => { - projectLookupStub.returns(Promise.resolve('foo/bar')); - const params = {changeNum: 1234}; - return element._normalizeLegacyRouteParams(params).then(() => { - assert.isTrue(projectLookupStub.called); - assert.isTrue(rangeStub.called); - assert.equal(params.project, 'foo/bar'); - assert.isTrue(redirectStub.calledOnce); - assert.isFalse(show404Stub.called); - }); - }); - - test('halts on project lookup failure', () => { - projectLookupStub.returns(Promise.resolve(undefined)); - const params = {changeNum: 1234}; - return element._normalizeLegacyRouteParams(params).then(() => { - assert.isTrue(projectLookupStub.called); - assert.isFalse(rangeStub.called); - assert.isUndefined(params.project); - assert.isFalse(redirectStub.called); - assert.isTrue(show404Stub.calledOnce); - }); - }); - }); - - suite('_normalizePatchRangeParams', () => { - test('range n..n normalizes to n', () => { - const params = {basePatchNum: 4, patchNum: 4}; - const needsRedirect = element._normalizePatchRangeParams(params); - assert.isTrue(needsRedirect); - assert.isNotOk(params.basePatchNum); - assert.equal(params.patchNum, 4); - }); - - test('range n.. normalizes to n', () => { - const params = {basePatchNum: 4}; - const needsRedirect = element._normalizePatchRangeParams(params); - assert.isFalse(needsRedirect); - assert.isNotOk(params.basePatchNum); - assert.equal(params.patchNum, 4); - }); - }); - }); - - suite('route handlers', () => { - let redirectStub; - let setParamsStub; - let handlePassThroughRoute; - - // Simple route handlers are direct mappings from parsed route data to a - // new set of app.params. This test helper asserts that passing `data` - // into `methodName` results in setting the params specified in `params`. - function assertDataToParams(data, methodName, params) { - element[methodName](data); - assert.deepEqual(setParamsStub.lastCall.args[0], params); - } - - setup(() => { - redirectStub = sandbox.stub(element, '_redirect'); - setParamsStub = sandbox.stub(element, '_setParams'); - handlePassThroughRoute = sandbox.stub(element, '_handlePassThroughRoute'); - }); - - test('_handleAgreementsRoute', () => { - const data = {params: {}}; - element._handleAgreementsRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/settings/#Agreements'); - }); - - test('_handleNewAgreementsRoute', () => { - element._handleNewAgreementsRoute({params: {}}); - assert.isTrue(setParamsStub.calledOnce); - assert.equal(setParamsStub.lastCall.args[0].view, - Gerrit.Nav.View.AGREEMENTS); - }); - - test('_handleSettingsLegacyRoute', () => { - const data = {params: {0: 'my-token'}}; - assertDataToParams(data, '_handleSettingsLegacyRoute', { - view: Gerrit.Nav.View.SETTINGS, - emailToken: 'my-token', - }); - }); - - test('_handleSettingsLegacyRoute with +', () => { - const data = {params: {0: 'my-token test'}}; - assertDataToParams(data, '_handleSettingsLegacyRoute', { - view: Gerrit.Nav.View.SETTINGS, - emailToken: 'my-token+test', - }); - }); - - test('_handleSettingsRoute', () => { - const data = {}; - assertDataToParams(data, '_handleSettingsRoute', { - view: Gerrit.Nav.View.SETTINGS, - }); - }); - - test('_handleDefaultRoute on first load', () => { - const appElementStub = {dispatchEvent: sinon.stub()}; - element._appElement = () => appElementStub; - element._handleDefaultRoute(); - assert.isTrue(appElementStub.dispatchEvent.calledOnce); - assert.equal( - appElementStub.dispatchEvent.lastCall.args[0].detail.response.status, - 404); - }); - - test('_handleDefaultRoute after internal navigation', () => { - let onExit = null; - const onRegisteringExit = (match, _onExit) => { - onExit = _onExit; - }; - sandbox.stub(window.page, 'exit', onRegisteringExit); - sandbox.stub(Gerrit.Nav, 'setup'); - sandbox.stub(window.page, 'start'); - sandbox.stub(window.page, 'base'); - sandbox.stub(window, 'page'); - element._startRouter(); - - const appElementStub = {dispatchEvent: sinon.stub()}; - element._appElement = () => appElementStub; - element._handleDefaultRoute(); - - onExit('', () => {}); // we left page; - - element._handleDefaultRoute(); - assert.isTrue(handlePassThroughRoute.calledOnce); - }); - - test('_handleImproperlyEncodedPlusRoute', () => { - // Regression test for Issue 7100. - element._handleImproperlyEncodedPlusRoute( - {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); - assert.isTrue(redirectStub.calledOnce); - assert.equal( - redirectStub.lastCall.args[0], - '/c/test/+/42'); - - sandbox.stub(element, '_getHashFromCanonicalPath').returns('foo'); - element._handleImproperlyEncodedPlusRoute( - {canonicalPath: '/c/test/%20/42', params: ['test', '42']}); - assert.equal( - redirectStub.lastCall.args[0], - '/c/test/+/42#foo'); - }); - - test('_handleQueryRoute', () => { - const data = {params: ['project:foo/bar/baz']}; - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: undefined, - }); - - data.params.push(',123', '123'); - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: '123', - }); - }); - - test('_handleQueryLegacySuffixRoute', () => { - element._handleQueryLegacySuffixRoute({path: '/q/foo+bar,n,z'}); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/q/foo+bar'); - }); - - test('_handleQueryRoute', () => { - const data = {params: ['project:foo/bar/baz']}; - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: undefined, - }); - - data.params.push(',123', '123'); - assertDataToParams(data, '_handleQueryRoute', { - view: Gerrit.Nav.View.SEARCH, - query: 'project:foo/bar/baz', - offset: '123', - }); - }); - - suite('_handleRegisterRoute', () => { - test('happy path', () => { - const ctx = {params: ['/foo/bar']}; - element._handleRegisterRoute(ctx); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar')); - assert.isTrue(setParamsStub.calledOnce); - assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); - }); - - test('no param', () => { - const ctx = {params: ['']}; - element._handleRegisterRoute(ctx); - assert.isTrue(redirectStub.calledWithExactly('/')); - assert.isTrue(setParamsStub.calledOnce); - assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); - }); - - test('prevent redirect', () => { - const ctx = {params: ['/register']}; - element._handleRegisterRoute(ctx); - assert.isTrue(redirectStub.calledWithExactly('/')); - assert.isTrue(setParamsStub.calledOnce); - assert.isTrue(setParamsStub.lastCall.args[0].justRegistered); - }); - }); - - suite('_handleRootRoute', () => { - test('closes for closeAfterLogin', () => { - const data = {querystring: 'closeAfterLogin', canonicalPath: ''}; - const closeStub = sandbox.stub(window, 'close'); const result = element._handleRootRoute(data); assert.isNotOk(result); - assert.isTrue(closeStub.called); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); + }); + + test('redirects hash-path URLs w/o leading slash', () => { + const data = { + canonicalPath: '/#foo/bar/baz', + querystring: '', + hash: 'foo/bar/baz', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); + }); + + test('normalizes "/ /" in hash to "/+/"', () => { + const data = { + canonicalPath: '/#/foo/bar/+/123/4', + querystring: '', + hash: '/foo/bar/ /123/4', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4')); + }); + + test('prepends baseurl to hash-path', () => { + const data = { + canonicalPath: '/#/foo/bar', + querystring: '', + hash: '/foo/bar', + }; + sandbox.stub(element, 'getBaseUrl').returns('/baz'); + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar')); + }); + + test('normalizes /VE/ settings hash-paths', () => { + const data = { + canonicalPath: '/#/VE/foo/bar', + querystring: '', + hash: '/VE/foo/bar', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly( + '/settings/VE/foo/bar')); + }); + + test('does not drop "inner hashes"', () => { + const data = { + canonicalPath: '/#/foo/bar#baz', + querystring: '', + hash: '/foo/bar', + }; + const result = element._handleRootRoute(data); + assert.isNotOk(result); + assert.isTrue(redirectStub.called); + assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz')); + }); + }); + }); + + suite('_handleDashboardRoute', () => { + let redirectToLoginStub; + + setup(() => { + redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); + }); + + test('own dashboard but signed out redirects to login', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}}; + return element._handleDashboardRoute(data, '').then(() => { + assert.isTrue(redirectToLoginStub.calledOnce); assert.isFalse(redirectStub.called); - }); - - test('redirects to dashboard if logged in', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - const data = { - canonicalPath: '/', path: '/', querystring: '', hash: '', - }; - const result = element._handleRootRoute(data); - assert.isOk(result); - return result.then(() => { - assert.isTrue(redirectStub.calledWithExactly('/dashboard/self')); - }); - }); - - test('redirects to open changes if not logged in', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const data = { - canonicalPath: '/', path: '/', querystring: '', hash: '', - }; - const result = element._handleRootRoute(data); - assert.isOk(result); - return result.then(() => { - assert.isTrue(redirectStub.calledWithExactly('/q/status:open')); - }); - }); - - suite('GWT hash-path URLs', () => { - test('redirects hash-path URLs', () => { - const data = { - canonicalPath: '/#/foo/bar/baz', - hash: '/foo/bar/baz', - querystring: '', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); - }); - - test('redirects hash-path URLs w/o leading slash', () => { - const data = { - canonicalPath: '/#foo/bar/baz', - querystring: '', - hash: 'foo/bar/baz', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar/baz')); - }); - - test('normalizes "/ /" in hash to "/+/"', () => { - const data = { - canonicalPath: '/#/foo/bar/+/123/4', - querystring: '', - hash: '/foo/bar/ /123/4', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar/+/123/4')); - }); - - test('prepends baseurl to hash-path', () => { - const data = { - canonicalPath: '/#/foo/bar', - querystring: '', - hash: '/foo/bar', - }; - sandbox.stub(element, 'getBaseUrl').returns('/baz'); - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/baz/foo/bar')); - }); - - test('normalizes /VE/ settings hash-paths', () => { - const data = { - canonicalPath: '/#/VE/foo/bar', - querystring: '', - hash: '/VE/foo/bar', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly( - '/settings/VE/foo/bar')); - }); - - test('does not drop "inner hashes"', () => { - const data = { - canonicalPath: '/#/foo/bar#baz', - querystring: '', - hash: '/foo/bar', - }; - const result = element._handleRootRoute(data); - assert.isNotOk(result); - assert.isTrue(redirectStub.called); - assert.isTrue(redirectStub.calledWithExactly('/foo/bar#baz')); - }); + assert.isFalse(setParamsStub.called); }); }); - suite('_handleDashboardRoute', () => { - let redirectToLoginStub; - - setup(() => { - redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); - }); - - test('own dashboard but signed out redirects to login', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}}; - return element._handleDashboardRoute(data, '').then(() => { - assert.isTrue(redirectToLoginStub.calledOnce); - assert.isFalse(redirectStub.called); - assert.isFalse(setParamsStub.called); - }); - }); - - test('non-self dashboard but signed out does not redirect', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; - return element._handleDashboardRoute(data, '').then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo'); - }); - }); - - test('dashboard while signed in sets params', () => { - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; - return element._handleDashboardRoute(data, '').then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'foo', - }); - }); - }); - }); - - suite('_handleCustomDashboardRoute', () => { - let redirectToLoginStub; - - setup(() => { - redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); - }); - - test('no user specified', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, '').then(() => { - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.called); - assert.equal(redirectStub.lastCall.args[0], '/dashboard/self'); - }); - }); - - test('custom dashboard without title', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, '?a=b&c&d=e') - .then(() => { - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections: [ - {name: 'a', query: 'b'}, - {name: 'd', query: 'e'}, - ], - title: 'Custom Dashboard', - }); - }); - }); - - test('custom dashboard with title', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, - '?a=b&c&d=&=e&title=t') - .then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections: [ - {name: 'a', query: 'b'}, - ], - title: 't', - }); - }); - }); - - test('custom dashboard with foreach', () => { - const data = {canonicalPath: '/dashboard/', params: {0: ''}}; - return element._handleCustomDashboardRoute(data, - '?a=b&c&d=&=e&foreach=is:open') - .then(() => { - assert.isFalse(redirectToLoginStub.called); - assert.isFalse(redirectStub.called); - assert.isTrue(setParamsStub.calledOnce); - assert.deepEqual(setParamsStub.lastCall.args[0], { - view: Gerrit.Nav.View.DASHBOARD, - user: 'self', - sections: [ - {name: 'a', query: 'is:open b'}, - ], - title: 'Custom Dashboard', - }); - }); - }); - }); - - suite('group routes', () => { - test('_handleGroupInfoRoute', () => { - const data = {params: {0: 1234}}; - element._handleGroupInfoRoute(data); + test('non-self dashboard but signed out does not redirect', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; + return element._handleDashboardRoute(data, '').then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(setParamsStub.called); assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234'); + assert.equal(redirectStub.lastCall.args[0], '/q/owner:foo'); + }); + }); + + test('dashboard while signed in sets params', () => { + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}}; + return element._handleDashboardRoute(data, '').then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'foo', + }); + }); + }); + }); + + suite('_handleCustomDashboardRoute', () => { + let redirectToLoginStub; + + setup(() => { + redirectToLoginStub = sandbox.stub(element, '_redirectToLogin'); + }); + + test('no user specified', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, '').then(() => { + assert.isFalse(setParamsStub.called); + assert.isTrue(redirectStub.called); + assert.equal(redirectStub.lastCall.args[0], '/dashboard/self'); + }); + }); + + test('custom dashboard without title', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, '?a=b&c&d=e') + .then(() => { + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections: [ + {name: 'a', query: 'b'}, + {name: 'd', query: 'e'}, + ], + title: 'Custom Dashboard', + }); + }); + }); + + test('custom dashboard with title', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, + '?a=b&c&d=&=e&title=t') + .then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections: [ + {name: 'a', query: 'b'}, + ], + title: 't', + }); + }); + }); + + test('custom dashboard with foreach', () => { + const data = {canonicalPath: '/dashboard/', params: {0: ''}}; + return element._handleCustomDashboardRoute(data, + '?a=b&c&d=&=e&foreach=is:open') + .then(() => { + assert.isFalse(redirectToLoginStub.called); + assert.isFalse(redirectStub.called); + assert.isTrue(setParamsStub.calledOnce); + assert.deepEqual(setParamsStub.lastCall.args[0], { + view: Gerrit.Nav.View.DASHBOARD, + user: 'self', + sections: [ + {name: 'a', query: 'is:open b'}, + ], + title: 'Custom Dashboard', + }); + }); + }); + }); + + suite('group routes', () => { + test('_handleGroupInfoRoute', () => { + const data = {params: {0: 1234}}; + element._handleGroupInfoRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/admin/groups/1234'); + }); + + test('_handleGroupAuditLogRoute', () => { + const data = {params: {0: 1234}}; + assertDataToParams(data, '_handleGroupAuditLogRoute', { + view: Gerrit.Nav.View.GROUP, + detail: 'log', + groupId: 1234, + }); + }); + + test('_handleGroupMembersRoute', () => { + const data = {params: {0: 1234}}; + assertDataToParams(data, '_handleGroupMembersRoute', { + view: Gerrit.Nav.View.GROUP, + detail: 'members', + groupId: 1234, + }); + }); + + test('_handleGroupListOffsetRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handleGroupListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 0, + filter: null, + openCreateModal: false, }); - test('_handleGroupAuditLogRoute', () => { - const data = {params: {0: 1234}}; - assertDataToParams(data, '_handleGroupAuditLogRoute', { - view: Gerrit.Nav.View.GROUP, - detail: 'log', - groupId: 1234, + data.params[1] = 42; + assertDataToParams(data, '_handleGroupListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 42, + filter: null, + openCreateModal: false, + }); + + data.hash = 'create'; + assertDataToParams(data, '_handleGroupListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 42, + filter: null, + openCreateModal: true, + }); + }); + + test('_handleGroupListFilterOffsetRoute', () => { + const data = {params: {filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handleGroupListFilterOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + offset: 42, + filter: 'foo', + }); + }); + + test('_handleGroupListFilterRoute', () => { + const data = {params: {filter: 'foo'}}; + assertDataToParams(data, '_handleGroupListFilterRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-admin-group-list', + filter: 'foo', + }); + }); + + test('_handleGroupRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleGroupRoute', { + view: Gerrit.Nav.View.GROUP, + groupId: 4321, + }); + }); + }); + + suite('repo routes', () => { + test('_handleProjectsOldRoute', () => { + const data = {params: {}}; + element._handleProjectsOldRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/admin/repos/'); + }); + + test('_handleProjectsOldRoute test', () => { + const data = {params: {1: 'test'}}; + element._handleProjectsOldRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test'); + }); + + test('_handleProjectsOldRoute test,branches', () => { + const data = {params: {1: 'test,branches'}}; + element._handleProjectsOldRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.equal( + redirectStub.lastCall.args[0], '/admin/repos/test,branches'); + }); + + test('_handleRepoRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleRepoRoute', { + view: Gerrit.Nav.View.REPO, + repo: 4321, + }); + }); + + test('_handleRepoCommandsRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleRepoCommandsRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.COMMANDS, + repo: 4321, + }); + }); + + test('_handleRepoAccessRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleRepoAccessRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.ACCESS, + repo: 4321, + }); + }); + + suite('branch list routes', () => { + test('_handleBranchListOffsetRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleBranchListOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + offset: 0, + filter: null, + }); + + data.params[2] = 42; + assertDataToParams(data, '_handleBranchListOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + offset: 42, + filter: null, }); }); - test('_handleGroupMembersRoute', () => { - const data = {params: {0: 1234}}; - assertDataToParams(data, '_handleGroupMembersRoute', { - view: Gerrit.Nav.View.GROUP, - detail: 'members', - groupId: 1234, + test('_handleBranchListFilterOffsetRoute', () => { + const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handleBranchListFilterOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + offset: 42, + filter: 'foo', }); }); - test('_handleGroupListOffsetRoute', () => { + test('_handleBranchListFilterRoute', () => { + const data = {params: {repo: 4321, filter: 'foo'}}; + assertDataToParams(data, '_handleBranchListFilterRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.BRANCHES, + repo: 4321, + filter: 'foo', + }); + }); + }); + + suite('tag list routes', () => { + test('_handleTagListOffsetRoute', () => { + const data = {params: {0: 4321}}; + assertDataToParams(data, '_handleTagListOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + offset: 0, + filter: null, + }); + }); + + test('_handleTagListFilterOffsetRoute', () => { + const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handleTagListFilterOffsetRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + offset: 42, + filter: 'foo', + }); + }); + + test('_handleTagListFilterRoute', () => { + const data = {params: {repo: 4321}}; + assertDataToParams(data, '_handleTagListFilterRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + filter: null, + }); + + data.params.filter = 'foo'; + assertDataToParams(data, '_handleTagListFilterRoute', { + view: Gerrit.Nav.View.REPO, + detail: Gerrit.Nav.RepoDetailView.TAGS, + repo: 4321, + filter: 'foo', + }); + }); + }); + + suite('repo list routes', () => { + test('_handleRepoListOffsetRoute', () => { const data = {params: {}}; - assertDataToParams(data, '_handleGroupListOffsetRoute', { + assertDataToParams(data, '_handleRepoListOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 0, filter: null, openCreateModal: false, }); data.params[1] = 42; - assertDataToParams(data, '_handleGroupListOffsetRoute', { + assertDataToParams(data, '_handleRepoListOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 42, filter: null, openCreateModal: false, }); data.hash = 'create'; - assertDataToParams(data, '_handleGroupListOffsetRoute', { + assertDataToParams(data, '_handleRepoListOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 42, filter: null, openCreateModal: true, }); }); - test('_handleGroupListFilterOffsetRoute', () => { + test('_handleRepoListFilterOffsetRoute', () => { const data = {params: {filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleGroupListFilterOffsetRoute', { + assertDataToParams(data, '_handleRepoListFilterOffsetRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', + adminView: 'gr-repo-list', offset: 42, filter: 'foo', }); }); - test('_handleGroupListFilterRoute', () => { - const data = {params: {filter: 'foo'}}; - assertDataToParams(data, '_handleGroupListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-admin-group-list', - filter: 'foo', - }); - }); - - test('_handleGroupRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleGroupRoute', { - view: Gerrit.Nav.View.GROUP, - groupId: 4321, - }); - }); - }); - - suite('repo routes', () => { - test('_handleProjectsOldRoute', () => { + test('_handleRepoListFilterRoute', () => { const data = {params: {}}; - element._handleProjectsOldRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/admin/repos/'); - }); - - test('_handleProjectsOldRoute test', () => { - const data = {params: {1: 'test'}}; - element._handleProjectsOldRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal(redirectStub.lastCall.args[0], '/admin/repos/test'); - }); - - test('_handleProjectsOldRoute test,branches', () => { - const data = {params: {1: 'test,branches'}}; - element._handleProjectsOldRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.equal( - redirectStub.lastCall.args[0], '/admin/repos/test,branches'); - }); - - test('_handleRepoRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleRepoRoute', { - view: Gerrit.Nav.View.REPO, - repo: 4321, - }); - }); - - test('_handleRepoCommandsRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleRepoCommandsRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.COMMANDS, - repo: 4321, - }); - }); - - test('_handleRepoAccessRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleRepoAccessRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.ACCESS, - repo: 4321, - }); - }); - - suite('branch list routes', () => { - test('_handleBranchListOffsetRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleBranchListOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - offset: 0, - filter: null, - }); - - data.params[2] = 42; - assertDataToParams(data, '_handleBranchListOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - offset: 42, - filter: null, - }); - }); - - test('_handleBranchListFilterOffsetRoute', () => { - const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleBranchListFilterOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - offset: 42, - filter: 'foo', - }); - }); - - test('_handleBranchListFilterRoute', () => { - const data = {params: {repo: 4321, filter: 'foo'}}; - assertDataToParams(data, '_handleBranchListFilterRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.BRANCHES, - repo: 4321, - filter: 'foo', - }); - }); - }); - - suite('tag list routes', () => { - test('_handleTagListOffsetRoute', () => { - const data = {params: {0: 4321}}; - assertDataToParams(data, '_handleTagListOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - offset: 0, - filter: null, - }); - }); - - test('_handleTagListFilterOffsetRoute', () => { - const data = {params: {repo: 4321, filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleTagListFilterOffsetRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - offset: 42, - filter: 'foo', - }); - }); - - test('_handleTagListFilterRoute', () => { - const data = {params: {repo: 4321}}; - assertDataToParams(data, '_handleTagListFilterRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - filter: null, - }); - - data.params.filter = 'foo'; - assertDataToParams(data, '_handleTagListFilterRoute', { - view: Gerrit.Nav.View.REPO, - detail: Gerrit.Nav.RepoDetailView.TAGS, - repo: 4321, - filter: 'foo', - }); - }); - }); - - suite('repo list routes', () => { - test('_handleRepoListOffsetRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handleRepoListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 0, - filter: null, - openCreateModal: false, - }); - - data.params[1] = 42; - assertDataToParams(data, '_handleRepoListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 42, - filter: null, - openCreateModal: false, - }); - - data.hash = 'create'; - assertDataToParams(data, '_handleRepoListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 42, - filter: null, - openCreateModal: true, - }); - }); - - test('_handleRepoListFilterOffsetRoute', () => { - const data = {params: {filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handleRepoListFilterOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - offset: 42, - filter: 'foo', - }); - }); - - test('_handleRepoListFilterRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handleRepoListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - filter: null, - }); - - data.params.filter = 'foo'; - assertDataToParams(data, '_handleRepoListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-repo-list', - filter: 'foo', - }); - }); - }); - }); - - suite('plugin routes', () => { - test('_handlePluginListOffsetRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handlePluginListOffsetRoute', { + assertDataToParams(data, '_handleRepoListFilterRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: 0, - filter: null, - }); - - data.params[1] = 42; - assertDataToParams(data, '_handlePluginListOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: 42, - filter: null, - }); - }); - - test('_handlePluginListFilterOffsetRoute', () => { - const data = {params: {filter: 'foo', offset: 42}}; - assertDataToParams(data, '_handlePluginListFilterOffsetRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - offset: 42, - filter: 'foo', - }); - }); - - test('_handlePluginListFilterRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handlePluginListFilterRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', + adminView: 'gr-repo-list', filter: null, }); data.params.filter = 'foo'; - assertDataToParams(data, '_handlePluginListFilterRoute', { + assertDataToParams(data, '_handleRepoListFilterRoute', { view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', + adminView: 'gr-repo-list', filter: 'foo', }); }); - - test('_handlePluginListRoute', () => { - const data = {params: {}}; - assertDataToParams(data, '_handlePluginListRoute', { - view: Gerrit.Nav.View.ADMIN, - adminView: 'gr-plugin-list', - }); - }); - }); - - suite('change/diff routes', () => { - test('_handleChangeNumberLegacyRoute', () => { - const data = {params: {0: 12345}}; - element._handleChangeNumberLegacyRoute(data); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly('/c/12345')); - }); - - test('_handleChangeLegacyRoute', () => { - const normalizeRouteStub = sandbox.stub(element, - '_normalizeLegacyRouteParams'); - const ctx = { - params: [ - 1234, // 0 Change number - null, // 1 Unused - null, // 2 Unused - 6, // 3 Base patch number - null, // 4 Unused - 9, // 5 Patch number - ], - querystring: '', - }; - element._handleChangeLegacyRoute(ctx); - assert.isTrue(normalizeRouteStub.calledOnce); - assert.deepEqual(normalizeRouteStub.lastCall.args[0], { - changeNum: 1234, - basePatchNum: 6, - patchNum: 9, - view: Gerrit.Nav.View.CHANGE, - querystring: '', - }); - }); - - test('_handleDiffLegacyRoute', () => { - const normalizeRouteStub = sandbox.stub(element, - '_normalizeLegacyRouteParams'); - const ctx = { - params: [ - 1234, // 0 Change number - null, // 1 Unused - 3, // 2 Base patch number - null, // 3 Unused - 8, // 4 Patch number - 'foo/bar', // 5 Diff path - ], - path: '/c/1234/3..8/foo/bar', - hash: 'b123', - }; - element._handleDiffLegacyRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRouteStub.calledOnce); - assert.deepEqual(normalizeRouteStub.lastCall.args[0], { - changeNum: 1234, - basePatchNum: 3, - patchNum: 8, - view: Gerrit.Nav.View.DIFF, - path: 'foo/bar', - lineNum: 123, - leftSide: true, - }); - }); - - test('_handleLegacyLinenum w/ @321', () => { - const ctx = {path: '/c/1234/3..8/foo/bar@321'}; - element._handleLegacyLinenum(ctx); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly( - '/c/1234/3..8/foo/bar#321')); - }); - - test('_handleLegacyLinenum w/ @b123', () => { - const ctx = {path: '/c/1234/3..8/foo/bar@b123'}; - element._handleLegacyLinenum(ctx); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly( - '/c/1234/3..8/foo/bar#b123')); - }); - - suite('_handleChangeRoute', () => { - let normalizeRangeStub; - - function makeParams(path, hash) { - return { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - null, // 2 Unused - null, // 3 Unused - 4, // 4 Base patch number - null, // 5 Unused - 7, // 6 Patch number - ], - }; - } - - setup(() => { - normalizeRangeStub = sandbox.stub(element, - '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - }); - - test('needs redirect', () => { - normalizeRangeStub.returns(true); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams(null, ''); - element._handleChangeRoute(ctx); - assert.isTrue(normalizeRangeStub.called); - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly('foo')); - }); - - test('change view', () => { - normalizeRangeStub.returns(false); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams(null, ''); - assertDataToParams(ctx, '_handleChangeRoute', { - view: Gerrit.Nav.View.CHANGE, - project: 'foo/bar', - changeNum: 1234, - basePatchNum: 4, - patchNum: 7, - }); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeStub.called); - }); - }); - - suite('_handleDiffRoute', () => { - let normalizeRangeStub; - - function makeParams(path, hash) { - return { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - null, // 2 Unused - null, // 3 Unused - 4, // 4 Base patch number - null, // 5 Unused - 7, // 6 Patch number - null, // 7 Unused, - path, // 8 Diff path - ], - hash, - }; - } - - setup(() => { - normalizeRangeStub = sandbox.stub(element, - '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - }); - - test('needs redirect', () => { - normalizeRangeStub.returns(true); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams(null, ''); - element._handleDiffRoute(ctx); - assert.isTrue(normalizeRangeStub.called); - assert.isFalse(setParamsStub.called); - assert.isTrue(redirectStub.calledOnce); - assert.isTrue(redirectStub.calledWithExactly('foo')); - }); - - test('diff view', () => { - normalizeRangeStub.returns(false); - sandbox.stub(element, '_generateUrl').returns('foo'); - const ctx = makeParams('foo/bar/baz', 'b44'); - assertDataToParams(ctx, '_handleDiffRoute', { - view: Gerrit.Nav.View.DIFF, - project: 'foo/bar', - changeNum: 1234, - basePatchNum: 4, - patchNum: 7, - path: 'foo/bar/baz', - leftSide: true, - lineNum: 44, - }); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeStub.called); - }); - }); - - test('_handleDiffEditRoute', () => { - const normalizeRangeSpy = - sandbox.spy(element, '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - const ctx = { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - 3, // 2 Patch num - 'foo/bar/baz', // 3 File path - ], - }; - const appParams = { - project: 'foo/bar', - changeNum: 1234, - view: Gerrit.Nav.View.EDIT, - path: 'foo/bar/baz', - patchNum: 3, - lineNum: undefined, - }; - - element._handleDiffEditRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeSpy.calledOnce); - assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); - assert.isFalse(normalizeRangeSpy.lastCall.returnValue); - assert.deepEqual(setParamsStub.lastCall.args[0], appParams); - }); - - test('_handleDiffEditRoute with lineNum', () => { - const normalizeRangeSpy = - sandbox.spy(element, '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - const ctx = { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - 3, // 2 Patch num - 'foo/bar/baz', // 3 File path - ], - hash: 4, - }; - const appParams = { - project: 'foo/bar', - changeNum: 1234, - view: Gerrit.Nav.View.EDIT, - path: 'foo/bar/baz', - patchNum: 3, - lineNum: 4, - }; - - element._handleDiffEditRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeSpy.calledOnce); - assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); - assert.isFalse(normalizeRangeSpy.lastCall.returnValue); - assert.deepEqual(setParamsStub.lastCall.args[0], appParams); - }); - - test('_handleChangeEditRoute', () => { - const normalizeRangeSpy = - sandbox.spy(element, '_normalizePatchRangeParams'); - sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - const ctx = { - params: [ - 'foo/bar', // 0 Project - 1234, // 1 Change number - null, - 3, // 3 Patch num - ], - }; - const appParams = { - project: 'foo/bar', - changeNum: 1234, - view: Gerrit.Nav.View.CHANGE, - patchNum: 3, - edit: true, - }; - - element._handleChangeEditRoute(ctx); - assert.isFalse(redirectStub.called); - assert.isTrue(normalizeRangeSpy.calledOnce); - assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); - assert.isFalse(normalizeRangeSpy.lastCall.returnValue); - assert.deepEqual(setParamsStub.lastCall.args[0], appParams); - }); - }); - - test('_handlePluginScreen', () => { - const ctx = {params: ['foo', 'bar']}; - assertDataToParams(ctx, '_handlePluginScreen', { - view: Gerrit.Nav.View.PLUGIN_SCREEN, - plugin: 'foo', - screen: 'bar', - }); - assert.isFalse(redirectStub.called); }); }); - suite('_parseQueryString', () => { - test('empty queries', () => { - assert.deepEqual(element._parseQueryString(''), []); - assert.deepEqual(element._parseQueryString('?'), []); - assert.deepEqual(element._parseQueryString('??'), []); - assert.deepEqual(element._parseQueryString('&&&'), []); + suite('plugin routes', () => { + test('_handlePluginListOffsetRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handlePluginListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: 0, + filter: null, + }); + + data.params[1] = 42; + assertDataToParams(data, '_handlePluginListOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: 42, + filter: null, + }); }); - test('url decoding', () => { - assert.deepEqual(element._parseQueryString('+'), [[' ', '']]); - assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]); - assert.deepEqual( - element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'), - [['name', 'value']]); + test('_handlePluginListFilterOffsetRoute', () => { + const data = {params: {filter: 'foo', offset: 42}}; + assertDataToParams(data, '_handlePluginListFilterOffsetRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + offset: 42, + filter: 'foo', + }); }); - test('multiple parameters', () => { - assert.deepEqual( - element._parseQueryString('a=b&c=d&e=f'), - [['a', 'b'], ['c', 'd'], ['e', 'f']]); - assert.deepEqual( - element._parseQueryString('&a=b&&&e=f&'), - [['a', 'b'], ['e', 'f']]); + test('_handlePluginListFilterRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handlePluginListFilterRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + filter: null, + }); + + data.params.filter = 'foo'; + assertDataToParams(data, '_handlePluginListFilterRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + filter: 'foo', + }); }); + + test('_handlePluginListRoute', () => { + const data = {params: {}}; + assertDataToParams(data, '_handlePluginListRoute', { + view: Gerrit.Nav.View.ADMIN, + adminView: 'gr-plugin-list', + }); + }); + }); + + suite('change/diff routes', () => { + test('_handleChangeNumberLegacyRoute', () => { + const data = {params: {0: 12345}}; + element._handleChangeNumberLegacyRoute(data); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly('/c/12345')); + }); + + test('_handleChangeLegacyRoute', () => { + const normalizeRouteStub = sandbox.stub(element, + '_normalizeLegacyRouteParams'); + const ctx = { + params: [ + 1234, // 0 Change number + null, // 1 Unused + null, // 2 Unused + 6, // 3 Base patch number + null, // 4 Unused + 9, // 5 Patch number + ], + querystring: '', + }; + element._handleChangeLegacyRoute(ctx); + assert.isTrue(normalizeRouteStub.calledOnce); + assert.deepEqual(normalizeRouteStub.lastCall.args[0], { + changeNum: 1234, + basePatchNum: 6, + patchNum: 9, + view: Gerrit.Nav.View.CHANGE, + querystring: '', + }); + }); + + test('_handleDiffLegacyRoute', () => { + const normalizeRouteStub = sandbox.stub(element, + '_normalizeLegacyRouteParams'); + const ctx = { + params: [ + 1234, // 0 Change number + null, // 1 Unused + 3, // 2 Base patch number + null, // 3 Unused + 8, // 4 Patch number + 'foo/bar', // 5 Diff path + ], + path: '/c/1234/3..8/foo/bar', + hash: 'b123', + }; + element._handleDiffLegacyRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRouteStub.calledOnce); + assert.deepEqual(normalizeRouteStub.lastCall.args[0], { + changeNum: 1234, + basePatchNum: 3, + patchNum: 8, + view: Gerrit.Nav.View.DIFF, + path: 'foo/bar', + lineNum: 123, + leftSide: true, + }); + }); + + test('_handleLegacyLinenum w/ @321', () => { + const ctx = {path: '/c/1234/3..8/foo/bar@321'}; + element._handleLegacyLinenum(ctx); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly( + '/c/1234/3..8/foo/bar#321')); + }); + + test('_handleLegacyLinenum w/ @b123', () => { + const ctx = {path: '/c/1234/3..8/foo/bar@b123'}; + element._handleLegacyLinenum(ctx); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly( + '/c/1234/3..8/foo/bar#b123')); + }); + + suite('_handleChangeRoute', () => { + let normalizeRangeStub; + + function makeParams(path, hash) { + return { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + null, // 2 Unused + null, // 3 Unused + 4, // 4 Base patch number + null, // 5 Unused + 7, // 6 Patch number + ], + }; + } + + setup(() => { + normalizeRangeStub = sandbox.stub(element, + '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + }); + + test('needs redirect', () => { + normalizeRangeStub.returns(true); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams(null, ''); + element._handleChangeRoute(ctx); + assert.isTrue(normalizeRangeStub.called); + assert.isFalse(setParamsStub.called); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly('foo')); + }); + + test('change view', () => { + normalizeRangeStub.returns(false); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams(null, ''); + assertDataToParams(ctx, '_handleChangeRoute', { + view: Gerrit.Nav.View.CHANGE, + project: 'foo/bar', + changeNum: 1234, + basePatchNum: 4, + patchNum: 7, + }); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeStub.called); + }); + }); + + suite('_handleDiffRoute', () => { + let normalizeRangeStub; + + function makeParams(path, hash) { + return { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + null, // 2 Unused + null, // 3 Unused + 4, // 4 Base patch number + null, // 5 Unused + 7, // 6 Patch number + null, // 7 Unused, + path, // 8 Diff path + ], + hash, + }; + } + + setup(() => { + normalizeRangeStub = sandbox.stub(element, + '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + }); + + test('needs redirect', () => { + normalizeRangeStub.returns(true); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams(null, ''); + element._handleDiffRoute(ctx); + assert.isTrue(normalizeRangeStub.called); + assert.isFalse(setParamsStub.called); + assert.isTrue(redirectStub.calledOnce); + assert.isTrue(redirectStub.calledWithExactly('foo')); + }); + + test('diff view', () => { + normalizeRangeStub.returns(false); + sandbox.stub(element, '_generateUrl').returns('foo'); + const ctx = makeParams('foo/bar/baz', 'b44'); + assertDataToParams(ctx, '_handleDiffRoute', { + view: Gerrit.Nav.View.DIFF, + project: 'foo/bar', + changeNum: 1234, + basePatchNum: 4, + patchNum: 7, + path: 'foo/bar/baz', + leftSide: true, + lineNum: 44, + }); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeStub.called); + }); + }); + + test('_handleDiffEditRoute', () => { + const normalizeRangeSpy = + sandbox.spy(element, '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + const ctx = { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + 3, // 2 Patch num + 'foo/bar/baz', // 3 File path + ], + }; + const appParams = { + project: 'foo/bar', + changeNum: 1234, + view: Gerrit.Nav.View.EDIT, + path: 'foo/bar/baz', + patchNum: 3, + lineNum: undefined, + }; + + element._handleDiffEditRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeSpy.calledOnce); + assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); + assert.isFalse(normalizeRangeSpy.lastCall.returnValue); + assert.deepEqual(setParamsStub.lastCall.args[0], appParams); + }); + + test('_handleDiffEditRoute with lineNum', () => { + const normalizeRangeSpy = + sandbox.spy(element, '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + const ctx = { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + 3, // 2 Patch num + 'foo/bar/baz', // 3 File path + ], + hash: 4, + }; + const appParams = { + project: 'foo/bar', + changeNum: 1234, + view: Gerrit.Nav.View.EDIT, + path: 'foo/bar/baz', + patchNum: 3, + lineNum: 4, + }; + + element._handleDiffEditRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeSpy.calledOnce); + assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); + assert.isFalse(normalizeRangeSpy.lastCall.returnValue); + assert.deepEqual(setParamsStub.lastCall.args[0], appParams); + }); + + test('_handleChangeEditRoute', () => { + const normalizeRangeSpy = + sandbox.spy(element, '_normalizePatchRangeParams'); + sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + const ctx = { + params: [ + 'foo/bar', // 0 Project + 1234, // 1 Change number + null, + 3, // 3 Patch num + ], + }; + const appParams = { + project: 'foo/bar', + changeNum: 1234, + view: Gerrit.Nav.View.CHANGE, + patchNum: 3, + edit: true, + }; + + element._handleChangeEditRoute(ctx); + assert.isFalse(redirectStub.called); + assert.isTrue(normalizeRangeSpy.calledOnce); + assert.deepEqual(normalizeRangeSpy.lastCall.args[0], appParams); + assert.isFalse(normalizeRangeSpy.lastCall.returnValue); + assert.deepEqual(setParamsStub.lastCall.args[0], appParams); + }); + }); + + test('_handlePluginScreen', () => { + const ctx = {params: ['foo', 'bar']}; + assertDataToParams(ctx, '_handlePluginScreen', { + view: Gerrit.Nav.View.PLUGIN_SCREEN, + plugin: 'foo', + screen: 'bar', + }); + assert.isFalse(redirectStub.called); }); }); + + suite('_parseQueryString', () => { + test('empty queries', () => { + assert.deepEqual(element._parseQueryString(''), []); + assert.deepEqual(element._parseQueryString('?'), []); + assert.deepEqual(element._parseQueryString('??'), []); + assert.deepEqual(element._parseQueryString('&&&'), []); + }); + + test('url decoding', () => { + assert.deepEqual(element._parseQueryString('+'), [[' ', '']]); + assert.deepEqual(element._parseQueryString('???+%3d+'), [[' = ', '']]); + assert.deepEqual( + element._parseQueryString('%6e%61%6d%65=%76%61%6c%75%65'), + [['name', 'value']]); + }); + + test('multiple parameters', () => { + assert.deepEqual( + element._parseQueryString('a=b&c=d&e=f'), + [['a', 'b'], ['c', 'd'], ['e', 'f']]); + assert.deepEqual( + element._parseQueryString('&a=b&&&e=f&'), + [['a', 'b'], ['e', 'f']]); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js index 41caab5..0ed5291 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,320 +14,332 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; - // Possible static search options for auto complete, without negations. - const SEARCH_OPERATORS = [ - 'added:', - 'age:', - 'age:1week', // Give an example age - 'assignee:', - 'author:', - 'branch:', - 'bug:', - 'cc:', - 'cc:self', - 'change:', - 'cherrypickof:', - 'comment:', - 'commentby:', - 'commit:', - 'committer:', - 'conflicts:', - 'deleted:', - 'delta:', - 'dir:', - 'directory:', - 'ext:', - 'extension:', - 'file:', - 'footer:', - 'from:', - 'has:', - 'has:draft', - 'has:edit', - 'has:star', - 'has:stars', - 'has:unresolved', - 'hashtag:', - 'intopic:', - 'is:', - 'is:abandoned', - 'is:assigned', - 'is:closed', - 'is:ignored', - 'is:merged', - 'is:open', - 'is:owner', - 'is:private', - 'is:reviewed', - 'is:reviewer', - 'is:starred', - 'is:submittable', - 'is:watched', - 'is:wip', - 'label:', - 'message:', - 'onlyexts:', - 'onlyextensions:', - 'owner:', - 'ownerin:', - 'parentproject:', - 'project:', - 'projects:', - 'query:', - 'ref:', - 'reviewedby:', - 'reviewer:', - 'reviewer:self', - 'reviewerin:', - 'size:', - 'star:', - 'status:', - 'status:abandoned', - 'status:closed', - 'status:merged', - 'status:open', - 'status:reviewed', - 'submissionid:', - 'topic:', - 'tr:', - ]; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-search-bar_html.js'; - // All of the ops, with corresponding negations. - const SEARCH_OPERATORS_WITH_NEGATIONS_SET = - new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`))); +// Possible static search options for auto complete, without negations. +const SEARCH_OPERATORS = [ + 'added:', + 'age:', + 'age:1week', // Give an example age + 'assignee:', + 'author:', + 'branch:', + 'bug:', + 'cc:', + 'cc:self', + 'change:', + 'cherrypickof:', + 'comment:', + 'commentby:', + 'commit:', + 'committer:', + 'conflicts:', + 'deleted:', + 'delta:', + 'dir:', + 'directory:', + 'ext:', + 'extension:', + 'file:', + 'footer:', + 'from:', + 'has:', + 'has:draft', + 'has:edit', + 'has:star', + 'has:stars', + 'has:unresolved', + 'hashtag:', + 'intopic:', + 'is:', + 'is:abandoned', + 'is:assigned', + 'is:closed', + 'is:ignored', + 'is:merged', + 'is:open', + 'is:owner', + 'is:private', + 'is:reviewed', + 'is:reviewer', + 'is:starred', + 'is:submittable', + 'is:watched', + 'is:wip', + 'label:', + 'message:', + 'onlyexts:', + 'onlyextensions:', + 'owner:', + 'ownerin:', + 'parentproject:', + 'project:', + 'projects:', + 'query:', + 'ref:', + 'reviewedby:', + 'reviewer:', + 'reviewer:self', + 'reviewerin:', + 'size:', + 'star:', + 'status:', + 'status:abandoned', + 'status:closed', + 'status:merged', + 'status:open', + 'status:reviewed', + 'submissionid:', + 'topic:', + 'tr:', +]; - const MAX_AUTOCOMPLETE_RESULTS = 10; +// All of the ops, with corresponding negations. +const SEARCH_OPERATORS_WITH_NEGATIONS_SET = + new Set(SEARCH_OPERATORS.concat(SEARCH_OPERATORS.map(op => `-${op}`))); - const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; +const MAX_AUTOCOMPLETE_RESULTS = 10; +const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+\s*/g; + +/** + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrSearchBar extends mixinBehaviors( [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-search-bar'; } /** - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element + * Fired when a search is committed + * + * @event handle-search */ - class GrSearchBar extends Polymer.mixinBehaviors( [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-search-bar'; } - /** - * Fired when a search is committed - * - * @event handle-search - */ - static get properties() { - return { - value: { - type: String, - value: '', - notify: true, - observer: '_valueChanged', + static get properties() { + return { + value: { + type: String, + value: '', + notify: true, + observer: '_valueChanged', + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + query: { + type: Function, + value() { + return this._getSearchSuggestions.bind(this); }, - keyEventTarget: { - type: Object, - value() { return document.body; }, + }, + projectSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); }, - query: { - type: Function, - value() { - return this._getSearchSuggestions.bind(this); - }, + }, + groupSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); }, - projectSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); - }, + }, + accountSuggestions: { + type: Function, + value() { + return () => Promise.resolve([]); }, - groupSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); - }, - }, - accountSuggestions: { - type: Function, - value() { - return () => Promise.resolve([]); - }, - }, - _inputVal: String, - _threshold: { - type: Number, - value: 1, - }, - }; - } + }, + _inputVal: String, + _threshold: { + type: Number, + value: 1, + }, + }; + } - attached() { - super.attached(); - this.$.restAPI.getConfig().then(serverConfig => { - const mergeability = serverConfig - && serverConfig.index - && serverConfig.index.mergeabilityComputationBehavior; - if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX' - || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') { - // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET - this._addOperator('is:mergeable'); - } - }); - } - - _addOperator(name, include_neg = true) { - SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name); - if (include_neg) { - SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`); + attached() { + super.attached(); + this.$.restAPI.getConfig().then(serverConfig => { + const mergeability = serverConfig + && serverConfig.index + && serverConfig.index.mergeabilityComputationBehavior; + if (mergeability === 'API_REF_UPDATED_AND_CHANGE_REINDEX' + || mergeability === 'REF_UPDATED_AND_CHANGE_REINDEX') { + // add 'is:mergeable' to SEARCH_OPERATORS_WITH_NEGATIONS_SET + this._addOperator('is:mergeable'); } - } + }); + } - keyboardShortcuts() { - return { - [this.Shortcut.SEARCH]: '_handleSearch', - }; - } - - _valueChanged(value) { - this._inputVal = value; - } - - _handleInputCommit(e) { - this._preventDefaultAndNavigateToInputVal(e); - } - - /** - * This function is called in a few different cases: - * - e.target is the search button - * - e.target is the gr-autocomplete widget (#searchInput) - * - e.target is the input element wrapped within #searchInput - * - * @param {!Event} e - */ - _preventDefaultAndNavigateToInputVal(e) { - e.preventDefault(); - const target = Polymer.dom(e).rootTarget; - // If the target is the #searchInput or has a sub-input component, that - // is what holds the focus as opposed to the target from the DOM event. - if (target.$.input) { - target.$.input.blur(); - } else { - target.blur(); - } - const trimmedInput = this._inputVal && this._inputVal.trim(); - if (trimmedInput) { - const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET] - .some(op => op.endsWith(':') && op === trimmedInput); - if (predefinedOpOnlyQuery) { - return; - } - this.dispatchEvent(new CustomEvent('handle-search', { - detail: {inputVal: this._inputVal}, - })); - } - } - - /** - * Determine what array of possible suggestions should be provided - * to _getSearchSuggestions. - * - * @param {string} input - The full search term, in lowercase. - * @return {!Promise} This returns a promise that resolves to an array of - * suggestion objects. - */ - _fetchSuggestions(input) { - // Split the input on colon to get a two part predicate/expression. - const splitInput = input.split(':'); - const predicate = splitInput[0]; - const expression = splitInput[1] || ''; - // Switch on the predicate to determine what to autocomplete. - switch (predicate) { - case 'ownerin': - case 'reviewerin': - // Fetch groups. - return this.groupSuggestions(predicate, expression); - - case 'parentproject': - case 'project': - // Fetch projects. - return this.projectSuggestions(predicate, expression); - - case 'author': - case 'cc': - case 'commentby': - case 'committer': - case 'from': - case 'owner': - case 'reviewedby': - case 'reviewer': - // Fetch accounts. - return this.accountSuggestions(predicate, expression); - - default: - return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET] - .filter(operator => operator.includes(input)) - .map(operator => { return {text: operator}; })); - } - } - - /** - * Get the sorted, pruned list of suggestions for the current search query. - * - * @param {string} input - The complete search query. - * @return {!Promise} This returns a promise that resolves to an array of - * suggestions. - */ - _getSearchSuggestions(input) { - // Allow spaces within quoted terms. - const tokens = input.match(TOKENIZE_REGEX); - const trimmedInput = tokens[tokens.length - 1].toLowerCase(); - - return this._fetchSuggestions(trimmedInput) - .then(suggestions => { - if (!suggestions || !suggestions.length) { return []; } - return suggestions - // Prioritize results that start with the input. - .sort((a, b) => { - const aContains = a.text.toLowerCase().indexOf(trimmedInput); - const bContains = b.text.toLowerCase().indexOf(trimmedInput); - if (aContains === bContains) { - return a.text.localeCompare(b.text); - } - if (aContains === -1) { - return 1; - } - if (bContains === -1) { - return -1; - } - return aContains - bContains; - }) - // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results. - .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1) - // Map to an object to play nice with gr-autocomplete. - .map(({text, label}) => { - return { - name: text, - value: text, - label, - }; - }); - }); - } - - _handleSearch(e) { - const keyboardEvent = this.getKeyboardEvent(e); - if (this.shouldSuppressKeyboardShortcut(e) || - (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; } - - e.preventDefault(); - this.$.searchInput.focus(); - this.$.searchInput.selectAll(); + _addOperator(name, include_neg = true) { + SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(name); + if (include_neg) { + SEARCH_OPERATORS_WITH_NEGATIONS_SET.add(`-${name}`); } } - customElements.define(GrSearchBar.is, GrSearchBar); -})(); + keyboardShortcuts() { + return { + [this.Shortcut.SEARCH]: '_handleSearch', + }; + } + + _valueChanged(value) { + this._inputVal = value; + } + + _handleInputCommit(e) { + this._preventDefaultAndNavigateToInputVal(e); + } + + /** + * This function is called in a few different cases: + * - e.target is the search button + * - e.target is the gr-autocomplete widget (#searchInput) + * - e.target is the input element wrapped within #searchInput + * + * @param {!Event} e + */ + _preventDefaultAndNavigateToInputVal(e) { + e.preventDefault(); + const target = dom(e).rootTarget; + // If the target is the #searchInput or has a sub-input component, that + // is what holds the focus as opposed to the target from the DOM event. + if (target.$.input) { + target.$.input.blur(); + } else { + target.blur(); + } + const trimmedInput = this._inputVal && this._inputVal.trim(); + if (trimmedInput) { + const predefinedOpOnlyQuery = [...SEARCH_OPERATORS_WITH_NEGATIONS_SET] + .some(op => op.endsWith(':') && op === trimmedInput); + if (predefinedOpOnlyQuery) { + return; + } + this.dispatchEvent(new CustomEvent('handle-search', { + detail: {inputVal: this._inputVal}, + })); + } + } + + /** + * Determine what array of possible suggestions should be provided + * to _getSearchSuggestions. + * + * @param {string} input - The full search term, in lowercase. + * @return {!Promise} This returns a promise that resolves to an array of + * suggestion objects. + */ + _fetchSuggestions(input) { + // Split the input on colon to get a two part predicate/expression. + const splitInput = input.split(':'); + const predicate = splitInput[0]; + const expression = splitInput[1] || ''; + // Switch on the predicate to determine what to autocomplete. + switch (predicate) { + case 'ownerin': + case 'reviewerin': + // Fetch groups. + return this.groupSuggestions(predicate, expression); + + case 'parentproject': + case 'project': + // Fetch projects. + return this.projectSuggestions(predicate, expression); + + case 'author': + case 'cc': + case 'commentby': + case 'committer': + case 'from': + case 'owner': + case 'reviewedby': + case 'reviewer': + // Fetch accounts. + return this.accountSuggestions(predicate, expression); + + default: + return Promise.resolve([...SEARCH_OPERATORS_WITH_NEGATIONS_SET] + .filter(operator => operator.includes(input)) + .map(operator => { return {text: operator}; })); + } + } + + /** + * Get the sorted, pruned list of suggestions for the current search query. + * + * @param {string} input - The complete search query. + * @return {!Promise} This returns a promise that resolves to an array of + * suggestions. + */ + _getSearchSuggestions(input) { + // Allow spaces within quoted terms. + const tokens = input.match(TOKENIZE_REGEX); + const trimmedInput = tokens[tokens.length - 1].toLowerCase(); + + return this._fetchSuggestions(trimmedInput) + .then(suggestions => { + if (!suggestions || !suggestions.length) { return []; } + return suggestions + // Prioritize results that start with the input. + .sort((a, b) => { + const aContains = a.text.toLowerCase().indexOf(trimmedInput); + const bContains = b.text.toLowerCase().indexOf(trimmedInput); + if (aContains === bContains) { + return a.text.localeCompare(b.text); + } + if (aContains === -1) { + return 1; + } + if (bContains === -1) { + return -1; + } + return aContains - bContains; + }) + // Return only the first {MAX_AUTOCOMPLETE_RESULTS} results. + .slice(0, MAX_AUTOCOMPLETE_RESULTS - 1) + // Map to an object to play nice with gr-autocomplete. + .map(({text, label}) => { + return { + name: text, + value: text, + label, + }; + }); + }); + } + + _handleSearch(e) { + const keyboardEvent = this.getKeyboardEvent(e); + if (this.shouldSuppressKeyboardShortcut(e) || + (this.modifierPressed(e) && !keyboardEvent.shiftKey)) { return; } + + e.preventDefault(); + this.$.searchInput.focus(); + this.$.searchInput.selectAll(); + } +} + +customElements.define(GrSearchBar.is, GrSearchBar);
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js index cb8e142..831b080 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-search-bar"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> form { display: flex; @@ -36,19 +29,7 @@ } </style> <form> - <gr-autocomplete - show-search-icon - id="searchInput" - text="{{_inputVal}}" - query="[[query]]" - on-commit="_handleInputCommit" - allow-non-suggested-values - multi - threshold="[[_threshold]]" - tab-complete - vertical-offset="30"></gr-autocomplete> + <gr-autocomplete show-search-icon="" id="searchInput" text="{{_inputVal}}" query="[[query]]" on-commit="_handleInputCommit" allow-non-suggested-values="" multi="" threshold="[[_threshold]]" tab-complete="" vertical-offset="30"></gr-autocomplete> </form> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-search-bar.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html index 5b5dc02..1bd0fca 100644 --- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html +++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -19,18 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-search-bar</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="gr-search-bar.html"> -<script src="../../../scripts/util.js"></script> +<script type="module" src="./gr-search-bar.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<script>void (0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-search-bar.js'; +import '../../../scripts/util.js'; +void (0); +</script> <test-fixture id="basic"> <template> @@ -38,199 +44,202 @@ </template> </test-fixture> -<script> - suite('gr-search-bar tests', async () => { - await readyToTest(); - const kb = window.Gerrit.KeyboardShortcutBinder; - kb.bindShortcut(kb.Shortcut.SEARCH, '/'); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-search-bar.js'; +import '../../../scripts/util.js'; +suite('gr-search-bar tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + kb.bindShortcut(kb.Shortcut.SEARCH, '/'); - let element; - let sandbox; + let element; + let sandbox; - setup(done => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - flush(done); + setup(done => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + flush(done); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('value is propagated to _inputVal', () => { + element.value = 'foo'; + assert.equal(element._inputVal, 'foo'); + }); + + const getActiveElement = () => (document.activeElement.shadowRoot ? + document.activeElement.shadowRoot.activeElement : + document.activeElement); + + test('enter in search input fires event', done => { + element.addEventListener('handle-search', () => { + assert.notEqual(getActiveElement(), element.$.searchInput); + assert.notEqual(getActiveElement(), element.$.searchButton); + done(); + }); + element.value = 'test'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + }); + + test('input blurred after commit', () => { + const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur'); + element.$.searchInput.text = 'fate/stay'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(blurSpy.called); + }); + + test('empty search query does not trigger nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = ''; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isFalse(searchSpy.called); + }); + + test('Predefined query op with no predication doesnt trigger nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'added:'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isFalse(searchSpy.called); + }); + + test('predefined predicate query triggers nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'age:1week'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(searchSpy.called); + }); + + test('undefined predicate query triggers nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'random:1week'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(searchSpy.called); + }); + + test('empty undefined predicate query triggers nav', () => { + const searchSpy = sandbox.spy(); + element.addEventListener('handle-search', searchSpy); + element.value = 'random:'; + MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, + null, 'enter'); + assert.isTrue(searchSpy.called); + }); + + test('keyboard shortcuts', () => { + const focusSpy = sandbox.spy(element.$.searchInput, 'focus'); + const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll'); + MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/'); + assert.isTrue(focusSpy.called); + assert.isTrue(selectAllSpy.called); + }); + + suite('_getSearchSuggestions', () => { + test('Autocompletes accounts', () => { + sandbox.stub(element, 'accountSuggestions', () => + Promise.resolve([{text: 'owner:fred@goog.co'}]) + ); + return element._getSearchSuggestions('owner:fr').then(s => { + assert.equal(s[0].value, 'owner:fred@goog.co'); + }); }); - teardown(() => { - sandbox.restore(); - }); - - test('value is propagated to _inputVal', () => { - element.value = 'foo'; - assert.equal(element._inputVal, 'foo'); - }); - - const getActiveElement = () => (document.activeElement.shadowRoot ? - document.activeElement.shadowRoot.activeElement : - document.activeElement); - - test('enter in search input fires event', done => { - element.addEventListener('handle-search', () => { - assert.notEqual(getActiveElement(), element.$.searchInput); - assert.notEqual(getActiveElement(), element.$.searchButton); + test('Autocompletes groups', done => { + sandbox.stub(element, 'groupSuggestions', () => + Promise.resolve([ + {text: 'ownerin:Polygerrit'}, + {text: 'ownerin:gerrit'}, + ]) + ); + element._getSearchSuggestions('ownerin:pol').then(s => { + assert.equal(s[0].value, 'ownerin:Polygerrit'); done(); }); - element.value = 'test'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); }); - test('input blurred after commit', () => { - const blurSpy = sandbox.spy(element.$.searchInput.$.input, 'blur'); - element.$.searchInput.text = 'fate/stay'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(blurSpy.called); + test('Autocompletes projects', done => { + sandbox.stub(element, 'projectSuggestions', () => + Promise.resolve([ + {text: 'project:Polygerrit'}, + {text: 'project:gerrit'}, + {text: 'project:gerrittest'}, + ]) + ); + element._getSearchSuggestions('project:pol').then(s => { + assert.equal(s[0].value, 'project:Polygerrit'); + done(); + }); }); - test('empty search query does not trigger nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = ''; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isFalse(searchSpy.called); + test('Autocompletes simple searches', done => { + element._getSearchSuggestions('is:o').then(s => { + assert.equal(s[0].name, 'is:open'); + assert.equal(s[0].value, 'is:open'); + assert.equal(s[1].name, 'is:owner'); + assert.equal(s[1].value, 'is:owner'); + done(); + }); }); - test('Predefined query op with no predication doesnt trigger nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'added:'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isFalse(searchSpy.called); + test('Does not autocomplete with no match', done => { + element._getSearchSuggestions('asdasdasdasd').then(s => { + assert.equal(s.length, 0); + done(); + }); }); - test('predefined predicate query triggers nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'age:1week'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(searchSpy.called); + test('Autocompltes without is:mergable when disabled', done => { + element._getSearchSuggestions('is:mergeab').then(s => { + assert.equal(s.length, 0); + done(); + }); }); + }); - test('undefined predicate query triggers nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'random:1week'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(searchSpy.called); - }); - - test('empty undefined predicate query triggers nav', () => { - const searchSpy = sandbox.spy(); - element.addEventListener('handle-search', searchSpy); - element.value = 'random:'; - MockInteractions.pressAndReleaseKeyOn(element.$.searchInput.$.input, 13, - null, 'enter'); - assert.isTrue(searchSpy.called); - }); - - test('keyboard shortcuts', () => { - const focusSpy = sandbox.spy(element.$.searchInput, 'focus'); - const selectAllSpy = sandbox.spy(element.$.searchInput, 'selectAll'); - MockInteractions.pressAndReleaseKeyOn(document.body, 191, null, '/'); - assert.isTrue(focusSpy.called); - assert.isTrue(selectAllSpy.called); - }); - - suite('_getSearchSuggestions', () => { - test('Autocompletes accounts', () => { - sandbox.stub(element, 'accountSuggestions', () => - Promise.resolve([{text: 'owner:fred@goog.co'}]) - ); - return element._getSearchSuggestions('owner:fr').then(s => { - assert.equal(s[0].value, 'owner:fred@goog.co'); + [ + 'API_REF_UPDATED_AND_CHANGE_REINDEX', + 'REF_UPDATED_AND_CHANGE_REINDEX', + ].forEach(mergeability => { + suite(`mergeability as ${mergeability}`, () => { + setup(done => { + stub('gr-rest-api-interface', { + getConfig() { + return Promise.resolve({ + index: { + mergeabilityComputationBehavior: mergeability, + }, + }); + }, }); + + element = fixture('basic'); + flush(done); }); - test('Autocompletes groups', done => { - sandbox.stub(element, 'groupSuggestions', () => - Promise.resolve([ - {text: 'ownerin:Polygerrit'}, - {text: 'ownerin:gerrit'}, - ]) - ); - element._getSearchSuggestions('ownerin:pol').then(s => { - assert.equal(s[0].value, 'ownerin:Polygerrit'); - done(); - }); - }); - - test('Autocompletes projects', done => { - sandbox.stub(element, 'projectSuggestions', () => - Promise.resolve([ - {text: 'project:Polygerrit'}, - {text: 'project:gerrit'}, - {text: 'project:gerrittest'}, - ]) - ); - element._getSearchSuggestions('project:pol').then(s => { - assert.equal(s[0].value, 'project:Polygerrit'); - done(); - }); - }); - - test('Autocompletes simple searches', done => { - element._getSearchSuggestions('is:o').then(s => { - assert.equal(s[0].name, 'is:open'); - assert.equal(s[0].value, 'is:open'); - assert.equal(s[1].name, 'is:owner'); - assert.equal(s[1].value, 'is:owner'); - done(); - }); - }); - - test('Does not autocomplete with no match', done => { - element._getSearchSuggestions('asdasdasdasd').then(s => { - assert.equal(s.length, 0); - done(); - }); - }); - - test('Autocompltes without is:mergable when disabled', done => { + test('Autocompltes with is:mergable when enabled', done => { element._getSearchSuggestions('is:mergeab').then(s => { - assert.equal(s.length, 0); + assert.equal(s.length, 2); + assert.equal(s[0].name, 'is:mergeable'); + assert.equal(s[0].value, 'is:mergeable'); + assert.equal(s[1].name, '-is:mergeable'); + assert.equal(s[1].value, '-is:mergeable'); done(); }); }); }); - - [ - 'API_REF_UPDATED_AND_CHANGE_REINDEX', - 'REF_UPDATED_AND_CHANGE_REINDEX', - ].forEach(mergeability => { - suite(`mergeability as ${mergeability}`, () => { - setup(done => { - stub('gr-rest-api-interface', { - getConfig() { - return Promise.resolve({ - index: { - mergeabilityComputationBehavior: mergeability, - }, - }); - }, - }); - - element = fixture('basic'); - flush(done); - }); - - test('Autocompltes with is:mergable when enabled', done => { - element._getSearchSuggestions('is:mergeab').then(s => { - assert.equal(s.length, 2); - assert.equal(s[0].name, 'is:mergeable'); - assert.equal(s[0].value, 'is:mergeable'); - assert.equal(s[1].name, '-is:mergeable'); - assert.equal(s[1].value, '-is:mergeable'); - done(); - }); - }); - }); - }); }); +}); </script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js index cfdd524..a93c139 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search.js
@@ -14,152 +14,162 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const MAX_AUTOCOMPLETE_RESULTS = 10; - const SELF_EXPRESSION = 'self'; - const ME_EXPRESSION = 'me'; +import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js'; +import '../gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-search-bar/gr-search-bar.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-smart-search_html.js'; - /** - * @appliesMixin Gerrit.DisplayNameMixin - * @extends Polymer.Element - */ - class GrSmartSearch extends Polymer.mixinBehaviors( [ - Gerrit.DisplayNameBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-smart-search'; } +const MAX_AUTOCOMPLETE_RESULTS = 10; +const SELF_EXPRESSION = 'self'; +const ME_EXPRESSION = 'me'; - static get properties() { - return { - searchQuery: String, - _config: Object, - _projectSuggestions: { - type: Function, - value() { - return this._fetchProjects.bind(this); - }, +/** + * @appliesMixin Gerrit.DisplayNameMixin + * @extends Polymer.Element + */ +class GrSmartSearch extends mixinBehaviors( [ + Gerrit.DisplayNameBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-smart-search'; } + + static get properties() { + return { + searchQuery: String, + _config: Object, + _projectSuggestions: { + type: Function, + value() { + return this._fetchProjects.bind(this); }, - _groupSuggestions: { - type: Function, - value() { - return this._fetchGroups.bind(this); - }, + }, + _groupSuggestions: { + type: Function, + value() { + return this._fetchGroups.bind(this); }, - _accountSuggestions: { - type: Function, - value() { - return this._fetchAccounts.bind(this); - }, + }, + _accountSuggestions: { + type: Function, + value() { + return this._fetchAccounts.bind(this); }, - }; - } + }, + }; + } - /** @override */ - attached() { - super.attached(); - this.$.restAPI.getConfig().then(cfg => { - this._config = cfg; - }); - } + /** @override */ + attached() { + super.attached(); + this.$.restAPI.getConfig().then(cfg => { + this._config = cfg; + }); + } - _handleSearch(e) { - const input = e.detail.inputVal; - if (input) { - Gerrit.Nav.navigateToSearchQuery(input); - } - } - - /** - * Fetch from the API the predicted projects. - * - * @param {string} predicate - The first part of the search term, e.g. - * 'project' - * @param {string} expression - The second part of the search term, e.g. - * 'gerr' - * @return {!Promise} This returns a promise that resolves to an array of - * strings. - */ - _fetchProjects(predicate, expression) { - return this.$.restAPI.getSuggestedProjects( - expression, - MAX_AUTOCOMPLETE_RESULTS) - .then(projects => { - if (!projects) { return []; } - const keys = Object.keys(projects); - return keys.map(key => { return {text: predicate + ':' + key}; }); - }); - } - - /** - * Fetch from the API the predicted groups. - * - * @param {string} predicate - The first part of the search term, e.g. - * 'ownerin' - * @param {string} expression - The second part of the search term, e.g. - * 'polyger' - * @return {!Promise} This returns a promise that resolves to an array of - * strings. - */ - _fetchGroups(predicate, expression) { - if (expression.length === 0) { return Promise.resolve([]); } - return this.$.restAPI.getSuggestedGroups( - expression, - MAX_AUTOCOMPLETE_RESULTS) - .then(groups => { - if (!groups) { return []; } - const keys = Object.keys(groups); - return keys.map(key => { return {text: predicate + ':' + key}; }); - }); - } - - /** - * Fetch from the API the predicted accounts. - * - * @param {string} predicate - The first part of the search term, e.g. - * 'owner' - * @param {string} expression - The second part of the search term, e.g. - * 'kasp' - * @return {!Promise} This returns a promise that resolves to an array of - * strings. - */ - _fetchAccounts(predicate, expression) { - if (expression.length === 0) { return Promise.resolve([]); } - return this.$.restAPI.getSuggestedAccounts( - expression, - MAX_AUTOCOMPLETE_RESULTS) - .then(accounts => { - if (!accounts) { return []; } - return this._mapAccountsHelper(accounts, predicate); - }) - .then(accounts => { - // When the expression supplied is a beginning substring of 'self', - // add it as an autocomplete option. - if (SELF_EXPRESSION.startsWith(expression)) { - return accounts.concat( - [{text: predicate + ':' + SELF_EXPRESSION}]); - } else if (ME_EXPRESSION.startsWith(expression)) { - return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]); - } else { - return accounts; - } - }); - } - - _mapAccountsHelper(accounts, predicate) { - return accounts.map(account => { - const userName = this.getUserName(this._serverConfig, account, false); - return { - label: account.name || '', - text: account.email ? - `${predicate}:${account.email}` : - `${predicate}:"${userName}"`, - }; - }); + _handleSearch(e) { + const input = e.detail.inputVal; + if (input) { + Gerrit.Nav.navigateToSearchQuery(input); } } - customElements.define(GrSmartSearch.is, GrSmartSearch); -})(); + /** + * Fetch from the API the predicted projects. + * + * @param {string} predicate - The first part of the search term, e.g. + * 'project' + * @param {string} expression - The second part of the search term, e.g. + * 'gerr' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchProjects(predicate, expression) { + return this.$.restAPI.getSuggestedProjects( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(projects => { + if (!projects) { return []; } + const keys = Object.keys(projects); + return keys.map(key => { return {text: predicate + ':' + key}; }); + }); + } + + /** + * Fetch from the API the predicted groups. + * + * @param {string} predicate - The first part of the search term, e.g. + * 'ownerin' + * @param {string} expression - The second part of the search term, e.g. + * 'polyger' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchGroups(predicate, expression) { + if (expression.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedGroups( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(groups => { + if (!groups) { return []; } + const keys = Object.keys(groups); + return keys.map(key => { return {text: predicate + ':' + key}; }); + }); + } + + /** + * Fetch from the API the predicted accounts. + * + * @param {string} predicate - The first part of the search term, e.g. + * 'owner' + * @param {string} expression - The second part of the search term, e.g. + * 'kasp' + * @return {!Promise} This returns a promise that resolves to an array of + * strings. + */ + _fetchAccounts(predicate, expression) { + if (expression.length === 0) { return Promise.resolve([]); } + return this.$.restAPI.getSuggestedAccounts( + expression, + MAX_AUTOCOMPLETE_RESULTS) + .then(accounts => { + if (!accounts) { return []; } + return this._mapAccountsHelper(accounts, predicate); + }) + .then(accounts => { + // When the expression supplied is a beginning substring of 'self', + // add it as an autocomplete option. + if (SELF_EXPRESSION.startsWith(expression)) { + return accounts.concat( + [{text: predicate + ':' + SELF_EXPRESSION}]); + } else if (ME_EXPRESSION.startsWith(expression)) { + return accounts.concat([{text: predicate + ':' + ME_EXPRESSION}]); + } else { + return accounts; + } + }); + } + + _mapAccountsHelper(accounts, predicate) { + return accounts.map(account => { + const userName = this.getUserName(this._serverConfig, account, false); + return { + label: account.name || '', + text: account.email ? + `${predicate}:${account.email}` : + `${predicate}:"${userName}"`, + }; + }); + } +} + +customElements.define(GrSmartSearch.is, GrSmartSearch);
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js index c4ae41b..78906a8 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_html.js
@@ -1,38 +1,25 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-search-bar/gr-search-bar.html"> - -<dom-module id="gr-smart-search"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> </style> - <gr-search-bar id="search" - value="{{searchQuery}}" - on-handle-search="_handleSearch" - project-suggestions="[[_projectSuggestions]]" - group-suggestions="[[_groupSuggestions]]" - account-suggestions="[[_accountSuggestions]]"></gr-search-bar> + <gr-search-bar id="search" value="{{searchQuery}}" on-handle-search="_handleSearch" project-suggestions="[[_projectSuggestions]]" group-suggestions="[[_groupSuggestions]]" account-suggestions="[[_accountSuggestions]]"></gr-search-bar> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-smart-search.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html index 6fd00c7..a0ba203 100644 --- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html +++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-smart-search</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-smart-search.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-smart-search.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-smart-search.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,125 +40,127 @@ </template> </test-fixture> -<script> - suite('gr-smart-search tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-smart-search.js'; +suite('gr-smart-search tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('Autocompletes accounts', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([ - { - name: 'fred', - email: 'fred@goog.co', - }, - ]) - ); - return element._fetchAccounts('owner', 'fr').then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); - }); - }); - - test('Inserts self as option when valid', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([ - { - name: 'fred', - email: 'fred@goog.co', - }, - ]) - ); - element._fetchAccounts('owner', 's') - .then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); - assert.deepEqual(s[1], {text: 'owner:self'}); - }) - .then(() => element._fetchAccounts('owner', 'selfs')) - .then(s => { - assert.notEqual(s[0], {text: 'owner:self'}); - }); - }); - - test('Inserts me as option when valid', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([ - { - name: 'fred', - email: 'fred@goog.co', - }, - ]) - ); - return element._fetchAccounts('owner', 'm') - .then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); - assert.deepEqual(s[1], {text: 'owner:me'}); - }) - .then(() => element._fetchAccounts('owner', 'meme')) - .then(s => { - assert.notEqual(s[0], {text: 'owner:me'}); - }); - }); - - test('Autocompletes groups', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => - Promise.resolve({ - Polygerrit: 0, - gerrit: 0, - gerrittest: 0, - }) - ); - return element._fetchGroups('ownerin', 'pol').then(s => { - assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); - }); - }); - - test('Autocompletes projects', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () => - Promise.resolve({Polygerrit: 0})); - return element._fetchProjects('project', 'pol').then(s => { - assert.deepEqual(s[0], {text: 'project:Polygerrit'}); - }); - }); - - test('Autocomplete doesnt override exact matches to input', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => - Promise.resolve({ - Polygerrit: 0, - gerrit: 0, - gerrittest: 0, - }) - ); - return element._fetchGroups('ownerin', 'gerrit').then(s => { - assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); - assert.deepEqual(s[1], {text: 'ownerin:gerrit'}); - assert.deepEqual(s[2], {text: 'ownerin:gerrittest'}); - }); - }); - - test('Autocompletes accounts with no email', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([{name: 'fred'}])); - return element._fetchAccounts('owner', 'fr').then(s => { - assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'}); - }); - }); - - test('Autocompletes accounts with email', () => { - sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => - Promise.resolve([{email: 'fred@goog.co'}])); - return element._fetchAccounts('owner', 'fr').then(s => { - assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''}); - }); + test('Autocompletes accounts', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]) + ); + return element._fetchAccounts('owner', 'fr').then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); }); }); + + test('Inserts self as option when valid', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]) + ); + element._fetchAccounts('owner', 's') + .then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); + assert.deepEqual(s[1], {text: 'owner:self'}); + }) + .then(() => element._fetchAccounts('owner', 'selfs')) + .then(s => { + assert.notEqual(s[0], {text: 'owner:self'}); + }); + }); + + test('Inserts me as option when valid', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([ + { + name: 'fred', + email: 'fred@goog.co', + }, + ]) + ); + return element._fetchAccounts('owner', 'm') + .then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: 'fred'}); + assert.deepEqual(s[1], {text: 'owner:me'}); + }) + .then(() => element._fetchAccounts('owner', 'meme')) + .then(s => { + assert.notEqual(s[0], {text: 'owner:me'}); + }); + }); + + test('Autocompletes groups', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => + Promise.resolve({ + Polygerrit: 0, + gerrit: 0, + gerrittest: 0, + }) + ); + return element._fetchGroups('ownerin', 'pol').then(s => { + assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); + }); + }); + + test('Autocompletes projects', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedProjects', () => + Promise.resolve({Polygerrit: 0})); + return element._fetchProjects('project', 'pol').then(s => { + assert.deepEqual(s[0], {text: 'project:Polygerrit'}); + }); + }); + + test('Autocomplete doesnt override exact matches to input', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedGroups', () => + Promise.resolve({ + Polygerrit: 0, + gerrit: 0, + gerrittest: 0, + }) + ); + return element._fetchGroups('ownerin', 'gerrit').then(s => { + assert.deepEqual(s[0], {text: 'ownerin:Polygerrit'}); + assert.deepEqual(s[1], {text: 'ownerin:gerrit'}); + assert.deepEqual(s[2], {text: 'ownerin:gerrittest'}); + }); + }); + + test('Autocompletes accounts with no email', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([{name: 'fred'}])); + return element._fetchAccounts('owner', 'fr').then(s => { + assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'}); + }); + }); + + test('Autocompletes accounts with email', () => { + sandbox.stub(element.$.restAPI, 'getSuggestedAccounts', () => + Promise.resolve([{email: 'fred@goog.co'}])); + return element._fetchAccounts('owner', 'fr').then(s => { + assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''}); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.html b/polygerrit-ui/app/elements/custom-dark-theme_test.html index cd07a67..308e2ee 100644 --- a/polygerrit-ui/app/elements/custom-dark-theme_test.html +++ b/polygerrit-ui/app/elements/custom-dark-theme_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app-it_test</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../test/test-pre-setup.js"></script> -<link rel="import" href="../test/common-test-setup.html"/> -<link rel="import" href="./gr-app.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../test/test-pre-setup.js"></script> +<script type="module" src="../test/common-test-setup.js"></script> +<script type="module" src="./gr-app.js"></script> -<script>void(0);</script> +<script type="module"> +import '../test/test-pre-setup.js'; +import '../test/common-test-setup.js'; +import './gr-app.js'; +void(0); +</script> <test-fixture id="element"> <template> @@ -35,69 +40,71 @@ </template> </test-fixture> -<script> - suite('gr-app custom dark theme tests', async () => { - await readyToTest(); - let sandbox; - let element; +<script type="module"> +import '../test/test-pre-setup.js'; +import '../test/common-test-setup.js'; +import './gr-app.js'; +suite('gr-app custom dark theme tests', () => { + let sandbox; + let element; - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-reporting', { - appStarted: sandbox.stub(), - }); - stub('gr-account-dropdown', { - _getTopContent: sinon.stub(), - }); - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(null); }, - getAccountCapabilities() { return Promise.resolve({}); }, - getConfig() { - return Promise.resolve({ - plugin: { - js_resource_paths: [], - html_resource_paths: [ - new URL('test/plugin.html', window.location.href).toString(), - ], - }, + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-reporting', { + appStarted: sandbox.stub(), + }); + stub('gr-account-dropdown', { + _getTopContent: sinon.stub(), + }); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + getAccountCapabilities() { return Promise.resolve({}); }, + getConfig() { + return Promise.resolve({ + plugin: { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html', window.location.href).toString(), + ], + }, + }); + }, + getVersion() { return Promise.resolve(42); }, + getLoggedIn() { return Promise.resolve(false); }, + }); + + window.localStorage.setItem('dark-theme', 'true'); + + element = fixture('element'); + + const importSpy = sandbox.spy( + element.$['app-element'].$.externalStyleForAll, + '_import'); + const importForThemeSpy = sandbox.spy( + element.$['app-element'].$.externalStyleForTheme, + '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues)) + .then(() => { + flush(done); }); - }, - getVersion() { return Promise.resolve(42); }, - getLoggedIn() { return Promise.resolve(false); }, - }); - - window.localStorage.setItem('dark-theme', 'true'); - - element = fixture('element'); - - const importSpy = sandbox.spy( - element.$['app-element'].$.externalStyleForAll, - '_import'); - const importForThemeSpy = sandbox.spy( - element.$['app-element'].$.externalStyleForTheme, - '_import'); - Gerrit.awaitPluginsLoaded().then(() => { - Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues)) - .then(() => { - flush(done); - }); - }); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('applies the right theme', () => { - assert.equal( - util.getComputedStyleValue('--primary-text-color', element), - 'red'); - assert.equal( - util.getComputedStyleValue('--header-background-color', element), - 'black'); - assert.equal( - util.getComputedStyleValue('--footer-background-color', element), - 'yellow'); }); }); + + teardown(() => { + sandbox.restore(); + }); + + test('applies the right theme', () => { + assert.equal( + util.getComputedStyleValue('--primary-text-color', element), + 'red'); + assert.equal( + util.getComputedStyleValue('--header-background-color', element), + 'black'); + assert.equal( + util.getComputedStyleValue('--footer-background-color', element), + 'yellow'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.html b/polygerrit-ui/app/elements/custom-light-theme_test.html index 13b872e..66567be 100644 --- a/polygerrit-ui/app/elements/custom-light-theme_test.html +++ b/polygerrit-ui/app/elements/custom-light-theme_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app-it_test</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../test/test-pre-setup.js"></script> -<link rel="import" href="../test/common-test-setup.html"/> -<link rel="import" href="gr-app.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../test/test-pre-setup.js"></script> +<script type="module" src="../test/common-test-setup.js"></script> +<script type="module" src="./gr-app.js"></script> -<script>void(0);</script> +<script type="module"> +import '../test/test-pre-setup.js'; +import '../test/common-test-setup.js'; +import './gr-app.js'; +void(0); +</script> <test-fixture id="element"> <template> @@ -35,69 +40,71 @@ </template> </test-fixture> -<script> - suite('gr-app custom light theme tests', async () => { - await readyToTest(); - let sandbox; - let element; +<script type="module"> +import '../test/test-pre-setup.js'; +import '../test/common-test-setup.js'; +import './gr-app.js'; +suite('gr-app custom light theme tests', () => { + let sandbox; + let element; - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-reporting', { - appStarted: sandbox.stub(), - }); - stub('gr-account-dropdown', { - _getTopContent: sinon.stub(), - }); - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(null); }, - getAccountCapabilities() { return Promise.resolve({}); }, - getConfig() { - return Promise.resolve({ - plugin: { - js_resource_paths: [], - html_resource_paths: [ - new URL('test/plugin.html', window.location.href).toString(), - ], - }, + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-reporting', { + appStarted: sandbox.stub(), + }); + stub('gr-account-dropdown', { + _getTopContent: sinon.stub(), + }); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + getAccountCapabilities() { return Promise.resolve({}); }, + getConfig() { + return Promise.resolve({ + plugin: { + js_resource_paths: [], + html_resource_paths: [ + new URL('test/plugin.html', window.location.href).toString(), + ], + }, + }); + }, + getVersion() { return Promise.resolve(42); }, + getLoggedIn() { return Promise.resolve(false); }, + }); + + window.localStorage.removeItem('dark-theme'); + + element = fixture('element'); + + const importSpy = sandbox.spy( + element.$['app-element'].$.externalStyleForAll, + '_import'); + const importForThemeSpy = sandbox.spy( + element.$['app-element'].$.externalStyleForTheme, + '_import'); + Gerrit.awaitPluginsLoaded().then(() => { + Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues)) + .then(() => { + flush(done); }); - }, - getVersion() { return Promise.resolve(42); }, - getLoggedIn() { return Promise.resolve(false); }, - }); - - window.localStorage.removeItem('dark-theme'); - - element = fixture('element'); - - const importSpy = sandbox.spy( - element.$['app-element'].$.externalStyleForAll, - '_import'); - const importForThemeSpy = sandbox.spy( - element.$['app-element'].$.externalStyleForTheme, - '_import'); - Gerrit.awaitPluginsLoaded().then(() => { - Promise.all(importSpy.returnValues.concat(importForThemeSpy.returnValues)) - .then(() => { - flush(done); - }); - }); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('applies the right theme', () => { - assert.equal( - util.getComputedStyleValue('--primary-text-color', element), - '#F00BAA'); - assert.equal( - util.getComputedStyleValue('--header-background-color', element), - '#F01BAA'); - assert.equal( - util.getComputedStyleValue('--footer-background-color', element), - '#F02BAA'); }); }); + + teardown(() => { + sandbox.restore(); + }); + + test('applies the right theme', () => { + assert.equal( + util.getComputedStyleValue('--primary-text-color', element), + '#F00BAA'); + assert.equal( + util.getComputedStyleValue('--header-background-color', element), + '#F01BAA'); + assert.equal( + util.getComputedStyleValue('--footer-background-color', element), + '#F02BAA'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js index 9104b90..d5075d7 100644 --- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js +++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js
@@ -14,200 +14,213 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '@polymer/iron-icon/iron-icon.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-diff/gr-diff.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-apply-fix-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrApplyFixDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-apply-fix-dialog'; } + + static get properties() { + return { + // Diff rendering preference API response. + prefs: Array, + // ChangeInfo API response object. + change: Object, + changeNum: String, + _patchNum: Number, + // robot ID associated with a robot comment. + _robotId: String, + // Selected FixSuggestionInfo entity from robot comment API response. + _currentFix: Object, + // Flattened /preview API response DiffInfo map object. + _currentPreviews: {type: Array, value: () => []}, + // FixSuggestionInfo entities from robot comment API response. + _fixSuggestions: Array, + _isApplyFixLoading: { + type: Boolean, + value: false, + }, + // Index of currently showing suggested fix. + _selectedFixIdx: Number, + }; + } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Given robot comment CustomEvent objevt, fetch diffs associated + * with first robot comment suggested fix and open dialog. + * + * @param {*} e CustomEvent to be passed from gr-comment with + * robot comment detail. + * @return {Promise<undefined>} Promise that resolves either when all + * preview diffs are fetched or no fix suggestions in custom event detail. */ - class GrApplyFixDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-apply-fix-dialog'; } - - static get properties() { - return { - // Diff rendering preference API response. - prefs: Array, - // ChangeInfo API response object. - change: Object, - changeNum: String, - _patchNum: Number, - // robot ID associated with a robot comment. - _robotId: String, - // Selected FixSuggestionInfo entity from robot comment API response. - _currentFix: Object, - // Flattened /preview API response DiffInfo map object. - _currentPreviews: {type: Array, value: () => []}, - // FixSuggestionInfo entities from robot comment API response. - _fixSuggestions: Array, - _isApplyFixLoading: { - type: Boolean, - value: false, - }, - // Index of currently showing suggested fix. - _selectedFixIdx: Number, - }; + open(e) { + this._patchNum = e.detail.patchNum; + this._fixSuggestions = e.detail.comment.fix_suggestions; + this._robotId = e.detail.comment.robot_id; + if (this._fixSuggestions == null || this._fixSuggestions.length == 0) { + return Promise.resolve(); } + this._selectedFixIdx = 0; + const promises = []; + promises.push( + this._showSelectedFixSuggestion(this._fixSuggestions[0]), + this.$.applyFixOverlay.open() + ); + return Promise.all(promises) + .then(() => { + // ensures gr-overlay repositions overlay in center + this.$.applyFixOverlay.fire('iron-resize'); + }); + } - /** - * Given robot comment CustomEvent objevt, fetch diffs associated - * with first robot comment suggested fix and open dialog. - * - * @param {*} e CustomEvent to be passed from gr-comment with - * robot comment detail. - * @return {Promise<undefined>} Promise that resolves either when all - * preview diffs are fetched or no fix suggestions in custom event detail. - */ - open(e) { - this._patchNum = e.detail.patchNum; - this._fixSuggestions = e.detail.comment.fix_suggestions; - this._robotId = e.detail.comment.robot_id; - if (this._fixSuggestions == null || this._fixSuggestions.length == 0) { - return Promise.resolve(); - } - this._selectedFixIdx = 0; - const promises = []; - promises.push( - this._showSelectedFixSuggestion(this._fixSuggestions[0]), - this.$.applyFixOverlay.open() - ); - return Promise.all(promises) - .then(() => { - // ensures gr-overlay repositions overlay in center - this.$.applyFixOverlay.fire('iron-resize'); - }); + attached() { + super.attached(); + this.refitOverlay = () => { + // re-center the dialog as content changed + this.$.applyFixOverlay.fire('iron-resize'); + }; + this.addEventListener('diff-context-expanded', this.refitOverlay); + } + + detached() { + super.detached(); + this.removeEventListener('diff-context-expanded', this.refitOverlay); + } + + _showSelectedFixSuggestion(fixSuggestion) { + this._currentFix = fixSuggestion; + return this._fetchFixPreview(fixSuggestion.fix_id); + } + + _fetchFixPreview(fixId) { + return this.$.restAPI + .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId) + .then(res => { + if (res != null) { + const previews = Object.keys(res).map(key => { + return {filepath: key, preview: res[key]}; + }); + this._currentPreviews = previews; + } + }) + .catch(err => { + this._close(); + throw err; + }); + } + + hasSingleFix(_fixSuggestions) { + return (_fixSuggestions || {}).length === 1; + } + + overridePartialPrefs(prefs) { + // generate a smaller gr-diff than fullscreen for dialog + return Object.assign({}, prefs, {line_length: 50}); + } + + onCancel(e) { + if (e) { + e.stopPropagation(); } + this._close(); + } - attached() { - super.attached(); - this.refitOverlay = () => { - // re-center the dialog as content changed - this.$.applyFixOverlay.fire('iron-resize'); - }; - this.addEventListener('diff-context-expanded', this.refitOverlay); - } + addOneTo(_selectedFixIdx) { + return _selectedFixIdx + 1; + } - detached() { - super.detached(); - this.removeEventListener('diff-context-expanded', this.refitOverlay); - } - - _showSelectedFixSuggestion(fixSuggestion) { - this._currentFix = fixSuggestion; - return this._fetchFixPreview(fixSuggestion.fix_id); - } - - _fetchFixPreview(fixId) { - return this.$.restAPI - .getRobotCommentFixPreview(this.changeNum, this._patchNum, fixId) - .then(res => { - if (res != null) { - const previews = Object.keys(res).map(key => { - return {filepath: key, preview: res[key]}; - }); - this._currentPreviews = previews; - } - }) - .catch(err => { - this._close(); - throw err; - }); - } - - hasSingleFix(_fixSuggestions) { - return (_fixSuggestions || {}).length === 1; - } - - overridePartialPrefs(prefs) { - // generate a smaller gr-diff than fullscreen for dialog - return Object.assign({}, prefs, {line_length: 50}); - } - - onCancel(e) { - if (e) { - e.stopPropagation(); - } - this._close(); - } - - addOneTo(_selectedFixIdx) { - return _selectedFixIdx + 1; - } - - _onPrevFixClick(e) { - if (e) e.stopPropagation(); - if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) { - this._selectedFixIdx -= 1; - return this._showSelectedFixSuggestion( - this._fixSuggestions[this._selectedFixIdx]); - } - } - - _onNextFixClick(e) { - if (e) e.stopPropagation(); - if (this._fixSuggestions && - this._selectedFixIdx < this._fixSuggestions.length) { - this._selectedFixIdx += 1; - return this._showSelectedFixSuggestion( - this._fixSuggestions[this._selectedFixIdx]); - } - } - - _noPrevFix(_selectedFixIdx) { - return _selectedFixIdx === 0; - } - - _noNextFix(_selectedFixIdx, fixSuggestions) { - if (fixSuggestions == null) return true; - return _selectedFixIdx === fixSuggestions.length - 1; - } - - _close() { - this._currentFix = {}; - this._currentPreviews = []; - this._isApplyFixLoading = false; - - this.dispatchEvent(new CustomEvent('close-fix-preview', { - bubbles: true, - composed: true, - })); - this.$.applyFixOverlay.close(); - } - - _getApplyFixButtonLabel(isLoading) { - return isLoading ? 'Saving...' : 'Apply Fix'; - } - - _handleApplyFix(e) { - if (e) { - e.stopPropagation(); - } - if (this._currentFix == null || this._currentFix.fix_id == null) { - return; - } - this._isApplyFixLoading = true; - return this.$.restAPI - .applyFixSuggestion( - this.changeNum, this._patchNum, this._currentFix.fix_id - ) - .then(res => { - if (res && res.ok) { - Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum); - this._close(); - } - this._isApplyFixLoading = false; - }); - } - - getFixDescription(currentFix) { - return currentFix != null && currentFix.description ? - currentFix.description : ''; + _onPrevFixClick(e) { + if (e) e.stopPropagation(); + if (this._selectedFixIdx >= 1 && this._fixSuggestions != null) { + this._selectedFixIdx -= 1; + return this._showSelectedFixSuggestion( + this._fixSuggestions[this._selectedFixIdx]); } } - customElements.define(GrApplyFixDialog.is, GrApplyFixDialog); -})(); + _onNextFixClick(e) { + if (e) e.stopPropagation(); + if (this._fixSuggestions && + this._selectedFixIdx < this._fixSuggestions.length) { + this._selectedFixIdx += 1; + return this._showSelectedFixSuggestion( + this._fixSuggestions[this._selectedFixIdx]); + } + } + + _noPrevFix(_selectedFixIdx) { + return _selectedFixIdx === 0; + } + + _noNextFix(_selectedFixIdx, fixSuggestions) { + if (fixSuggestions == null) return true; + return _selectedFixIdx === fixSuggestions.length - 1; + } + + _close() { + this._currentFix = {}; + this._currentPreviews = []; + this._isApplyFixLoading = false; + + this.dispatchEvent(new CustomEvent('close-fix-preview', { + bubbles: true, + composed: true, + })); + this.$.applyFixOverlay.close(); + } + + _getApplyFixButtonLabel(isLoading) { + return isLoading ? 'Saving...' : 'Apply Fix'; + } + + _handleApplyFix(e) { + if (e) { + e.stopPropagation(); + } + if (this._currentFix == null || this._currentFix.fix_id == null) { + return; + } + this._isApplyFixLoading = true; + return this.$.restAPI + .applyFixSuggestion( + this.changeNum, this._patchNum, this._currentFix.fix_id + ) + .then(res => { + if (res && res.ok) { + Gerrit.Nav.navigateToChange(this.change, 'edit', this._patchNum); + this._close(); + } + this._isApplyFixLoading = false; + }); + } + + getFixDescription(currentFix) { + return currentFix != null && currentFix.description ? + currentFix.description : ''; + } +} + +customElements.define(GrApplyFixDialog.is, GrApplyFixDialog);
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js index c650bb5..f6cd1ec 100644 --- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js +++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2019 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-diff/gr-diff.html"> - -<dom-module id="gr-apply-fix-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> gr-diff { --content-width: 90vw; @@ -52,13 +44,8 @@ margin-right: var(--spacing-l); } </style> - <gr-overlay id="applyFixOverlay" with-backdrop> - <gr-dialog - id="applyFixDialog" - on-confirm="_handleApplyFix" - confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]" - disabled="[[_isApplyFixLoading]]" - on-cancel="onCancel"> + <gr-overlay id="applyFixOverlay" with-backdrop=""> + <gr-dialog id="applyFixDialog" on-confirm="_handleApplyFix" confirm-label="[[_getApplyFixButtonLabel(_isApplyFixLoading)]]" disabled="[[_isApplyFixLoading]]" on-cancel="onCancel"> <div slot="header">[[_robotId]] - [[getFixDescription(_currentFix)]]</div> <div slot="main"> <template is="dom-repeat" items="[[_currentPreviews]]"> @@ -66,26 +53,20 @@ <span>[[item.filepath]]</span> </div> <div class="diffContainer"> - <gr-diff - prefs="[[overridePartialPrefs(prefs)]]" - change-num="[[changeNum]]" - path="[[item.filepath]]" - diff="[[item.preview]]"></gr-diff> + <gr-diff prefs="[[overridePartialPrefs(prefs)]]" change-num="[[changeNum]]" path="[[item.filepath]]" diff="[[item.preview]]"></gr-diff> </div> </template> </div> - <div slot="footer" class="fix-picker" hidden$="[[hasSingleFix(_fixSuggestions)]]"> + <div slot="footer" class="fix-picker" hidden\$="[[hasSingleFix(_fixSuggestions)]]"> <span>Suggested fix [[addOneTo(_selectedFixIdx)]] of [[_fixSuggestions.length]]</span> - <gr-button id="prevFix" on-click="_onPrevFixClick" disabled$="[[_noPrevFix(_selectedFixIdx)]]"> + <gr-button id="prevFix" on-click="_onPrevFixClick" disabled\$="[[_noPrevFix(_selectedFixIdx)]]"> <iron-icon icon="gr-icons:chevron-left"></iron-icon> </gr-button> - <gr-button id="nextFix" on-click="_onNextFixClick" disabled$="[[_noNextFix(_selectedFixIdx, _fixSuggestions)]]"> + <gr-button id="nextFix" on-click="_onNextFixClick" disabled\$="[[_noNextFix(_selectedFixIdx, _fixSuggestions)]]"> <iron-icon icon="gr-icons:chevron-right"></iron-icon> </gr-button> </div> </gr-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-apply-fix-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html index f22ab57..386f829 100644 --- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html +++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.html
@@ -17,17 +17,23 @@ --> <meta name='viewport' content='width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes'> <title>gr-apply-fix-dialog</title> -<link rel='import' href='../../../test/common-test-setup.html'> -<script src='/bower_components/webcomponentsjs/custom-elements-es5-adapter.js'></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src='/bower_components/webcomponentsjs/webcomponents-lite.js'></script> -<script src='/bower_components/web-component-tester/browser.js'></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel='import' href='../../../test/common-test-setup.html' /> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel='import' href='./gr-apply-fix-dialog.html'> +<script type="module" src="./gr-apply-fix-dialog.js"></script> -<script>void (0);</script> +<script type="module"> +import '../../../test/common-test-setup.js'; +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-apply-fix-dialog.js'; +void (0); +</script> <test-fixture id='basic'> <template> @@ -35,229 +41,232 @@ </template> </test-fixture> -<script> - suite('gr-apply-fix-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; - const ROBOT_COMMENT_WITH_TWO_FIXES = { - robot_id: 'robot_1', - fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}], +<script type="module"> +import '../../../test/common-test-setup.js'; +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-apply-fix-dialog.js'; +suite('gr-apply-fix-dialog tests', () => { + let element; + let sandbox; + const ROBOT_COMMENT_WITH_TWO_FIXES = { + robot_id: 'robot_1', + fix_suggestions: [{fix_id: 'fix_1'}, {fix_id: 'fix_2'}], + }; + + const ROBOT_COMMENT_WITH_ONE_FIX = { + robot_id: 'robot_1', + fix_suggestions: [{fix_id: 'fix_1'}], + }; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.changeNum = '1'; + element._patchNum = 2; + element.change = { + _number: '1', + project: 'project', }; - - const ROBOT_COMMENT_WITH_ONE_FIX = { - robot_id: 'robot_1', - fix_suggestions: [{fix_id: 'fix_1'}], + element.prefs = { + font_size: 12, + line_length: 100, + tab_size: 4, }; + }); - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.changeNum = '1'; - element._patchNum = 2; - element.change = { - _number: '1', - project: 'project', - }; - element.prefs = { - font_size: 12, - line_length: 100, - tab_size: 4, - }; - }); + teardown(() => { + sandbox.restore(); + }); - teardown(() => { - sandbox.restore(); - }); + test('dialog opens fetch and sets previews', done => { + sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') + .returns(Promise.resolve({ + f1: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['loqlwkqll'], + }, + { + b: ['qwqqsqw'], + }, + { + ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'], + }, + ], + }, + f2: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['eqweqweqwex'], + }, + { + b: ['zassdasd'], + }, + { + ab: ['zassdasd', 'dasdasda', 'asdasdad'], + }, + ], + }, + })); + sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); - test('dialog opens fetch and sets previews', done => { - sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') - .returns(Promise.resolve({ - f1: { - meta_a: {}, - meta_b: {}, - content: [ - { - ab: ['loqlwkqll'], - }, - { - b: ['qwqqsqw'], - }, - { - ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'], - }, - ], - }, - f2: { - meta_a: {}, - meta_b: {}, - content: [ - { - ab: ['eqweqweqwex'], - }, - { - b: ['zassdasd'], - }, - { - ab: ['zassdasd', 'dasdasda', 'asdasdad'], - }, - ], - }, - })); - sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); - - element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}}) - .then(() => { - assert.equal(element._currentFix.fix_id, 'fix_1'); - assert.equal(element._currentPreviews.length, 2); - assert.equal(element._robotId, 'robot_1'); - done(); - }); - }); - - test('next button state updated when suggestions changed', done => { - sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') - .returns(Promise.resolve({})); - sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); - - element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}}) - .then(() => assert.isTrue(element.$.nextFix.disabled)) - .then(() => - element.open({detail: {patchNum: 2, - comment: ROBOT_COMMENT_WITH_TWO_FIXES}})) - .then(() => { - assert.isFalse(element.$.nextFix.disabled); - done(); - }); - }); - - test('preview endpoint throws error should reset dialog', done => { - sandbox.stub(window, 'fetch', (url => { - if (url.endsWith('/preview')) { - return Promise.reject(new Error('backend error')); - } - return Promise.resolve({ - ok: true, - text() { return Promise.resolve(''); }, - status: 200, + element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}}) + .then(() => { + assert.equal(element._currentFix.fix_id, 'fix_1'); + assert.equal(element._currentPreviews.length, 2); + assert.equal(element._robotId, 'robot_1'); + done(); }); - })); - const errorStub = sinon.stub(); - document.addEventListener('network-error', errorStub); - element.open({detail: {patchNum: 2, - comment: ROBOT_COMMENT_WITH_TWO_FIXES}}); - flush(() => { - assert.isTrue(errorStub.called); - assert.deepEqual(element._currentFix, {}); - done(); - }); - }); + }); - test('apply fix button should call apply ' + - 'and navigate to change view', done => { - sandbox.stub(element.$.restAPI, 'applyFixSuggestion') - .returns(Promise.resolve({ok: true})); - sandbox.stub(Gerrit.Nav, 'navigateToChange'); - element._currentFix = {fix_id: '123'}; + test('next button state updated when suggestions changed', done => { + sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') + .returns(Promise.resolve({})); + sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); - element._handleApplyFix().then(() => { - assert.isTrue(element.$.restAPI.applyFixSuggestion - .calledWithExactly('1', 2, '123')); - assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({ - _number: '1', - project: 'project', - }, 'edit', 2)); - - // reset gr-apply-fix-dialog and close - assert.deepEqual(element._currentFix, {}); - assert.equal(element._currentPreviews.length, 0); - done(); - }); - }); - - test('should not navigate to change view if incorect reponse', done => { - sandbox.stub(element.$.restAPI, 'applyFixSuggestion') - .returns(Promise.resolve({})); - sandbox.stub(Gerrit.Nav, 'navigateToChange'); - element._currentFix = {fix_id: '123'}; - - element._handleApplyFix().then(() => { - assert.isTrue(element.$.restAPI.applyFixSuggestion - .calledWithExactly('1', 2, '123')); - assert.isTrue(Gerrit.Nav.navigateToChange.notCalled); - - assert.equal(element._isApplyFixLoading, false); - done(); - }); - }); - - test('select fix forward and back of multiple suggested fixes', done => { - sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') - .returns(Promise.resolve({ - f1: { - meta_a: {}, - meta_b: {}, - content: [ - { - ab: ['loqlwkqll'], - }, - { - b: ['qwqqsqw'], - }, - { - ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'], - }, - ], - }, - f2: { - meta_a: {}, - meta_b: {}, - content: [ - { - ab: ['eqweqweqwex'], - }, - { - b: ['zassdasd'], - }, - { - ab: ['zassdasd', 'dasdasda', 'asdasdad'], - }, - ], - }, - })); - sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); - - element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}}) - .then(() => { - element._onNextFixClick(); - assert.equal(element._currentFix.fix_id, 'fix_2'); - element._onPrevFixClick(); - assert.equal(element._currentFix.fix_id, 'fix_1'); - done(); - }); - }); - - test('server-error should throw for failed apply call', done => { - sandbox.stub(window, 'fetch', (url => { - if (url.endsWith('/apply')) { - return Promise.reject(new Error('backend error')); - } - return Promise.resolve({ - ok: true, - text() { return Promise.resolve(''); }, - status: 200, + element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}}) + .then(() => assert.isTrue(element.$.nextFix.disabled)) + .then(() => + element.open({detail: {patchNum: 2, + comment: ROBOT_COMMENT_WITH_TWO_FIXES}})) + .then(() => { + assert.isFalse(element.$.nextFix.disabled); + done(); }); - })); - const errorStub = sinon.stub(); - document.addEventListener('network-error', errorStub); - sandbox.stub(Gerrit.Nav, 'navigateToChange'); - element._currentFix = {fix_id: '123'}; - element._handleApplyFix(); - flush(() => { - assert.isFalse(Gerrit.Nav.navigateToChange.called); - assert.isTrue(errorStub.called); - done(); + }); + + test('preview endpoint throws error should reset dialog', done => { + sandbox.stub(window, 'fetch', (url => { + if (url.endsWith('/preview')) { + return Promise.reject(new Error('backend error')); + } + return Promise.resolve({ + ok: true, + text() { return Promise.resolve(''); }, + status: 200, }); + })); + const errorStub = sinon.stub(); + document.addEventListener('network-error', errorStub); + element.open({detail: {patchNum: 2, + comment: ROBOT_COMMENT_WITH_TWO_FIXES}}); + flush(() => { + assert.isTrue(errorStub.called); + assert.deepEqual(element._currentFix, {}); + done(); }); }); + + test('apply fix button should call apply ' + + 'and navigate to change view', done => { + sandbox.stub(element.$.restAPI, 'applyFixSuggestion') + .returns(Promise.resolve({ok: true})); + sandbox.stub(Gerrit.Nav, 'navigateToChange'); + element._currentFix = {fix_id: '123'}; + + element._handleApplyFix().then(() => { + assert.isTrue(element.$.restAPI.applyFixSuggestion + .calledWithExactly('1', 2, '123')); + assert.isTrue(Gerrit.Nav.navigateToChange.calledWithExactly({ + _number: '1', + project: 'project', + }, 'edit', 2)); + + // reset gr-apply-fix-dialog and close + assert.deepEqual(element._currentFix, {}); + assert.equal(element._currentPreviews.length, 0); + done(); + }); + }); + + test('should not navigate to change view if incorect reponse', done => { + sandbox.stub(element.$.restAPI, 'applyFixSuggestion') + .returns(Promise.resolve({})); + sandbox.stub(Gerrit.Nav, 'navigateToChange'); + element._currentFix = {fix_id: '123'}; + + element._handleApplyFix().then(() => { + assert.isTrue(element.$.restAPI.applyFixSuggestion + .calledWithExactly('1', 2, '123')); + assert.isTrue(Gerrit.Nav.navigateToChange.notCalled); + + assert.equal(element._isApplyFixLoading, false); + done(); + }); + }); + + test('select fix forward and back of multiple suggested fixes', done => { + sandbox.stub(element.$.restAPI, 'getRobotCommentFixPreview') + .returns(Promise.resolve({ + f1: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['loqlwkqll'], + }, + { + b: ['qwqqsqw'], + }, + { + ab: ['qwqqsqw', 'qweqeqweqeq', 'qweqweq'], + }, + ], + }, + f2: { + meta_a: {}, + meta_b: {}, + content: [ + { + ab: ['eqweqweqwex'], + }, + { + b: ['zassdasd'], + }, + { + ab: ['zassdasd', 'dasdasda', 'asdasdad'], + }, + ], + }, + })); + sandbox.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve()); + + element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_TWO_FIXES}}) + .then(() => { + element._onNextFixClick(); + assert.equal(element._currentFix.fix_id, 'fix_2'); + element._onPrevFixClick(); + assert.equal(element._currentFix.fix_id, 'fix_1'); + done(); + }); + }); + + test('server-error should throw for failed apply call', done => { + sandbox.stub(window, 'fetch', (url => { + if (url.endsWith('/apply')) { + return Promise.reject(new Error('backend error')); + } + return Promise.resolve({ + ok: true, + text() { return Promise.resolve(''); }, + status: 200, + }); + })); + const errorStub = sinon.stub(); + document.addEventListener('network-error', errorStub); + sandbox.stub(Gerrit.Nav, 'navigateToChange'); + element._currentFix = {fix_id: '123'}; + element._handleApplyFix(); + flush(() => { + assert.isFalse(Gerrit.Nav.navigateToChange.called); + assert.isTrue(errorStub.called); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js index 490367a..95de0d1 100644 --- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js +++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api.js
@@ -14,520 +14,528 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const PARENT = 'PARENT'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-comment-api_html.js'; - /** - * Construct a change comments object, which can be data-bound to child - * elements of that which uses the gr-comment-api. - * - * @constructor - * @param {!Object} comments - * @param {!Object} robotComments - * @param {!Object} drafts - * @param {number} changeNum - */ - function ChangeComments(comments, robotComments, drafts, changeNum) { - this._comments = comments; - this._robotComments = robotComments; - this._drafts = drafts; - this._changeNum = changeNum; +const PARENT = 'PARENT'; + +/** + * Construct a change comments object, which can be data-bound to child + * elements of that which uses the gr-comment-api. + * + * @constructor + * @param {!Object} comments + * @param {!Object} robotComments + * @param {!Object} drafts + * @param {number} changeNum + */ +function ChangeComments(comments, robotComments, drafts, changeNum) { + this._comments = comments; + this._robotComments = robotComments; + this._drafts = drafts; + this._changeNum = changeNum; +} + +ChangeComments.prototype = { + get comments() { + return this._comments; + }, + get drafts() { + return this._drafts; + }, + get robotComments() { + return this._robotComments; + }, +}; + +ChangeComments.prototype._patchNumEquals = + Gerrit.PatchSetBehavior.patchNumEquals; +ChangeComments.prototype._isMergeParent = + Gerrit.PatchSetBehavior.isMergeParent; +ChangeComments.prototype._getParentIndex = + Gerrit.PatchSetBehavior.getParentIndex; + +/** + * Get an object mapping file paths to a boolean representing whether that + * path contains diff comments in the given patch set (including drafts and + * robot comments). + * + * Paths with comments are mapped to true, whereas paths without comments + * are not mapped. + * + * @param {Gerrit.PatchRange=} opt_patchRange The patch-range object containing + * patchNum and basePatchNum properties to represent the range. + * @return {!Object} + */ +ChangeComments.prototype.getPaths = function(opt_patchRange) { + const responses = [this.comments, this.drafts, this.robotComments]; + const commentMap = {}; + for (const response of responses) { + for (const path in response) { + if (response.hasOwnProperty(path) && + response[path].some(c => { + // If don't care about patch range, we know that the path exists. + if (!opt_patchRange) { return true; } + return this._isInPatchRange(c, opt_patchRange); + })) { + commentMap[path] = true; + } + } } + return commentMap; +}; - ChangeComments.prototype = { - get comments() { - return this._comments; - }, - get drafts() { - return this._drafts; - }, - get robotComments() { - return this._robotComments; - }, - }; +/** + * Gets all the comments and robot comments for the given change. + * + * @param {number=} opt_patchNum + * @return {!Object} + */ +ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) { + return this.getAllComments(false, opt_patchNum); +}; - ChangeComments.prototype._patchNumEquals = - Gerrit.PatchSetBehavior.patchNumEquals; - ChangeComments.prototype._isMergeParent = - Gerrit.PatchSetBehavior.isMergeParent; - ChangeComments.prototype._getParentIndex = - Gerrit.PatchSetBehavior.getParentIndex; +/** + * Gets all the comments for a particular thread group. Used for refreshing + * comments after the thread group has already been built. + * + * @param {string} rootId + * @return {!Array} an array of comments + */ +ChangeComments.prototype.getCommentsForThread = function(rootId) { + const allThreads = this.getAllThreadsForChange(); + const threadMatch = allThreads.find(t => t.rootId === rootId); - /** - * Get an object mapping file paths to a boolean representing whether that - * path contains diff comments in the given patch set (including drafts and - * robot comments). - * - * Paths with comments are mapped to true, whereas paths without comments - * are not mapped. - * - * @param {Gerrit.PatchRange=} opt_patchRange The patch-range object containing - * patchNum and basePatchNum properties to represent the range. - * @return {!Object} - */ - ChangeComments.prototype.getPaths = function(opt_patchRange) { - const responses = [this.comments, this.drafts, this.robotComments]; - const commentMap = {}; - for (const response of responses) { - for (const path in response) { - if (response.hasOwnProperty(path) && - response[path].some(c => { - // If don't care about patch range, we know that the path exists. - if (!opt_patchRange) { return true; } - return this._isInPatchRange(c, opt_patchRange); - })) { - commentMap[path] = true; - } - } + // In the event that a single draft comment was removed by the thread-list + // and the diff view is updating comments, there will no longer be a thread + // found. In this case, return null. + return threadMatch ? threadMatch.comments : null; +}; + +/** + * Filters an array of comments by line and side + * + * @param {!Array} comments + * @param {boolean} parentOnly whether the only comments returned should have + * the side attribute set to PARENT + * @param {string} commentSide whether the comment was left on the left or the + * right side regardless or unified or side-by-side + * @param {number=} opt_line line number, can be undefined if file comment + * @return {!Array} an array of comments + */ +ChangeComments.prototype._filterCommentsBySideAndLine = function(comments, + parentOnly, commentSide, opt_line) { + return comments.filter(c => { + // if parentOnly, only match comments with PARENT for the side. + let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT; + if (parentOnly) { + sideMatch = sideMatch && c.side === PARENT; } - return commentMap; - }; + return sideMatch && c.line === opt_line; + }).map(c => { + c.__commentSide = commentSide; + return c; + }); +}; - /** - * Gets all the comments and robot comments for the given change. - * - * @param {number=} opt_patchNum - * @return {!Object} - */ - ChangeComments.prototype.getAllPublishedComments = function(opt_patchNum) { - return this.getAllComments(false, opt_patchNum); - }; - - /** - * Gets all the comments for a particular thread group. Used for refreshing - * comments after the thread group has already been built. - * - * @param {string} rootId - * @return {!Array} an array of comments - */ - ChangeComments.prototype.getCommentsForThread = function(rootId) { - const allThreads = this.getAllThreadsForChange(); - const threadMatch = allThreads.find(t => t.rootId === rootId); - - // In the event that a single draft comment was removed by the thread-list - // and the diff view is updating comments, there will no longer be a thread - // found. In this case, return null. - return threadMatch ? threadMatch.comments : null; - }; - - /** - * Filters an array of comments by line and side - * - * @param {!Array} comments - * @param {boolean} parentOnly whether the only comments returned should have - * the side attribute set to PARENT - * @param {string} commentSide whether the comment was left on the left or the - * right side regardless or unified or side-by-side - * @param {number=} opt_line line number, can be undefined if file comment - * @return {!Array} an array of comments - */ - ChangeComments.prototype._filterCommentsBySideAndLine = function(comments, - parentOnly, commentSide, opt_line) { - return comments.filter(c => { - // if parentOnly, only match comments with PARENT for the side. - let sideMatch = parentOnly ? c.side === PARENT : c.side !== PARENT; - if (parentOnly) { - sideMatch = sideMatch && c.side === PARENT; - } - return sideMatch && c.line === opt_line; - }).map(c => { - c.__commentSide = commentSide; - return c; - }); - }; - - /** - * Gets all the comments and robot comments for the given change. - * - * @param {boolean=} opt_includeDrafts - * @param {number=} opt_patchNum - * @return {!Object} - */ - ChangeComments.prototype.getAllComments = function(opt_includeDrafts, - opt_patchNum) { - const paths = this.getPaths(); - const publishedComments = {}; - for (const path of Object.keys(paths)) { - let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum); - if (opt_includeDrafts) { - const drafts = this.getAllDraftsForPath(path, opt_patchNum) - .map(d => Object.assign({__draft: true}, d)); - commentsToAdd = commentsToAdd.concat(drafts); - } - publishedComments[path] = commentsToAdd; - } - return publishedComments; - }; - - /** - * Gets all the comments and robot comments for the given change. - * - * @param {number=} opt_patchNum - * @return {!Object} - */ - ChangeComments.prototype.getAllDrafts = function(opt_patchNum) { - const paths = this.getPaths(); - const drafts = {}; - for (const path of Object.keys(paths)) { - drafts[path] = this.getAllDraftsForPath(path, opt_patchNum); - } - return drafts; - }; - - /** - * Get the comments (robot comments) for a path and optional patch num. - * - * @param {!string} path - * @param {number=} opt_patchNum - * @param {boolean=} opt_includeDrafts - * @return {!Array} - */ - ChangeComments.prototype.getAllCommentsForPath = function(path, - opt_patchNum, opt_includeDrafts) { - const comments = this._comments[path] || []; - const robotComments = this._robotComments[path] || []; - let allComments = comments.concat(robotComments); +/** + * Gets all the comments and robot comments for the given change. + * + * @param {boolean=} opt_includeDrafts + * @param {number=} opt_patchNum + * @return {!Object} + */ +ChangeComments.prototype.getAllComments = function(opt_includeDrafts, + opt_patchNum) { + const paths = this.getPaths(); + const publishedComments = {}; + for (const path of Object.keys(paths)) { + let commentsToAdd = this.getAllCommentsForPath(path, opt_patchNum); if (opt_includeDrafts) { - const drafts = this.getAllDraftsForPath(path) + const drafts = this.getAllDraftsForPath(path, opt_patchNum) .map(d => Object.assign({__draft: true}, d)); - allComments = allComments.concat(drafts); + commentsToAdd = commentsToAdd.concat(drafts); } - if (!opt_patchNum) { return allComments; } - return (allComments || []).filter(c => - this._patchNumEquals(c.patch_set, opt_patchNum) - ); - }; + publishedComments[path] = commentsToAdd; + } + return publishedComments; +}; - /** - * Get the drafts for a path and optional patch num. - * - * @param {!string} path - * @param {number=} opt_patchNum - * @return {!Array} - */ - ChangeComments.prototype.getAllDraftsForPath = function(path, - opt_patchNum) { - const comments = this._drafts[path] || []; - if (!opt_patchNum) { return comments; } - return (comments || []).filter(c => - this._patchNumEquals(c.patch_set, opt_patchNum) - ); - }; +/** + * Gets all the comments and robot comments for the given change. + * + * @param {number=} opt_patchNum + * @return {!Object} + */ +ChangeComments.prototype.getAllDrafts = function(opt_patchNum) { + const paths = this.getPaths(); + const drafts = {}; + for (const path of Object.keys(paths)) { + drafts[path] = this.getAllDraftsForPath(path, opt_patchNum); + } + return drafts; +}; - /** - * Get the comments (with drafts and robot comments) for a path and - * patch-range. Returns an object with left and right properties mapping to - * arrays of comments in on either side of the patch range for that path. - * - * @param {!string} path - * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum - * and basePatchNum properties to represent the range. - * @param {Object=} opt_projectConfig Optional project config object to - * include in the meta sub-object. - * @return {!Gerrit.CommentsBySide} - */ - ChangeComments.prototype.getCommentsBySideForPath = function(path, - patchRange, opt_projectConfig) { - let comments = []; - let drafts = []; - let robotComments = []; - if (this.comments && this.comments[path]) { - comments = this.comments[path]; - } - if (this.drafts && this.drafts[path]) { - drafts = this.drafts[path]; - } - if (this.robotComments && this.robotComments[path]) { - robotComments = this.robotComments[path]; - } +/** + * Get the comments (robot comments) for a path and optional patch num. + * + * @param {!string} path + * @param {number=} opt_patchNum + * @param {boolean=} opt_includeDrafts + * @return {!Array} + */ +ChangeComments.prototype.getAllCommentsForPath = function(path, + opt_patchNum, opt_includeDrafts) { + const comments = this._comments[path] || []; + const robotComments = this._robotComments[path] || []; + let allComments = comments.concat(robotComments); + if (opt_includeDrafts) { + const drafts = this.getAllDraftsForPath(path) + .map(d => Object.assign({__draft: true}, d)); + allComments = allComments.concat(drafts); + } + if (!opt_patchNum) { return allComments; } + return (allComments || []).filter(c => + this._patchNumEquals(c.patch_set, opt_patchNum) + ); +}; - drafts.forEach(d => { d.__draft = true; }); +/** + * Get the drafts for a path and optional patch num. + * + * @param {!string} path + * @param {number=} opt_patchNum + * @return {!Array} + */ +ChangeComments.prototype.getAllDraftsForPath = function(path, + opt_patchNum) { + const comments = this._drafts[path] || []; + if (!opt_patchNum) { return comments; } + return (comments || []).filter(c => + this._patchNumEquals(c.patch_set, opt_patchNum) + ); +}; - const all = comments.concat(drafts).concat(robotComments); - - const baseComments = all.filter(c => - this._isInBaseOfPatchRange(c, patchRange)); - const revisionComments = all.filter(c => - this._isInRevisionOfPatchRange(c, patchRange)); - - return { - meta: { - changeNum: this._changeNum, - path, - patchRange, - projectConfig: opt_projectConfig, - }, - left: baseComments, - right: revisionComments, - }; - }; - - /** - * @param {!Object} comments Object keyed by file, with a value of an array - * of comments left on that file. - * @return {!Array} A flattened list of all comments, where each comment - * also includes the file that it was left on, which was the key of the - * originall object. - */ - ChangeComments.prototype._commentObjToArrayWithFile = function(comments) { - let commentArr = []; - for (const file of Object.keys(comments)) { - const commentsForFile = []; - for (const comment of comments[file]) { - commentsForFile.push(Object.assign({__path: file}, comment)); - } - commentArr = commentArr.concat(commentsForFile); - } - return commentArr; - }; - - ChangeComments.prototype._commentObjToArray = function(comments) { - let commentArr = []; - for (const file of Object.keys(comments)) { - commentArr = commentArr.concat(comments[file]); - } - return commentArr; - }; - - /** - * Computes a string counting the number of commens in a given file and path. - * - * @param {number} patchNum - * @param {string=} opt_path - * @return {number} - */ - ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) { - if (opt_path) { - return this.getAllCommentsForPath(opt_path, patchNum).length; - } - const allComments = this.getAllPublishedComments(patchNum); - return this._commentObjToArray(allComments).length; - }; - - /** - * Computes a string counting the number of draft comments in the entire - * change, optionally filtered by path and/or patchNum. - * - * @param {number=} opt_patchNum - * @param {string=} opt_path - * @return {number} - */ - ChangeComments.prototype.computeDraftCount = function(opt_patchNum, - opt_path) { - if (opt_path) { - return this.getAllDraftsForPath(opt_path, opt_patchNum).length; - } - const allDrafts = this.getAllDrafts(opt_patchNum); - return this._commentObjToArray(allDrafts).length; - }; - - /** - * Computes a number of unresolved comment threads in a given file and path. - * - * @param {number} patchNum - * @param {string=} opt_path - * @return {number} - */ - ChangeComments.prototype.computeUnresolvedNum = function(patchNum, - opt_path) { - let comments = []; - let drafts = []; - - if (opt_path) { - comments = this.getAllCommentsForPath(opt_path, patchNum); - drafts = this.getAllDraftsForPath(opt_path, patchNum); - } else { - comments = this._commentObjToArray( - this.getAllPublishedComments(patchNum)); - } - - comments = comments.concat(drafts); - - const threads = this.getCommentThreads(this._sortComments(comments)); - - const unresolvedThreads = threads - .filter(thread => - thread.comments.length && - thread.comments[thread.comments.length - 1].unresolved); - - return unresolvedThreads.length; - }; - - ChangeComments.prototype.getAllThreadsForChange = function() { - const comments = this._commentObjToArrayWithFile(this.getAllComments(true)); - const sortedComments = this._sortComments(comments); - return this.getCommentThreads(sortedComments); - }; - - ChangeComments.prototype._sortComments = function(comments) { - return comments.slice(0) - .sort( - (c1, c2) => util.parseDate(c1.updated) - util.parseDate(c2.updated) - ); - }; - - /** - * Computes all of the comments in thread format. - * - * @param {!Array} comments sorted by updated timestamp. - * @return {!Array} - */ - ChangeComments.prototype.getCommentThreads = function(comments) { - const threads = []; - const idThreadMap = {}; - for (const comment of comments) { - // If the comment is in reply to another comment, find that comment's - // thread and append to it. - if (comment.in_reply_to) { - const thread = idThreadMap[comment.in_reply_to]; - if (thread) { - thread.comments.push(comment); - idThreadMap[comment.id] = thread; - continue; - } - } - - // Otherwise, this comment starts its own thread. - const newThread = { - comments: [comment], - patchNum: comment.patch_set, - path: comment.__path, - line: comment.line, - rootId: comment.id, - }; - if (comment.side) { - newThread.commentSide = comment.side; - } - threads.push(newThread); - idThreadMap[comment.id] = newThread; - } - return threads; - }; - - /** - * Whether the given comment should be included in the base side of the - * given patch range. - * - * @param {!Object} comment - * @param {!Gerrit.PatchRange} range - * @return {boolean} - */ - ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) { - // If the base of the patch range is a parent of a merge, and the comment - // appears on a specific parent then only show the comment if the parent - // index of the comment matches that of the range. - if (comment.parent && comment.side === PARENT) { - return this._isMergeParent(range.basePatchNum) && - comment.parent === this._getParentIndex(range.basePatchNum); - } - - // If the base of the range is the parent of the patch: - if (range.basePatchNum === PARENT && - comment.side === PARENT && - this._patchNumEquals(comment.patch_set, range.patchNum)) { - return true; - } - // If the base of the range is not the parent of the patch: - if (range.basePatchNum !== PARENT && - comment.side !== PARENT && - this._patchNumEquals(comment.patch_set, range.basePatchNum)) { - return true; - } - return false; - }; - - /** - * Whether the given comment should be included in the revision side of the - * given patch range. - * - * @param {!Object} comment - * @param {!Gerrit.PatchRange} range - * @return {boolean} - */ - ChangeComments.prototype._isInRevisionOfPatchRange = function(comment, - range) { - return comment.side !== PARENT && - this._patchNumEquals(comment.patch_set, range.patchNum); - }; - - /** - * Whether the given comment should be included in the given patch range. - * - * @param {!Object} comment - * @param {!Gerrit.PatchRange} range - * @return {boolean|undefined} - */ - ChangeComments.prototype._isInPatchRange = function(comment, range) { - return this._isInBaseOfPatchRange(comment, range) || - this._isInRevisionOfPatchRange(comment, range); - }; - - /** - * @appliesMixin Gerrit.PatchSetMixin - * @extends Polymer.Element - */ - class GrCommentApi extends Polymer.mixinBehaviors( [ - Gerrit.PatchSetBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-comment-api'; } - - static get properties() { - return { - _changeComments: Object, - }; - } - - /** @override */ - created() { - super.created(); - this.addEventListener('reload-drafts', - changeNum => this.reloadDrafts(changeNum)); - } - - /** - * Load all comments (with drafts and robot comments) for the given change - * number. The returned promise resolves when the comments have loaded, but - * does not yield the comment data. - * - * @param {number} changeNum - * @return {!Promise<!Object>} - */ - loadAll(changeNum) { - const promises = []; - promises.push(this.$.restAPI.getDiffComments(changeNum)); - promises.push(this.$.restAPI.getDiffRobotComments(changeNum)); - promises.push(this.$.restAPI.getDiffDrafts(changeNum)); - - return Promise.all(promises).then(([comments, robotComments, drafts]) => { - this._changeComments = new ChangeComments(comments, - robotComments, drafts, changeNum); - return this._changeComments; - }); - } - - /** - * Re-initialize _changeComments with a new ChangeComments object, that - * uses the previous values for comments and robot comments, but fetches - * updated draft comments. - * - * @param {number} changeNum - * @return {!Promise<!Object>} - */ - reloadDrafts(changeNum) { - if (!this._changeComments) { - return this.loadAll(changeNum); - } - return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => { - this._changeComments = new ChangeComments(this._changeComments.comments, - this._changeComments.robotComments, drafts, changeNum); - return this._changeComments; - }); - } +/** + * Get the comments (with drafts and robot comments) for a path and + * patch-range. Returns an object with left and right properties mapping to + * arrays of comments in on either side of the patch range for that path. + * + * @param {!string} path + * @param {!Gerrit.PatchRange} patchRange The patch-range object containing patchNum + * and basePatchNum properties to represent the range. + * @param {Object=} opt_projectConfig Optional project config object to + * include in the meta sub-object. + * @return {!Gerrit.CommentsBySide} + */ +ChangeComments.prototype.getCommentsBySideForPath = function(path, + patchRange, opt_projectConfig) { + let comments = []; + let drafts = []; + let robotComments = []; + if (this.comments && this.comments[path]) { + comments = this.comments[path]; + } + if (this.drafts && this.drafts[path]) { + drafts = this.drafts[path]; + } + if (this.robotComments && this.robotComments[path]) { + robotComments = this.robotComments[path]; } - customElements.define(GrCommentApi.is, GrCommentApi); -})(); + drafts.forEach(d => { d.__draft = true; }); + + const all = comments.concat(drafts).concat(robotComments); + + const baseComments = all.filter(c => + this._isInBaseOfPatchRange(c, patchRange)); + const revisionComments = all.filter(c => + this._isInRevisionOfPatchRange(c, patchRange)); + + return { + meta: { + changeNum: this._changeNum, + path, + patchRange, + projectConfig: opt_projectConfig, + }, + left: baseComments, + right: revisionComments, + }; +}; + +/** + * @param {!Object} comments Object keyed by file, with a value of an array + * of comments left on that file. + * @return {!Array} A flattened list of all comments, where each comment + * also includes the file that it was left on, which was the key of the + * originall object. + */ +ChangeComments.prototype._commentObjToArrayWithFile = function(comments) { + let commentArr = []; + for (const file of Object.keys(comments)) { + const commentsForFile = []; + for (const comment of comments[file]) { + commentsForFile.push(Object.assign({__path: file}, comment)); + } + commentArr = commentArr.concat(commentsForFile); + } + return commentArr; +}; + +ChangeComments.prototype._commentObjToArray = function(comments) { + let commentArr = []; + for (const file of Object.keys(comments)) { + commentArr = commentArr.concat(comments[file]); + } + return commentArr; +}; + +/** + * Computes a string counting the number of commens in a given file and path. + * + * @param {number} patchNum + * @param {string=} opt_path + * @return {number} + */ +ChangeComments.prototype.computeCommentCount = function(patchNum, opt_path) { + if (opt_path) { + return this.getAllCommentsForPath(opt_path, patchNum).length; + } + const allComments = this.getAllPublishedComments(patchNum); + return this._commentObjToArray(allComments).length; +}; + +/** + * Computes a string counting the number of draft comments in the entire + * change, optionally filtered by path and/or patchNum. + * + * @param {number=} opt_patchNum + * @param {string=} opt_path + * @return {number} + */ +ChangeComments.prototype.computeDraftCount = function(opt_patchNum, + opt_path) { + if (opt_path) { + return this.getAllDraftsForPath(opt_path, opt_patchNum).length; + } + const allDrafts = this.getAllDrafts(opt_patchNum); + return this._commentObjToArray(allDrafts).length; +}; + +/** + * Computes a number of unresolved comment threads in a given file and path. + * + * @param {number} patchNum + * @param {string=} opt_path + * @return {number} + */ +ChangeComments.prototype.computeUnresolvedNum = function(patchNum, + opt_path) { + let comments = []; + let drafts = []; + + if (opt_path) { + comments = this.getAllCommentsForPath(opt_path, patchNum); + drafts = this.getAllDraftsForPath(opt_path, patchNum); + } else { + comments = this._commentObjToArray( + this.getAllPublishedComments(patchNum)); + } + + comments = comments.concat(drafts); + + const threads = this.getCommentThreads(this._sortComments(comments)); + + const unresolvedThreads = threads + .filter(thread => + thread.comments.length && + thread.comments[thread.comments.length - 1].unresolved); + + return unresolvedThreads.length; +}; + +ChangeComments.prototype.getAllThreadsForChange = function() { + const comments = this._commentObjToArrayWithFile(this.getAllComments(true)); + const sortedComments = this._sortComments(comments); + return this.getCommentThreads(sortedComments); +}; + +ChangeComments.prototype._sortComments = function(comments) { + return comments.slice(0) + .sort( + (c1, c2) => util.parseDate(c1.updated) - util.parseDate(c2.updated) + ); +}; + +/** + * Computes all of the comments in thread format. + * + * @param {!Array} comments sorted by updated timestamp. + * @return {!Array} + */ +ChangeComments.prototype.getCommentThreads = function(comments) { + const threads = []; + const idThreadMap = {}; + for (const comment of comments) { + // If the comment is in reply to another comment, find that comment's + // thread and append to it. + if (comment.in_reply_to) { + const thread = idThreadMap[comment.in_reply_to]; + if (thread) { + thread.comments.push(comment); + idThreadMap[comment.id] = thread; + continue; + } + } + + // Otherwise, this comment starts its own thread. + const newThread = { + comments: [comment], + patchNum: comment.patch_set, + path: comment.__path, + line: comment.line, + rootId: comment.id, + }; + if (comment.side) { + newThread.commentSide = comment.side; + } + threads.push(newThread); + idThreadMap[comment.id] = newThread; + } + return threads; +}; + +/** + * Whether the given comment should be included in the base side of the + * given patch range. + * + * @param {!Object} comment + * @param {!Gerrit.PatchRange} range + * @return {boolean} + */ +ChangeComments.prototype._isInBaseOfPatchRange = function(comment, range) { + // If the base of the patch range is a parent of a merge, and the comment + // appears on a specific parent then only show the comment if the parent + // index of the comment matches that of the range. + if (comment.parent && comment.side === PARENT) { + return this._isMergeParent(range.basePatchNum) && + comment.parent === this._getParentIndex(range.basePatchNum); + } + + // If the base of the range is the parent of the patch: + if (range.basePatchNum === PARENT && + comment.side === PARENT && + this._patchNumEquals(comment.patch_set, range.patchNum)) { + return true; + } + // If the base of the range is not the parent of the patch: + if (range.basePatchNum !== PARENT && + comment.side !== PARENT && + this._patchNumEquals(comment.patch_set, range.basePatchNum)) { + return true; + } + return false; +}; + +/** + * Whether the given comment should be included in the revision side of the + * given patch range. + * + * @param {!Object} comment + * @param {!Gerrit.PatchRange} range + * @return {boolean} + */ +ChangeComments.prototype._isInRevisionOfPatchRange = function(comment, + range) { + return comment.side !== PARENT && + this._patchNumEquals(comment.patch_set, range.patchNum); +}; + +/** + * Whether the given comment should be included in the given patch range. + * + * @param {!Object} comment + * @param {!Gerrit.PatchRange} range + * @return {boolean|undefined} + */ +ChangeComments.prototype._isInPatchRange = function(comment, range) { + return this._isInBaseOfPatchRange(comment, range) || + this._isInRevisionOfPatchRange(comment, range); +}; + +/** + * @appliesMixin Gerrit.PatchSetMixin + * @extends Polymer.Element + */ +class GrCommentApi extends mixinBehaviors( [ + Gerrit.PatchSetBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-comment-api'; } + + static get properties() { + return { + _changeComments: Object, + }; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('reload-drafts', + changeNum => this.reloadDrafts(changeNum)); + } + + /** + * Load all comments (with drafts and robot comments) for the given change + * number. The returned promise resolves when the comments have loaded, but + * does not yield the comment data. + * + * @param {number} changeNum + * @return {!Promise<!Object>} + */ + loadAll(changeNum) { + const promises = []; + promises.push(this.$.restAPI.getDiffComments(changeNum)); + promises.push(this.$.restAPI.getDiffRobotComments(changeNum)); + promises.push(this.$.restAPI.getDiffDrafts(changeNum)); + + return Promise.all(promises).then(([comments, robotComments, drafts]) => { + this._changeComments = new ChangeComments(comments, + robotComments, drafts, changeNum); + return this._changeComments; + }); + } + + /** + * Re-initialize _changeComments with a new ChangeComments object, that + * uses the previous values for comments and robot comments, but fetches + * updated draft comments. + * + * @param {number} changeNum + * @return {!Promise<!Object>} + */ + reloadDrafts(changeNum) { + if (!this._changeComments) { + return this.loadAll(changeNum); + } + return this.$.restAPI.getDiffDrafts(changeNum).then(drafts => { + this._changeComments = new ChangeComments(this._changeComments.comments, + this._changeComments.robotComments, drafts, changeNum); + return this._changeComments; + }); + } +} + +customElements.define(GrCommentApi.is, GrCommentApi);
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js index 317e9e5..215bfac 100644 --- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js +++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_html.js
@@ -1,27 +1,21 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-comment-api"> - <template> +export const htmlTemplate = html` <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-comment-api.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html index f2f7c0f..e39319a 100644 --- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html +++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="./gr-comment-api.html"> +<script type="module" src="./gr-comment-api.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-comment-api.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,697 +41,699 @@ </template> </test-fixture> -<script> - suite('gr-comment-api tests', async () => { - await readyToTest(); - const PARENT = 'PARENT'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-comment-api.js'; +suite('gr-comment-api tests', () => { + const PARENT = 'PARENT'; - let element; - let sandbox; + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + + test('loads logged-out', () => { + const changeNum = 1234; + + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(false)); + sandbox.stub(element.$.restAPI, 'getDiffComments') + .returns(Promise.resolve({ + 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}], + })); + sandbox.stub(element.$.restAPI, 'getDiffRobotComments') + .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]})); + sandbox.stub(element.$.restAPI, 'getDiffDrafts') + .returns(Promise.resolve({})); + + return element.loadAll(changeNum).then(() => { + assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly( + changeNum)); + assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly( + changeNum)); + assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly( + changeNum)); + assert.isOk(element._changeComments._comments); + assert.isOk(element._changeComments._robotComments); + assert.deepEqual(element._changeComments._drafts, {}); + }); + }); + + test('loads logged-in', () => { + const changeNum = 1234; + + sandbox.stub(element.$.restAPI, 'getLoggedIn') + .returns(Promise.resolve(true)); + sandbox.stub(element.$.restAPI, 'getDiffComments') + .returns(Promise.resolve({ + 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}], + })); + sandbox.stub(element.$.restAPI, 'getDiffRobotComments') + .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]})); + sandbox.stub(element.$.restAPI, 'getDiffDrafts') + .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]})); + + return element.loadAll(changeNum).then(() => { + assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly( + changeNum)); + assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly( + changeNum)); + assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly( + changeNum)); + assert.isOk(element._changeComments._comments); + assert.isOk(element._changeComments._robotComments); + assert.notDeepEqual(element._changeComments._drafts, {}); + }); + }); + + suite('reloadDrafts', () => { + let commentStub; + let robotCommentStub; + let draftStub; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('loads logged-out', () => { - const changeNum = 1234; - - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(false)); - sandbox.stub(element.$.restAPI, 'getDiffComments') - .returns(Promise.resolve({ - 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}], - })); - sandbox.stub(element.$.restAPI, 'getDiffRobotComments') - .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]})); - sandbox.stub(element.$.restAPI, 'getDiffDrafts') + commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments') .returns(Promise.resolve({})); + robotCommentStub = sandbox.stub(element.$.restAPI, + 'getDiffRobotComments').returns(Promise.resolve({})); + draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts') + .returns(Promise.resolve({})); + }); - return element.loadAll(changeNum).then(() => { - assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly( - changeNum)); - assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly( - changeNum)); - assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly( - changeNum)); - assert.isOk(element._changeComments._comments); - assert.isOk(element._changeComments._robotComments); - assert.deepEqual(element._changeComments._drafts, {}); + test('without loadAll first', done => { + assert.isNotOk(element._changeComments); + sandbox.spy(element, 'loadAll'); + element.reloadDrafts().then(() => { + assert.isTrue(element.loadAll.called); + assert.isOk(element._changeComments); + assert.equal(commentStub.callCount, 1); + assert.equal(robotCommentStub.callCount, 1); + assert.equal(draftStub.callCount, 1); + done(); }); }); - test('loads logged-in', () => { + test('with loadAll first', done => { + assert.isNotOk(element._changeComments); + element.loadAll() + .then(() => { + assert.isOk(element._changeComments); + assert.equal(commentStub.callCount, 1); + assert.equal(robotCommentStub.callCount, 1); + assert.equal(draftStub.callCount, 1); + return element.reloadDrafts(); + }) + .then(() => { + assert.isOk(element._changeComments); + assert.equal(commentStub.callCount, 1); + assert.equal(robotCommentStub.callCount, 1); + assert.equal(draftStub.callCount, 2); + done(); + }); + }); + }); + + suite('_changeComment methods', () => { + setup(done => { const changeNum = 1234; - - sandbox.stub(element.$.restAPI, 'getLoggedIn') - .returns(Promise.resolve(true)); - sandbox.stub(element.$.restAPI, 'getDiffComments') - .returns(Promise.resolve({ - 'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}], - })); - sandbox.stub(element.$.restAPI, 'getDiffRobotComments') - .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]})); - sandbox.stub(element.$.restAPI, 'getDiffDrafts') - .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]})); - - return element.loadAll(changeNum).then(() => { - assert.isTrue(element.$.restAPI.getDiffComments.calledWithExactly( - changeNum)); - assert.isTrue(element.$.restAPI.getDiffRobotComments.calledWithExactly( - changeNum)); - assert.isTrue(element.$.restAPI.getDiffDrafts.calledWithExactly( - changeNum)); - assert.isOk(element._changeComments._comments); - assert.isOk(element._changeComments._robotComments); - assert.notDeepEqual(element._changeComments._drafts, {}); + stub('gr-rest-api-interface', { + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + }); + element.loadAll(changeNum).then(() => { + done(); }); }); - suite('reloadDrafts', () => { - let commentStub; - let robotCommentStub; - let draftStub; + test('_isInBaseOfPatchRange', () => { + const comment = {patch_set: 1}; + const patchRange = {basePatchNum: 1, patchNum: 2}; + assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment, + patchRange)); + + patchRange.basePatchNum = PARENT; + assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment, + patchRange)); + + comment.side = PARENT; + assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment, + patchRange)); + + comment.patch_set = 2; + assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment, + patchRange)); + + patchRange.basePatchNum = -2; + comment.side = PARENT; + comment.parent = 1; + assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment, + patchRange)); + + comment.parent = 2; + assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment, + patchRange)); + }); + + test('_isInRevisionOfPatchRange', () => { + const comment = {patch_set: 123}; + const patchRange = {basePatchNum: 122, patchNum: 124}; + assert.isFalse(element._changeComments._isInRevisionOfPatchRange( + comment, patchRange)); + + patchRange.patchNum = 123; + assert.isTrue(element._changeComments._isInRevisionOfPatchRange( + comment, patchRange)); + + comment.side = PARENT; + assert.isFalse(element._changeComments._isInRevisionOfPatchRange( + comment, patchRange)); + }); + + test('_isInPatchRange', () => { + const patchRange1 = {basePatchNum: 122, patchNum: 124}; + const patchRange2 = {basePatchNum: 123, patchNum: 125}; + const patchRange3 = {basePatchNum: 124, patchNum: 125}; + + const isInBasePatchStub = sandbox.stub(element._changeComments, + '_isInBaseOfPatchRange'); + const isInRevisionPatchStub = sandbox.stub(element._changeComments, + '_isInRevisionOfPatchRange'); + + isInBasePatchStub.withArgs({}, patchRange1).returns(true); + isInBasePatchStub.withArgs({}, patchRange2).returns(false); + isInBasePatchStub.withArgs({}, patchRange3).returns(false); + + isInRevisionPatchStub.withArgs({}, patchRange1).returns(false); + isInRevisionPatchStub.withArgs({}, patchRange2).returns(true); + isInRevisionPatchStub.withArgs({}, patchRange3).returns(false); + + assert.isTrue(element._changeComments._isInPatchRange({}, patchRange1)); + assert.isTrue(element._changeComments._isInPatchRange({}, patchRange2)); + assert.isFalse(element._changeComments._isInPatchRange({}, + patchRange3)); + }); + + suite('comment ranges and paths', () => { + function makeTime(mins) { + return `2013-02-26 15:0${mins}:43.986000000`; + } + setup(() => { - commentStub = sandbox.stub(element.$.restAPI, 'getDiffComments') - .returns(Promise.resolve({})); - robotCommentStub = sandbox.stub(element.$.restAPI, - 'getDiffRobotComments').returns(Promise.resolve({})); - draftStub = sandbox.stub(element.$.restAPI, 'getDiffDrafts') - .returns(Promise.resolve({})); + element._changeComments._drafts = { + 'file/one': [ + { + id: 11, + patch_set: 2, + side: PARENT, + line: 1, + updated: makeTime(3), + }, + { + id: 12, + in_reply_to: 2, + patch_set: 2, + line: 1, + updated: makeTime(3), + }, + ], + 'file/two': [ + { + id: 5, + patch_set: 3, + line: 1, + updated: makeTime(3), + }, + ], + }; + element._changeComments._robotComments = { + 'file/one': [ + { + id: 1, + patch_set: 2, + side: PARENT, + line: 1, + updated: makeTime(1), + range: { + start_line: 1, + start_character: 2, + end_line: 2, + end_character: 2, + }, + }, { + id: 2, + in_reply_to: 4, + patch_set: 2, + unresolved: true, + line: 1, + updated: makeTime(2), + }, + ], + }; + element._changeComments._comments = { + 'file/one': [ + {id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)}, + {id: 4, patch_set: 2, line: 1, updated: makeTime(1)}, + ], + 'file/two': [ + {id: 5, patch_set: 2, line: 2, updated: makeTime(1)}, + {id: 6, patch_set: 3, line: 2, updated: makeTime(1)}, + ], + 'file/three': [ + { + id: 7, + patch_set: 2, + side: PARENT, + unresolved: true, + line: 1, + updated: makeTime(1), + }, + {id: 8, patch_set: 3, line: 1, updated: makeTime(1)}, + ], + 'file/four': [ + {id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)}, + {id: 10, patch_set: 5, line: 1, updated: makeTime(1)}, + ], + }; }); - test('without loadAll first', done => { - assert.isNotOk(element._changeComments); - sandbox.spy(element, 'loadAll'); - element.reloadDrafts().then(() => { - assert.isTrue(element.loadAll.called); - assert.isOk(element._changeComments); - assert.equal(commentStub.callCount, 1); - assert.equal(robotCommentStub.callCount, 1); - assert.equal(draftStub.callCount, 1); - done(); - }); - }); - - test('with loadAll first', done => { - assert.isNotOk(element._changeComments); - element.loadAll() - .then(() => { - assert.isOk(element._changeComments); - assert.equal(commentStub.callCount, 1); - assert.equal(robotCommentStub.callCount, 1); - assert.equal(draftStub.callCount, 1); - return element.reloadDrafts(); - }) - .then(() => { - assert.isOk(element._changeComments); - assert.equal(commentStub.callCount, 1); - assert.equal(robotCommentStub.callCount, 1); - assert.equal(draftStub.callCount, 2); - done(); - }); - }); - }); - - suite('_changeComment methods', () => { - setup(done => { - const changeNum = 1234; - stub('gr-rest-api-interface', { - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - }); - element.loadAll(changeNum).then(() => { - done(); - }); - }); - - test('_isInBaseOfPatchRange', () => { - const comment = {patch_set: 1}; - const patchRange = {basePatchNum: 1, patchNum: 2}; - assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment, - patchRange)); + test('getPaths', () => { + const patchRange = {basePatchNum: 1, patchNum: 4}; + let paths = element._changeComments.getPaths(patchRange); + assert.equal(Object.keys(paths).length, 0); patchRange.basePatchNum = PARENT; - assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment, - patchRange)); + patchRange.patchNum = 3; + paths = element._changeComments.getPaths(patchRange); + assert.notProperty(paths, 'file/one'); + assert.property(paths, 'file/two'); + assert.property(paths, 'file/three'); + assert.notProperty(paths, 'file/four'); - comment.side = PARENT; - assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment, - patchRange)); + patchRange.patchNum = 2; + paths = element._changeComments.getPaths(patchRange); + assert.property(paths, 'file/one'); + assert.property(paths, 'file/two'); + assert.property(paths, 'file/three'); + assert.notProperty(paths, 'file/four'); - comment.patch_set = 2; - assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment, - patchRange)); - - patchRange.basePatchNum = -2; - comment.side = PARENT; - comment.parent = 1; - assert.isFalse(element._changeComments._isInBaseOfPatchRange(comment, - patchRange)); - - comment.parent = 2; - assert.isTrue(element._changeComments._isInBaseOfPatchRange(comment, - patchRange)); + paths = element._changeComments.getPaths(); + assert.property(paths, 'file/one'); + assert.property(paths, 'file/two'); + assert.property(paths, 'file/three'); + assert.property(paths, 'file/four'); }); - test('_isInRevisionOfPatchRange', () => { - const comment = {patch_set: 123}; - const patchRange = {basePatchNum: 122, patchNum: 124}; - assert.isFalse(element._changeComments._isInRevisionOfPatchRange( - comment, patchRange)); + test('getCommentsBySideForPath', () => { + const patchRange = {basePatchNum: 1, patchNum: 3}; + let path = 'file/one'; + let comments = element._changeComments.getCommentsBySideForPath(path, + patchRange); + assert.equal(comments.meta.changeNum, 1234); + assert.equal(comments.left.length, 0); + assert.equal(comments.right.length, 0); - patchRange.patchNum = 123; - assert.isTrue(element._changeComments._isInRevisionOfPatchRange( - comment, patchRange)); + path = 'file/two'; + comments = element._changeComments.getCommentsBySideForPath(path, + patchRange); + assert.equal(comments.left.length, 0); + assert.equal(comments.right.length, 2); - comment.side = PARENT; - assert.isFalse(element._changeComments._isInRevisionOfPatchRange( - comment, patchRange)); + patchRange.basePatchNum = 2; + comments = element._changeComments.getCommentsBySideForPath(path, + patchRange); + assert.equal(comments.left.length, 1); + assert.equal(comments.right.length, 2); + + patchRange.basePatchNum = PARENT; + path = 'file/three'; + comments = element._changeComments.getCommentsBySideForPath(path, + patchRange); + assert.equal(comments.left.length, 0); + assert.equal(comments.right.length, 1); }); - test('_isInPatchRange', () => { - const patchRange1 = {basePatchNum: 122, patchNum: 124}; - const patchRange2 = {basePatchNum: 123, patchNum: 125}; - const patchRange3 = {basePatchNum: 124, patchNum: 125}; - - const isInBasePatchStub = sandbox.stub(element._changeComments, - '_isInBaseOfPatchRange'); - const isInRevisionPatchStub = sandbox.stub(element._changeComments, - '_isInRevisionOfPatchRange'); - - isInBasePatchStub.withArgs({}, patchRange1).returns(true); - isInBasePatchStub.withArgs({}, patchRange2).returns(false); - isInBasePatchStub.withArgs({}, patchRange3).returns(false); - - isInRevisionPatchStub.withArgs({}, patchRange1).returns(false); - isInRevisionPatchStub.withArgs({}, patchRange2).returns(true); - isInRevisionPatchStub.withArgs({}, patchRange3).returns(false); - - assert.isTrue(element._changeComments._isInPatchRange({}, patchRange1)); - assert.isTrue(element._changeComments._isInPatchRange({}, patchRange2)); - assert.isFalse(element._changeComments._isInPatchRange({}, - patchRange3)); + test('getAllCommentsForPath', () => { + let path = 'file/one'; + let comments = element._changeComments.getAllCommentsForPath(path); + assert.deepEqual(comments.length, 4); + path = 'file/two'; + comments = element._changeComments.getAllCommentsForPath(path, 2); + assert.deepEqual(comments.length, 1); }); - suite('comment ranges and paths', () => { - function makeTime(mins) { - return `2013-02-26 15:0${mins}:43.986000000`; - } + test('getAllDraftsForPath', () => { + const path = 'file/one'; + const drafts = element._changeComments.getAllDraftsForPath(path); + assert.deepEqual(drafts.length, 2); + }); - setup(() => { - element._changeComments._drafts = { - 'file/one': [ - { - id: 11, - patch_set: 2, - side: PARENT, - line: 1, - updated: makeTime(3), - }, - { - id: 12, - in_reply_to: 2, - patch_set: 2, - line: 1, - updated: makeTime(3), - }, - ], - 'file/two': [ + test('computeUnresolvedNum', () => { + assert.equal(element._changeComments + .computeUnresolvedNum(2, 'file/one'), 0); + assert.equal(element._changeComments + .computeUnresolvedNum(1, 'file/one'), 0); + assert.equal(element._changeComments + .computeUnresolvedNum(2, 'file/three'), 1); + }); + + test('computeUnresolvedNum w/ non-linear thread', () => { + element._changeComments._drafts = {}; + element._changeComments._robotComments = {}; + element._changeComments._comments = { + path: [{ + id: '9c6ba3c6_28b7d467', + patch_set: 1, + updated: '2018-02-28 14:41:13.000000000', + unresolved: true, + }, { + id: '3df7b331_0bead405', + patch_set: 1, + in_reply_to: '1c346623_ab85d14a', + updated: '2018-02-28 23:07:55.000000000', + unresolved: false, + }, { + id: '6153dce6_69958d1e', + patch_set: 1, + in_reply_to: '9c6ba3c6_28b7d467', + updated: '2018-02-28 17:11:31.000000000', + unresolved: true, + }, { + id: '1c346623_ab85d14a', + patch_set: 1, + in_reply_to: '9c6ba3c6_28b7d467', + updated: '2018-02-28 23:01:39.000000000', + unresolved: false, + }], + }; + assert.equal( + element._changeComments.computeUnresolvedNum(1, 'path'), 0); + }); + + test('computeCommentCount', () => { + assert.equal(element._changeComments + .computeCommentCount(2, 'file/one'), 4); + assert.equal(element._changeComments + .computeCommentCount(1, 'file/one'), 0); + assert.equal(element._changeComments + .computeCommentCount(2, 'file/three'), 1); + }); + + test('computeDraftCount', () => { + assert.equal(element._changeComments + .computeDraftCount(2, 'file/one'), 2); + assert.equal(element._changeComments + .computeDraftCount(1, 'file/one'), 0); + assert.equal(element._changeComments + .computeDraftCount(2, 'file/three'), 0); + assert.equal(element._changeComments + .computeDraftCount(), 3); + }); + + test('getAllPublishedComments', () => { + let publishedComments = element._changeComments + .getAllPublishedComments(); + assert.equal(Object.keys(publishedComments).length, 4); + assert.equal(Object.keys(publishedComments[['file/one']]).length, 4); + assert.equal(Object.keys(publishedComments[['file/two']]).length, 2); + publishedComments = element._changeComments + .getAllPublishedComments(2); + assert.equal(Object.keys(publishedComments[['file/one']]).length, 4); + assert.equal(Object.keys(publishedComments[['file/two']]).length, 1); + }); + + test('getAllComments', () => { + let comments = element._changeComments.getAllComments(); + assert.equal(Object.keys(comments).length, 4); + assert.equal(Object.keys(comments[['file/one']]).length, 4); + assert.equal(Object.keys(comments[['file/two']]).length, 2); + comments = element._changeComments.getAllComments(false, 2); + assert.equal(Object.keys(comments).length, 4); + assert.equal(Object.keys(comments[['file/one']]).length, 4); + assert.equal(Object.keys(comments[['file/two']]).length, 1); + // Include drafts + comments = element._changeComments.getAllComments(true); + assert.equal(Object.keys(comments).length, 4); + assert.equal(Object.keys(comments[['file/one']]).length, 6); + assert.equal(Object.keys(comments[['file/two']]).length, 3); + comments = element._changeComments.getAllComments(true, 2); + assert.equal(Object.keys(comments).length, 4); + assert.equal(Object.keys(comments[['file/one']]).length, 6); + assert.equal(Object.keys(comments[['file/two']]).length, 1); + }); + + test('computeAllThreads', () => { + const expectedThreads = [ + { + comments: [ { id: 5, - patch_set: 3, - line: 1, - updated: makeTime(3), + patch_set: 2, + line: 2, + __path: 'file/two', + updated: '2013-02-26 15:01:43.986000000', }, ], - }; - element._changeComments._robotComments = { - 'file/one': [ + patchNum: 2, + path: 'file/two', + line: 2, + rootId: 5, + }, { + comments: [ + { + id: 3, + patch_set: 2, + side: 'PARENT', + line: 2, + __path: 'file/one', + updated: '2013-02-26 15:01:43.986000000', + }, + ], + commentSide: 'PARENT', + patchNum: 2, + path: 'file/one', + line: 2, + rootId: 3, + }, { + comments: [ { id: 1, patch_set: 2, - side: PARENT, + side: 'PARENT', line: 1, - updated: makeTime(1), + updated: '2013-02-26 15:01:43.986000000', range: { start_line: 1, start_character: 2, end_line: 2, end_character: 2, }, - }, { + __path: 'file/one', + }, + ], + commentSide: 'PARENT', + patchNum: 2, + path: 'file/one', + line: 1, + rootId: 1, + }, { + comments: [ + { + id: 9, + patch_set: 5, + side: 'PARENT', + line: 1, + __path: 'file/four', + updated: '2013-02-26 15:01:43.986000000', + }, + ], + commentSide: 'PARENT', + patchNum: 5, + path: 'file/four', + line: 1, + rootId: 9, + }, { + comments: [ + { + id: 8, + patch_set: 3, + line: 1, + __path: 'file/three', + updated: '2013-02-26 15:01:43.986000000', + }, + ], + patchNum: 3, + path: 'file/three', + line: 1, + rootId: 8, + }, { + comments: [ + { + id: 7, + patch_set: 2, + side: 'PARENT', + unresolved: true, + line: 1, + __path: 'file/three', + updated: '2013-02-26 15:01:43.986000000', + }, + ], + commentSide: 'PARENT', + patchNum: 2, + path: 'file/three', + line: 1, + rootId: 7, + }, { + comments: [ + { + id: 4, + patch_set: 2, + line: 1, + __path: 'file/one', + updated: '2013-02-26 15:01:43.986000000', + }, + { id: 2, in_reply_to: 4, patch_set: 2, unresolved: true, line: 1, - updated: makeTime(2), + __path: 'file/one', + updated: '2013-02-26 15:02:43.986000000', }, - ], - }; - element._changeComments._comments = { - 'file/one': [ - {id: 3, patch_set: 2, side: PARENT, line: 2, updated: makeTime(1)}, - {id: 4, patch_set: 2, line: 1, updated: makeTime(1)}, - ], - 'file/two': [ - {id: 5, patch_set: 2, line: 2, updated: makeTime(1)}, - {id: 6, patch_set: 3, line: 2, updated: makeTime(1)}, - ], - 'file/three': [ { - id: 7, + id: 12, + in_reply_to: 2, patch_set: 2, - side: PARENT, - unresolved: true, line: 1, - updated: makeTime(1), + __path: 'file/one', + __draft: true, + updated: '2013-02-26 15:03:43.986000000', }, - {id: 8, patch_set: 3, line: 1, updated: makeTime(1)}, ], - 'file/four': [ - {id: 9, patch_set: 5, side: PARENT, line: 1, updated: makeTime(1)}, - {id: 10, patch_set: 5, line: 1, updated: makeTime(1)}, - ], - }; - }); - - test('getPaths', () => { - const patchRange = {basePatchNum: 1, patchNum: 4}; - let paths = element._changeComments.getPaths(patchRange); - assert.equal(Object.keys(paths).length, 0); - - patchRange.basePatchNum = PARENT; - patchRange.patchNum = 3; - paths = element._changeComments.getPaths(patchRange); - assert.notProperty(paths, 'file/one'); - assert.property(paths, 'file/two'); - assert.property(paths, 'file/three'); - assert.notProperty(paths, 'file/four'); - - patchRange.patchNum = 2; - paths = element._changeComments.getPaths(patchRange); - assert.property(paths, 'file/one'); - assert.property(paths, 'file/two'); - assert.property(paths, 'file/three'); - assert.notProperty(paths, 'file/four'); - - paths = element._changeComments.getPaths(); - assert.property(paths, 'file/one'); - assert.property(paths, 'file/two'); - assert.property(paths, 'file/three'); - assert.property(paths, 'file/four'); - }); - - test('getCommentsBySideForPath', () => { - const patchRange = {basePatchNum: 1, patchNum: 3}; - let path = 'file/one'; - let comments = element._changeComments.getCommentsBySideForPath(path, - patchRange); - assert.equal(comments.meta.changeNum, 1234); - assert.equal(comments.left.length, 0); - assert.equal(comments.right.length, 0); - - path = 'file/two'; - comments = element._changeComments.getCommentsBySideForPath(path, - patchRange); - assert.equal(comments.left.length, 0); - assert.equal(comments.right.length, 2); - - patchRange.basePatchNum = 2; - comments = element._changeComments.getCommentsBySideForPath(path, - patchRange); - assert.equal(comments.left.length, 1); - assert.equal(comments.right.length, 2); - - patchRange.basePatchNum = PARENT; - path = 'file/three'; - comments = element._changeComments.getCommentsBySideForPath(path, - patchRange); - assert.equal(comments.left.length, 0); - assert.equal(comments.right.length, 1); - }); - - test('getAllCommentsForPath', () => { - let path = 'file/one'; - let comments = element._changeComments.getAllCommentsForPath(path); - assert.deepEqual(comments.length, 4); - path = 'file/two'; - comments = element._changeComments.getAllCommentsForPath(path, 2); - assert.deepEqual(comments.length, 1); - }); - - test('getAllDraftsForPath', () => { - const path = 'file/one'; - const drafts = element._changeComments.getAllDraftsForPath(path); - assert.deepEqual(drafts.length, 2); - }); - - test('computeUnresolvedNum', () => { - assert.equal(element._changeComments - .computeUnresolvedNum(2, 'file/one'), 0); - assert.equal(element._changeComments - .computeUnresolvedNum(1, 'file/one'), 0); - assert.equal(element._changeComments - .computeUnresolvedNum(2, 'file/three'), 1); - }); - - test('computeUnresolvedNum w/ non-linear thread', () => { - element._changeComments._drafts = {}; - element._changeComments._robotComments = {}; - element._changeComments._comments = { - path: [{ - id: '9c6ba3c6_28b7d467', - patch_set: 1, - updated: '2018-02-28 14:41:13.000000000', - unresolved: true, - }, { - id: '3df7b331_0bead405', - patch_set: 1, - in_reply_to: '1c346623_ab85d14a', - updated: '2018-02-28 23:07:55.000000000', - unresolved: false, - }, { - id: '6153dce6_69958d1e', - patch_set: 1, - in_reply_to: '9c6ba3c6_28b7d467', - updated: '2018-02-28 17:11:31.000000000', - unresolved: true, - }, { - id: '1c346623_ab85d14a', - patch_set: 1, - in_reply_to: '9c6ba3c6_28b7d467', - updated: '2018-02-28 23:01:39.000000000', - unresolved: false, - }], - }; - assert.equal( - element._changeComments.computeUnresolvedNum(1, 'path'), 0); - }); - - test('computeCommentCount', () => { - assert.equal(element._changeComments - .computeCommentCount(2, 'file/one'), 4); - assert.equal(element._changeComments - .computeCommentCount(1, 'file/one'), 0); - assert.equal(element._changeComments - .computeCommentCount(2, 'file/three'), 1); - }); - - test('computeDraftCount', () => { - assert.equal(element._changeComments - .computeDraftCount(2, 'file/one'), 2); - assert.equal(element._changeComments - .computeDraftCount(1, 'file/one'), 0); - assert.equal(element._changeComments - .computeDraftCount(2, 'file/three'), 0); - assert.equal(element._changeComments - .computeDraftCount(), 3); - }); - - test('getAllPublishedComments', () => { - let publishedComments = element._changeComments - .getAllPublishedComments(); - assert.equal(Object.keys(publishedComments).length, 4); - assert.equal(Object.keys(publishedComments[['file/one']]).length, 4); - assert.equal(Object.keys(publishedComments[['file/two']]).length, 2); - publishedComments = element._changeComments - .getAllPublishedComments(2); - assert.equal(Object.keys(publishedComments[['file/one']]).length, 4); - assert.equal(Object.keys(publishedComments[['file/two']]).length, 1); - }); - - test('getAllComments', () => { - let comments = element._changeComments.getAllComments(); - assert.equal(Object.keys(comments).length, 4); - assert.equal(Object.keys(comments[['file/one']]).length, 4); - assert.equal(Object.keys(comments[['file/two']]).length, 2); - comments = element._changeComments.getAllComments(false, 2); - assert.equal(Object.keys(comments).length, 4); - assert.equal(Object.keys(comments[['file/one']]).length, 4); - assert.equal(Object.keys(comments[['file/two']]).length, 1); - // Include drafts - comments = element._changeComments.getAllComments(true); - assert.equal(Object.keys(comments).length, 4); - assert.equal(Object.keys(comments[['file/one']]).length, 6); - assert.equal(Object.keys(comments[['file/two']]).length, 3); - comments = element._changeComments.getAllComments(true, 2); - assert.equal(Object.keys(comments).length, 4); - assert.equal(Object.keys(comments[['file/one']]).length, 6); - assert.equal(Object.keys(comments[['file/two']]).length, 1); - }); - - test('computeAllThreads', () => { - const expectedThreads = [ - { - comments: [ - { - id: 5, - patch_set: 2, - line: 2, - __path: 'file/two', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - patchNum: 2, - path: 'file/two', - line: 2, - rootId: 5, - }, { - comments: [ - { - id: 3, - patch_set: 2, - side: 'PARENT', - line: 2, - __path: 'file/one', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - commentSide: 'PARENT', - patchNum: 2, - path: 'file/one', - line: 2, - rootId: 3, - }, { - comments: [ - { - id: 1, - patch_set: 2, - side: 'PARENT', - line: 1, - updated: '2013-02-26 15:01:43.986000000', - range: { - start_line: 1, - start_character: 2, - end_line: 2, - end_character: 2, - }, - __path: 'file/one', - }, - ], - commentSide: 'PARENT', - patchNum: 2, - path: 'file/one', - line: 1, - rootId: 1, - }, { - comments: [ - { - id: 9, - patch_set: 5, - side: 'PARENT', - line: 1, - __path: 'file/four', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - commentSide: 'PARENT', - patchNum: 5, - path: 'file/four', - line: 1, - rootId: 9, - }, { - comments: [ - { - id: 8, - patch_set: 3, - line: 1, - __path: 'file/three', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - patchNum: 3, - path: 'file/three', - line: 1, - rootId: 8, - }, { - comments: [ - { - id: 7, - patch_set: 2, - side: 'PARENT', - unresolved: true, - line: 1, - __path: 'file/three', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - commentSide: 'PARENT', - patchNum: 2, - path: 'file/three', - line: 1, - rootId: 7, - }, { - comments: [ - { - id: 4, - patch_set: 2, - line: 1, - __path: 'file/one', - updated: '2013-02-26 15:01:43.986000000', - }, - { - id: 2, - in_reply_to: 4, - patch_set: 2, - unresolved: true, - line: 1, - __path: 'file/one', - updated: '2013-02-26 15:02:43.986000000', - }, - { - id: 12, - in_reply_to: 2, - patch_set: 2, - line: 1, - __path: 'file/one', - __draft: true, - updated: '2013-02-26 15:03:43.986000000', - }, - ], - patchNum: 2, - path: 'file/one', - line: 1, - rootId: 4, - }, { - comments: [ - { - id: 6, - patch_set: 3, - line: 2, - __path: 'file/two', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - patchNum: 3, - path: 'file/two', - line: 2, - rootId: 6, - }, { - comments: [ - { - id: 10, - patch_set: 5, - line: 1, - __path: 'file/four', - updated: '2013-02-26 15:01:43.986000000', - }, - ], - rootId: 10, - patchNum: 5, - path: 'file/four', - line: 1, - }, { - comments: [ - { - id: 5, - patch_set: 3, - line: 1, - __path: 'file/two', - __draft: true, - updated: '2013-02-26 15:03:43.986000000', - }, - ], - rootId: 5, - patchNum: 3, - path: 'file/two', - line: 1, - }, { - comments: [ - { - id: 11, - patch_set: 2, - side: 'PARENT', - line: 1, - __path: 'file/one', - __draft: true, - updated: '2013-02-26 15:03:43.986000000', - }, - ], - rootId: 11, - commentSide: 'PARENT', - patchNum: 2, - path: 'file/one', - line: 1, - }, - ]; - const threads = element._changeComments.getAllThreadsForChange(); - assert.deepEqual(threads, expectedThreads); - }); - - test('getCommentsForThreadGroup', () => { - let expectedComments = [ - { - __path: 'file/one', - id: 4, - patch_set: 2, - line: 1, - updated: '2013-02-26 15:01:43.986000000', - }, - { - __path: 'file/one', - id: 2, - in_reply_to: 4, - patch_set: 2, - unresolved: true, - line: 1, - updated: '2013-02-26 15:02:43.986000000', - }, - { - __path: 'file/one', - __draft: true, - id: 12, - in_reply_to: 2, - patch_set: 2, - line: 1, - updated: '2013-02-26 15:03:43.986000000', - }, - ]; - assert.deepEqual(element._changeComments.getCommentsForThread(4), - expectedComments); - - expectedComments = [{ - id: 11, - patch_set: 2, - side: 'PARENT', + patchNum: 2, + path: 'file/one', line: 1, + rootId: 4, + }, { + comments: [ + { + id: 6, + patch_set: 3, + line: 2, + __path: 'file/two', + updated: '2013-02-26 15:01:43.986000000', + }, + ], + patchNum: 3, + path: 'file/two', + line: 2, + rootId: 6, + }, { + comments: [ + { + id: 10, + patch_set: 5, + line: 1, + __path: 'file/four', + updated: '2013-02-26 15:01:43.986000000', + }, + ], + rootId: 10, + patchNum: 5, + path: 'file/four', + line: 1, + }, { + comments: [ + { + id: 5, + patch_set: 3, + line: 1, + __path: 'file/two', + __draft: true, + updated: '2013-02-26 15:03:43.986000000', + }, + ], + rootId: 5, + patchNum: 3, + path: 'file/two', + line: 1, + }, { + comments: [ + { + id: 11, + patch_set: 2, + side: 'PARENT', + line: 1, + __path: 'file/one', + __draft: true, + updated: '2013-02-26 15:03:43.986000000', + }, + ], + rootId: 11, + commentSide: 'PARENT', + patchNum: 2, + path: 'file/one', + line: 1, + }, + ]; + const threads = element._changeComments.getAllThreadsForChange(); + assert.deepEqual(threads, expectedThreads); + }); + + test('getCommentsForThreadGroup', () => { + let expectedComments = [ + { + __path: 'file/one', + id: 4, + patch_set: 2, + line: 1, + updated: '2013-02-26 15:01:43.986000000', + }, + { + __path: 'file/one', + id: 2, + in_reply_to: 4, + patch_set: 2, + unresolved: true, + line: 1, + updated: '2013-02-26 15:02:43.986000000', + }, + { __path: 'file/one', __draft: true, + id: 12, + in_reply_to: 2, + patch_set: 2, + line: 1, updated: '2013-02-26 15:03:43.986000000', - }]; + }, + ]; + assert.deepEqual(element._changeComments.getCommentsForThread(4), + expectedComments); - assert.deepEqual(element._changeComments.getCommentsForThread(11), - expectedComments); + expectedComments = [{ + id: 11, + patch_set: 2, + side: 'PARENT', + line: 1, + __path: 'file/one', + __draft: true, + updated: '2013-02-26 15:03:43.986000000', + }]; - assert.deepEqual(element._changeComments.getCommentsForThread(1000), - null); - }); + assert.deepEqual(element._changeComments.getCommentsForThread(11), + expectedComments); + + assert.deepEqual(element._changeComments.getCommentsForThread(1000), + null); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js index 1bc4674..529c559 100644 --- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer.js
@@ -14,101 +14,108 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const TOOLTIP_MAP = new Map([ - [Gerrit.CoverageType.COVERED, 'Covered by tests.'], - [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'], - [Gerrit.CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'], - [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'], - ]); +import '../../../types/types.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-coverage-layer_html.js'; - /** @extends Polymer.Element */ - class GrCoverageLayer extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-coverage-layer'; } +const TOOLTIP_MAP = new Map([ + [Gerrit.CoverageType.COVERED, 'Covered by tests.'], + [Gerrit.CoverageType.NOT_COVERED, 'Not covered by tests.'], + [Gerrit.CoverageType.PARTIALLY_COVERED, 'Partially covered by tests.'], + [Gerrit.CoverageType.NOT_INSTRUMENTED, 'Not instrumented by any tests.'], +]); - static get properties() { - return { - /** - * Must be sorted by code_range.start_line. - * Must only contain ranges that match the side. - * - * @type {!Array<!Gerrit.CoverageRange>} - */ - coverageRanges: Array, - side: String, +/** @extends Polymer.Element */ +class GrCoverageLayer extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** - * We keep track of the line number from the previous annotate() call, - * and also of the index of the coverage range that had matched. - * annotate() calls are coming in with increasing line numbers and - * coverage ranges are sorted by line number. So this is a very simple - * and efficient way for finding the coverage range that matches a given - * line number. - */ - _lineNumber: { - type: Number, - value: 0, - }, - _index: { - type: Number, - value: 0, - }, - }; - } + static get is() { return 'gr-coverage-layer'; } + static get properties() { + return { /** - * Layer method to add annotations to a line. + * Must be sorted by code_range.start_line. + * Must only contain ranges that match the side. * - * @param {!HTMLElement} el Not used for this layer. - * @param {!HTMLElement} lineNumberEl The <td> element with the line number. - * @param {!Object} line Not used for this layer. + * @type {!Array<!Gerrit.CoverageRange>} */ - annotate(el, lineNumberEl, line) { - if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) { - return; - } - const elementLineNumber = parseInt( - lineNumberEl.getAttribute('data-value'), 10); - if (!elementLineNumber || elementLineNumber < 1) return; + coverageRanges: Array, + side: String, - // If the line number is smaller than before, then we have to reset our - // algorithm and start searching the coverage ranges from the beginning. - // That happens for example when you expand diff sections. - if (elementLineNumber < this._lineNumber) { - this._index = 0; - } - this._lineNumber = elementLineNumber; - - // We simply loop through all the coverage ranges until we find one that - // matches the line number. - while (this._index < this.coverageRanges.length) { - const coverageRange = this.coverageRanges[this._index]; - - // If the line number has moved past the current coverage range, then - // try the next coverage range. - if (this._lineNumber > coverageRange.code_range.end_line) { - this._index++; - continue; - } - - // If the line number has not reached the next coverage range (and the - // range before also did not match), then this line has not been - // instrumented. Nothing to do for this line. - if (this._lineNumber < coverageRange.code_range.start_line) { - return; - } - - // The line number is within the current coverage range. Style it! - lineNumberEl.classList.add(coverageRange.type); - lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type); - return; - } - } + /** + * We keep track of the line number from the previous annotate() call, + * and also of the index of the coverage range that had matched. + * annotate() calls are coming in with increasing line numbers and + * coverage ranges are sorted by line number. So this is a very simple + * and efficient way for finding the coverage range that matches a given + * line number. + */ + _lineNumber: { + type: Number, + value: 0, + }, + _index: { + type: Number, + value: 0, + }, + }; } - customElements.define(GrCoverageLayer.is, GrCoverageLayer); -})(); + /** + * Layer method to add annotations to a line. + * + * @param {!HTMLElement} el Not used for this layer. + * @param {!HTMLElement} lineNumberEl The <td> element with the line number. + * @param {!Object} line Not used for this layer. + */ + annotate(el, lineNumberEl, line) { + if (!lineNumberEl || !lineNumberEl.classList.contains(this.side)) { + return; + } + const elementLineNumber = parseInt( + lineNumberEl.getAttribute('data-value'), 10); + if (!elementLineNumber || elementLineNumber < 1) return; + + // If the line number is smaller than before, then we have to reset our + // algorithm and start searching the coverage ranges from the beginning. + // That happens for example when you expand diff sections. + if (elementLineNumber < this._lineNumber) { + this._index = 0; + } + this._lineNumber = elementLineNumber; + + // We simply loop through all the coverage ranges until we find one that + // matches the line number. + while (this._index < this.coverageRanges.length) { + const coverageRange = this.coverageRanges[this._index]; + + // If the line number has moved past the current coverage range, then + // try the next coverage range. + if (this._lineNumber > coverageRange.code_range.end_line) { + this._index++; + continue; + } + + // If the line number has not reached the next coverage range (and the + // range before also did not match), then this line has not been + // instrumented. Nothing to do for this line. + if (this._lineNumber < coverageRange.code_range.start_line) { + return; + } + + // The line number is within the current coverage range. Style it! + lineNumberEl.classList.add(coverageRange.type); + lineNumberEl.title = TOOLTIP_MAP.get(coverageRange.type); + return; + } + } +} + +customElements.define(GrCoverageLayer.is, GrCoverageLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js index 63517cf..29757e5 100644 --- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js +++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_html.js
@@ -1,26 +1,21 @@ -<!-- -@license -Copyright (C) 2019 The Android Open Source Project +/** + * @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.js'; -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 +export const htmlTemplate = html` -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<script src="../../../types/types.js"></script> -<script src="../gr-diff-highlight/gr-annotation.js"></script> - -<dom-module id="gr-coverage-layer"> - <template> - </template> - <script src="gr-coverage-layer.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html index 8439a22..69948c3 100644 --- a/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-coverage-layer/gr-coverage-layer_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-coverage-layer</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../gr-diff/gr-diff-line.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../gr-diff/gr-diff-line.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-coverage-layer.html"> +<script type="module" src="./gr-coverage-layer.js"></script> -<script>void(0);</script> +<script type="module"> +import '../gr-diff/gr-diff-line.js'; +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-coverage-layer.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,106 +43,109 @@ </template> </test-fixture> -<script> - suite('gr-coverage-layer', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../gr-diff/gr-diff-line.js'; +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-coverage-layer.js'; +suite('gr-coverage-layer', () => { + let element; - setup(() => { - const initialCoverageRanges = [ - { - type: 'COVERED', - side: 'right', - code_range: { - start_line: 1, - end_line: 2, - }, + setup(() => { + const initialCoverageRanges = [ + { + type: 'COVERED', + side: 'right', + code_range: { + start_line: 1, + end_line: 2, }, - { - type: 'NOT_COVERED', - side: 'right', - code_range: { - start_line: 3, - end_line: 4, - }, + }, + { + type: 'NOT_COVERED', + side: 'right', + code_range: { + start_line: 3, + end_line: 4, }, - { - type: 'PARTIALLY_COVERED', - side: 'right', - code_range: { - start_line: 5, - end_line: 6, - }, + }, + { + type: 'PARTIALLY_COVERED', + side: 'right', + code_range: { + start_line: 5, + end_line: 6, }, - { - type: 'NOT_INSTRUMENTED', - side: 'right', - code_range: { - start_line: 8, - end_line: 9, - }, + }, + { + type: 'NOT_INSTRUMENTED', + side: 'right', + code_range: { + start_line: 8, + end_line: 9, }, - ]; + }, + ]; - element = fixture('basic'); - element.coverageRanges = initialCoverageRanges; - element.side = 'right'; + element = fixture('basic'); + element.coverageRanges = initialCoverageRanges; + element.side = 'right'; + }); + + suite('annotate', () => { + function createLine(lineNumber) { + const lineEl = document.createElement('div'); + lineEl.setAttribute('data-side', 'right'); + lineEl.setAttribute('data-value', lineNumber); + lineEl.className = 'right'; + return lineEl; + } + + function checkLine(lineNumber, className, opt_negated) { + const line = createLine(lineNumber); + element.annotate(undefined, line, undefined); + let contains = line.classList.contains(className); + if (opt_negated) contains = !contains; + assert.isTrue(contains); + } + + test('line 1-2 are covered', () => { + checkLine(1, 'COVERED'); + checkLine(2, 'COVERED'); }); - suite('annotate', () => { - function createLine(lineNumber) { - const lineEl = document.createElement('div'); - lineEl.setAttribute('data-side', 'right'); - lineEl.setAttribute('data-value', lineNumber); - lineEl.className = 'right'; - return lineEl; - } + test('line 3-4 are not covered', () => { + checkLine(3, 'NOT_COVERED'); + checkLine(4, 'NOT_COVERED'); + }); - function checkLine(lineNumber, className, opt_negated) { - const line = createLine(lineNumber); - element.annotate(undefined, line, undefined); - let contains = line.classList.contains(className); - if (opt_negated) contains = !contains; - assert.isTrue(contains); - } + test('line 5-6 are partially covered', () => { + checkLine(5, 'PARTIALLY_COVERED'); + checkLine(6, 'PARTIALLY_COVERED'); + }); - test('line 1-2 are covered', () => { - checkLine(1, 'COVERED'); - checkLine(2, 'COVERED'); - }); + test('line 7 is implicitly not instrumented', () => { + checkLine(7, 'COVERED', true); + checkLine(7, 'NOT_COVERED', true); + checkLine(7, 'PARTIALLY_COVERED', true); + checkLine(7, 'NOT_INSTRUMENTED', true); + }); - test('line 3-4 are not covered', () => { - checkLine(3, 'NOT_COVERED'); - checkLine(4, 'NOT_COVERED'); - }); + test('line 8-9 are not instrumented', () => { + checkLine(8, 'NOT_INSTRUMENTED'); + checkLine(9, 'NOT_INSTRUMENTED'); + }); - test('line 5-6 are partially covered', () => { - checkLine(5, 'PARTIALLY_COVERED'); - checkLine(6, 'PARTIALLY_COVERED'); - }); - - test('line 7 is implicitly not instrumented', () => { - checkLine(7, 'COVERED', true); - checkLine(7, 'NOT_COVERED', true); - checkLine(7, 'PARTIALLY_COVERED', true); - checkLine(7, 'NOT_INSTRUMENTED', true); - }); - - test('line 8-9 are not instrumented', () => { - checkLine(8, 'NOT_INSTRUMENTED'); - checkLine(9, 'NOT_INSTRUMENTED'); - }); - - test('coverage correct, if annotate is called out of order', () => { - checkLine(8, 'NOT_INSTRUMENTED'); - checkLine(1, 'COVERED'); - checkLine(5, 'PARTIALLY_COVERED'); - checkLine(3, 'NOT_COVERED'); - checkLine(6, 'PARTIALLY_COVERED'); - checkLine(4, 'NOT_COVERED'); - checkLine(9, 'NOT_INSTRUMENTED'); - checkLine(2, 'COVERED'); - }); + test('coverage correct, if annotate is called out of order', () => { + checkLine(8, 'NOT_INSTRUMENTED'); + checkLine(1, 'COVERED'); + checkLine(5, 'PARTIALLY_COVERED'); + checkLine(3, 'NOT_COVERED'); + checkLine(6, 'PARTIALLY_COVERED'); + checkLine(4, 'NOT_COVERED'); + checkLine(9, 'NOT_INSTRUMENTED'); + checkLine(2, 'COVERED'); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js index 9e1ec9e..637c8f7 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -1,417 +1,438 @@ /** * @license - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2016 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the 'License'); + * 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, + * 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. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-coverage-layer/gr-coverage-layer.js'; +import '../gr-diff-processor/gr-diff-processor.js'; +import '../../shared/gr-hovercard/gr-hovercard.js'; +import '../gr-ranged-comment-layer/gr-ranged-comment-layer.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff/gr-diff-group.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import './gr-diff-builder.js'; +import './gr-diff-builder-side-by-side.js'; +import './gr-diff-builder-unified.js'; +import './gr-diff-builder-image.js'; +import './gr-diff-builder-binary.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-builder-element_html.js'; - const TRAILING_WHITESPACE_PATTERN = /\s+$/; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; - // https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740 - const COMMIT_MSG_PATH = '/COMMIT_MSG'; - const COMMIT_MSG_LINE_LENGTH = 72; +const TRAILING_WHITESPACE_PATTERN = /\s+$/; + +// https://gerrit.googlesource.com/gerrit/+/234616a8627334686769f1de989d286039f4d6a5/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js#740 +const COMMIT_MSG_PATH = '/COMMIT_MSG'; +const COMMIT_MSG_LINE_LENGTH = 72; + +/** + * @appliesMixin Gerrit.FireMixin + */ +class GrDiffBuilderElement extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-builder'; } + /** + * Fired when the diff begins rendering. + * + * @event render-start + */ /** - * @appliesMixin Gerrit.FireMixin + * Fired when the diff finishes rendering text content. + * + * @event render-content */ - class GrDiffBuilderElement extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff-builder'; } - /** - * Fired when the diff begins rendering. - * - * @event render-start - */ - /** - * Fired when the diff finishes rendering text content. - * - * @event render-content - */ + static get properties() { + return { + diff: Object, + changeNum: String, + patchNum: String, + viewMode: String, + isImageDiff: Boolean, + baseImage: Object, + revisionImage: Object, + parentIndex: Number, + path: String, + projectName: String, - static get properties() { - return { - diff: Object, - changeNum: String, - patchNum: String, - viewMode: String, - isImageDiff: Boolean, - baseImage: Object, - revisionImage: Object, - parentIndex: Number, - path: String, - projectName: String, + _builder: Object, + _groups: Array, + _layers: Array, + _showTabs: Boolean, + /** @type {!Array<!Gerrit.HoveredRange>} */ + commentRanges: { + type: Array, + value: () => [], + }, + /** @type {!Array<!Gerrit.CoverageRange>} */ + coverageRanges: { + type: Array, + value: () => [], + }, + _leftCoverageRanges: { + type: Array, + computed: '_computeLeftCoverageRanges(coverageRanges)', + }, + _rightCoverageRanges: { + type: Array, + computed: '_computeRightCoverageRanges(coverageRanges)', + }, + /** + * The promise last returned from `render()` while the asynchronous + * rendering is running - `null` otherwise. Provides a `cancel()` + * method that rejects it with `{isCancelled: true}`. + * + * @type {?Object} + */ + _cancelableRenderPromise: Object, + layers: { + type: Array, + value: [], + }, + }; + } - _builder: Object, - _groups: Array, - _layers: Array, - _showTabs: Boolean, - /** @type {!Array<!Gerrit.HoveredRange>} */ - commentRanges: { - type: Array, - value: () => [], - }, - /** @type {!Array<!Gerrit.CoverageRange>} */ - coverageRanges: { - type: Array, - value: () => [], - }, - _leftCoverageRanges: { - type: Array, - computed: '_computeLeftCoverageRanges(coverageRanges)', - }, - _rightCoverageRanges: { - type: Array, - computed: '_computeRightCoverageRanges(coverageRanges)', - }, - /** - * The promise last returned from `render()` while the asynchronous - * rendering is running - `null` otherwise. Provides a `cancel()` - * method that rejects it with `{isCancelled: true}`. - * - * @type {?Object} - */ - _cancelableRenderPromise: Object, - layers: { - type: Array, - value: [], - }, - }; + get diffElement() { + return this.queryEffectiveChildren('#diffTable'); + } + + static get observers() { + return [ + '_groupsChanged(_groups.splices)', + ]; + } + + _computeLeftCoverageRanges(coverageRanges) { + return coverageRanges.filter(range => range && range.side === 'left'); + } + + _computeRightCoverageRanges(coverageRanges) { + return coverageRanges.filter(range => range && range.side === 'right'); + } + + render(keyLocations, prefs) { + // Setting up annotation layers must happen after plugins are + // installed, and |render| satisfies the requirement, however, + // |attached| doesn't because in the diff view page, the element is + // attached before plugins are installed. + this._setupAnnotationLayers(); + + this._showTabs = !!prefs.show_tabs; + this._showTrailingWhitespace = !!prefs.show_whitespace_errors; + + // Stop the processor if it's running. + this.cancel(); + + this._builder = this._getDiffBuilder(this.diff, prefs); + + this.$.processor.context = prefs.context; + this.$.processor.keyLocations = keyLocations; + + this._clearDiffContent(); + this._builder.addColumns(this.diffElement, prefs.font_size); + + const isBinary = !!(this.isImageDiff || this.diff.binary); + + this.dispatchEvent(new CustomEvent( + 'render-start', {bubbles: true, composed: true})); + this._cancelableRenderPromise = util.makeCancelable( + this.$.processor.process(this.diff.content, isBinary) + .then(() => { + if (this.isImageDiff) { + this._builder.renderDiff(); + } + this.dispatchEvent(new CustomEvent('render-content', + {bubbles: true, composed: true})); + })); + return this._cancelableRenderPromise + .finally(() => { this._cancelableRenderPromise = null; }) + // Mocca testing does not like uncaught rejections, so we catch + // the cancels which are expected and should not throw errors in + // tests. + .catch(e => { if (!e.isCanceled) return Promise.reject(e); }); + } + + _setupAnnotationLayers() { + const layers = [ + this._createTrailingWhitespaceLayer(), + this._createIntralineLayer(), + this._createTabIndicatorLayer(), + this.$.rangeLayer, + this.$.coverageLayerLeft, + this.$.coverageLayerRight, + ]; + + if (this.layers) { + layers.push(...this.layers); } + this._layers = layers; + } - get diffElement() { - return this.queryEffectiveChildren('#diffTable'); - } - - static get observers() { - return [ - '_groupsChanged(_groups.splices)', - ]; - } - - _computeLeftCoverageRanges(coverageRanges) { - return coverageRanges.filter(range => range && range.side === 'left'); - } - - _computeRightCoverageRanges(coverageRanges) { - return coverageRanges.filter(range => range && range.side === 'right'); - } - - render(keyLocations, prefs) { - // Setting up annotation layers must happen after plugins are - // installed, and |render| satisfies the requirement, however, - // |attached| doesn't because in the diff view page, the element is - // attached before plugins are installed. - this._setupAnnotationLayers(); - - this._showTabs = !!prefs.show_tabs; - this._showTrailingWhitespace = !!prefs.show_whitespace_errors; - - // Stop the processor if it's running. - this.cancel(); - - this._builder = this._getDiffBuilder(this.diff, prefs); - - this.$.processor.context = prefs.context; - this.$.processor.keyLocations = keyLocations; - - this._clearDiffContent(); - this._builder.addColumns(this.diffElement, prefs.font_size); - - const isBinary = !!(this.isImageDiff || this.diff.binary); - - this.dispatchEvent(new CustomEvent( - 'render-start', {bubbles: true, composed: true})); - this._cancelableRenderPromise = util.makeCancelable( - this.$.processor.process(this.diff.content, isBinary) - .then(() => { - if (this.isImageDiff) { - this._builder.renderDiff(); - } - this.dispatchEvent(new CustomEvent('render-content', - {bubbles: true, composed: true})); - })); - return this._cancelableRenderPromise - .finally(() => { this._cancelableRenderPromise = null; }) - // Mocca testing does not like uncaught rejections, so we catch - // the cancels which are expected and should not throw errors in - // tests. - .catch(e => { if (!e.isCanceled) return Promise.reject(e); }); - } - - _setupAnnotationLayers() { - const layers = [ - this._createTrailingWhitespaceLayer(), - this._createIntralineLayer(), - this._createTabIndicatorLayer(), - this.$.rangeLayer, - this.$.coverageLayerLeft, - this.$.coverageLayerRight, - ]; - - if (this.layers) { - layers.push(...this.layers); - } - this._layers = layers; - } - - getLineElByChild(node) { - while (node) { - if (node instanceof Element) { - if (node.classList.contains('lineNum')) { - return node; - } - if (node.classList.contains('section')) { - return null; - } + getLineElByChild(node) { + while (node) { + if (node instanceof Element) { + if (node.classList.contains('lineNum')) { + return node; } - node = node.previousSibling || node.parentElement; - } - return null; - } - - getLineNumberByChild(node) { - const lineEl = this.getLineElByChild(node); - return lineEl ? - parseInt(lineEl.getAttribute('data-value'), 10) : - null; - } - - getContentByLine(lineNumber, opt_side, opt_root) { - return this._builder.getContentByLine(lineNumber, opt_side, opt_root); - } - - getContentByLineEl(lineEl) { - const root = Polymer.dom(lineEl.parentElement); - const side = this.getSideByLineEl(lineEl); - const line = lineEl.getAttribute('data-value'); - return this.getContentByLine(line, side, root); - } - - getLineElByNumber(lineNumber, opt_side) { - const sideSelector = opt_side ? ('.' + opt_side) : ''; - return this.diffElement.querySelector( - '.lineNum[data-value="' + lineNumber + '"]' + sideSelector); - } - - getContentsByLineRange(startLine, endLine, opt_side) { - const result = []; - this._builder.findLinesByRange(startLine, endLine, opt_side, null, - result); - return result; - } - - getSideByLineEl(lineEl) { - return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? - GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; - } - - emitGroup(group, sectionEl) { - this._builder.emitGroup(group, sectionEl); - } - - showContext(newGroups, sectionEl) { - const groups = this._builder.groups; - - const contextIndex = groups.findIndex(group => - group.element === sectionEl - ); - groups.splice(contextIndex, 1, ...newGroups); - - for (const newGroup of newGroups) { - this._builder.emitGroup(newGroup, sectionEl); - } - sectionEl.parentNode.removeChild(sectionEl); - - this.async(() => this.fire('render-content'), 1); - } - - cancel() { - this.$.processor.cancel(); - if (this._cancelableRenderPromise) { - this._cancelableRenderPromise.cancel(); - this._cancelableRenderPromise = null; - } - } - - _handlePreferenceError(pref) { - const message = `The value of the '${pref}' user preference is ` + - `invalid. Fix in diff preferences`; - this.dispatchEvent(new CustomEvent('show-alert', { - detail: { - message, - }, bubbles: true, composed: true})); - throw Error(`Invalid preference value: ${pref}`); - } - - _getDiffBuilder(diff, prefs) { - if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) { - this._handlePreferenceError('tab size'); - return; - } - - if (isNaN(prefs.line_length) || prefs.line_length <= 0) { - this._handlePreferenceError('diff width'); - return; - } - - const localPrefs = Object.assign({}, prefs); - if (this.path === COMMIT_MSG_PATH) { - // override line_length for commit msg the same way as - // in gr-diff - localPrefs.line_length = COMMIT_MSG_LINE_LENGTH; - } - - let builder = null; - if (this.isImageDiff) { - builder = new GrDiffBuilderImage( - diff, - localPrefs, - this.diffElement, - this.baseImage, - this.revisionImage); - } else if (diff.binary) { - // If the diff is binary, but not an image. - return new GrDiffBuilderBinary( - diff, - localPrefs, - this.diffElement); - } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { - builder = new GrDiffBuilderSideBySide( - diff, - localPrefs, - this.diffElement, - this._layers - ); - } else if (this.viewMode === DiffViewMode.UNIFIED) { - builder = new GrDiffBuilderUnified( - diff, - localPrefs, - this.diffElement, - this._layers); - } - if (!builder) { - throw Error('Unsupported diff view mode: ' + this.viewMode); - } - return builder; - } - - _clearDiffContent() { - this.diffElement.innerHTML = null; - } - - _groupsChanged(changeRecord) { - if (!changeRecord) { return; } - for (const splice of changeRecord.indexSplices) { - let group; - for (let i = 0; i < splice.addedCount; i++) { - group = splice.object[splice.index + i]; - this._builder.groups.push(group); - this._builder.emitGroup(group); + if (node.classList.contains('section')) { + return null; } } + node = node.previousSibling || node.parentElement; } + return null; + } - _createIntralineLayer() { - return { - // Take a DIV.contentText element and a line object with intraline - // differences to highlight and apply them to the element as - // annotations. - annotate(contentEl, lineNumberEl, line) { - const HL_CLASS = 'style-scope gr-diff intraline'; - for (const highlight of line.highlights) { - // The start and end indices could be the same if a highlight is - // meant to start at the end of a line and continue onto the - // next one. Ignore it. - if (highlight.startIndex === highlight.endIndex) { continue; } + getLineNumberByChild(node) { + const lineEl = this.getLineElByChild(node); + return lineEl ? + parseInt(lineEl.getAttribute('data-value'), 10) : + null; + } - // If endIndex isn't present, continue to the end of the line. - const endIndex = highlight.endIndex === undefined ? - line.text.length : - highlight.endIndex; + getContentByLine(lineNumber, opt_side, opt_root) { + return this._builder.getContentByLine(lineNumber, opt_side, opt_root); + } - GrAnnotation.annotateElement( - contentEl, - highlight.startIndex, - endIndex - highlight.startIndex, - HL_CLASS); - } - }, - }; + getContentByLineEl(lineEl) { + const root = dom(lineEl.parentElement); + const side = this.getSideByLineEl(lineEl); + const line = lineEl.getAttribute('data-value'); + return this.getContentByLine(line, side, root); + } + + getLineElByNumber(lineNumber, opt_side) { + const sideSelector = opt_side ? ('.' + opt_side) : ''; + return this.diffElement.querySelector( + '.lineNum[data-value="' + lineNumber + '"]' + sideSelector); + } + + getContentsByLineRange(startLine, endLine, opt_side) { + const result = []; + this._builder.findLinesByRange(startLine, endLine, opt_side, null, + result); + return result; + } + + getSideByLineEl(lineEl) { + return lineEl.classList.contains(GrDiffBuilder.Side.RIGHT) ? + GrDiffBuilder.Side.RIGHT : GrDiffBuilder.Side.LEFT; + } + + emitGroup(group, sectionEl) { + this._builder.emitGroup(group, sectionEl); + } + + showContext(newGroups, sectionEl) { + const groups = this._builder.groups; + + const contextIndex = groups.findIndex(group => + group.element === sectionEl + ); + groups.splice(contextIndex, 1, ...newGroups); + + for (const newGroup of newGroups) { + this._builder.emitGroup(newGroup, sectionEl); } + sectionEl.parentNode.removeChild(sectionEl); - _createTabIndicatorLayer() { - const show = () => this._showTabs; - return { - annotate(contentEl, lineNumberEl, line) { - // If visible tabs are disabled, do nothing. - if (!show()) { return; } + this.async(() => this.fire('render-content'), 1); + } - // Find and annotate the locations of tabs. - const split = line.text.split('\t'); - if (!split) { return; } - for (let i = 0, pos = 0; i < split.length - 1; i++) { - // Skip forward by the length of the content - pos += split[i].length; - - GrAnnotation.annotateElement(contentEl, pos, 1, - 'style-scope gr-diff tab-indicator'); - - // Skip forward by one tab character. - pos++; - } - }, - }; - } - - _createTrailingWhitespaceLayer() { - const show = function() { - return this._showTrailingWhitespace; - }.bind(this); - - return { - annotate(contentEl, lineNumberEl, line) { - if (!show()) { return; } - - const match = line.text.match(TRAILING_WHITESPACE_PATTERN); - if (match) { - // Normalize string positions in case there is unicode before or - // within the match. - const index = GrAnnotation.getStringLength( - line.text.substr(0, match.index)); - const length = GrAnnotation.getStringLength(match[0]); - GrAnnotation.annotateElement(contentEl, index, length, - 'style-scope gr-diff trailing-whitespace'); - } - }, - }; - } - - setBlame(blame) { - if (!this._builder || !blame) { return; } - this._builder.setBlame(blame); + cancel() { + this.$.processor.cancel(); + if (this._cancelableRenderPromise) { + this._cancelableRenderPromise.cancel(); + this._cancelableRenderPromise = null; } } - customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement); -})(); + _handlePreferenceError(pref) { + const message = `The value of the '${pref}' user preference is ` + + `invalid. Fix in diff preferences`; + this.dispatchEvent(new CustomEvent('show-alert', { + detail: { + message, + }, bubbles: true, composed: true})); + throw Error(`Invalid preference value: ${pref}`); + } + + _getDiffBuilder(diff, prefs) { + if (isNaN(prefs.tab_size) || prefs.tab_size <= 0) { + this._handlePreferenceError('tab size'); + return; + } + + if (isNaN(prefs.line_length) || prefs.line_length <= 0) { + this._handlePreferenceError('diff width'); + return; + } + + const localPrefs = Object.assign({}, prefs); + if (this.path === COMMIT_MSG_PATH) { + // override line_length for commit msg the same way as + // in gr-diff + localPrefs.line_length = COMMIT_MSG_LINE_LENGTH; + } + + let builder = null; + if (this.isImageDiff) { + builder = new GrDiffBuilderImage( + diff, + localPrefs, + this.diffElement, + this.baseImage, + this.revisionImage); + } else if (diff.binary) { + // If the diff is binary, but not an image. + return new GrDiffBuilderBinary( + diff, + localPrefs, + this.diffElement); + } else if (this.viewMode === DiffViewMode.SIDE_BY_SIDE) { + builder = new GrDiffBuilderSideBySide( + diff, + localPrefs, + this.diffElement, + this._layers + ); + } else if (this.viewMode === DiffViewMode.UNIFIED) { + builder = new GrDiffBuilderUnified( + diff, + localPrefs, + this.diffElement, + this._layers); + } + if (!builder) { + throw Error('Unsupported diff view mode: ' + this.viewMode); + } + return builder; + } + + _clearDiffContent() { + this.diffElement.innerHTML = null; + } + + _groupsChanged(changeRecord) { + if (!changeRecord) { return; } + for (const splice of changeRecord.indexSplices) { + let group; + for (let i = 0; i < splice.addedCount; i++) { + group = splice.object[splice.index + i]; + this._builder.groups.push(group); + this._builder.emitGroup(group); + } + } + } + + _createIntralineLayer() { + return { + // Take a DIV.contentText element and a line object with intraline + // differences to highlight and apply them to the element as + // annotations. + annotate(contentEl, lineNumberEl, line) { + const HL_CLASS = 'style-scope gr-diff intraline'; + for (const highlight of line.highlights) { + // The start and end indices could be the same if a highlight is + // meant to start at the end of a line and continue onto the + // next one. Ignore it. + if (highlight.startIndex === highlight.endIndex) { continue; } + + // If endIndex isn't present, continue to the end of the line. + const endIndex = highlight.endIndex === undefined ? + line.text.length : + highlight.endIndex; + + GrAnnotation.annotateElement( + contentEl, + highlight.startIndex, + endIndex - highlight.startIndex, + HL_CLASS); + } + }, + }; + } + + _createTabIndicatorLayer() { + const show = () => this._showTabs; + return { + annotate(contentEl, lineNumberEl, line) { + // If visible tabs are disabled, do nothing. + if (!show()) { return; } + + // Find and annotate the locations of tabs. + const split = line.text.split('\t'); + if (!split) { return; } + for (let i = 0, pos = 0; i < split.length - 1; i++) { + // Skip forward by the length of the content + pos += split[i].length; + + GrAnnotation.annotateElement(contentEl, pos, 1, + 'style-scope gr-diff tab-indicator'); + + // Skip forward by one tab character. + pos++; + } + }, + }; + } + + _createTrailingWhitespaceLayer() { + const show = function() { + return this._showTrailingWhitespace; + }.bind(this); + + return { + annotate(contentEl, lineNumberEl, line) { + if (!show()) { return; } + + const match = line.text.match(TRAILING_WHITESPACE_PATTERN); + if (match) { + // Normalize string positions in case there is unicode before or + // within the match. + const index = GrAnnotation.getStringLength( + line.text.substr(0, match.index)); + const length = GrAnnotation.getStringLength(match[0]); + GrAnnotation.annotateElement(contentEl, index, length, + 'style-scope gr-diff trailing-whitespace'); + } + }, + }; + } + + setBlame(blame) { + if (!this._builder || !blame) { return; } + this._builder.setBlame(blame); + } +} + +customElements.define(GrDiffBuilderElement.is, GrDiffBuilderElement);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js index bf8b0dc..c8df78f 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_html.js
@@ -1,54 +1,27 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../gr-coverage-layer/gr-coverage-layer.html"> -<link rel="import" href="../gr-diff-processor/gr-diff-processor.html"> -<link rel="import" href="../../../elements/shared/gr-hovercard/gr-hovercard.html"> -<link rel="import" href="../gr-ranged-comment-layer/gr-ranged-comment-layer.html"> -<script src="../../../scripts/util.js"></script> -<script src="../gr-diff/gr-diff-line.js"></script> -<script src="../gr-diff/gr-diff-group.js"></script> -<script src="../gr-diff-highlight/gr-annotation.js"></script> -<script src="gr-diff-builder.js"></script> -<script src="gr-diff-builder-side-by-side.js"></script> -<script src="gr-diff-builder-unified.js"></script> -<script src="gr-diff-builder-image.js"></script> -<script src="gr-diff-builder-binary.js"></script> - -<dom-module id="gr-diff-builder"> - <template> +export const htmlTemplate = html` <div class="contentWrapper"> <slot></slot> </div> - <gr-ranged-comment-layer - id="rangeLayer" - comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer> - <gr-coverage-layer - id="coverageLayerLeft" - coverage-ranges="[[_leftCoverageRanges]]" - side="left"></gr-coverage-layer> - <gr-coverage-layer - id="coverageLayerRight" - coverage-ranges="[[_rightCoverageRanges]]" - side="right"></gr-coverage-layer> - <gr-diff-processor - id="processor" - groups="{{_groups}}"></gr-diff-processor> - </template> - <script src="gr-diff-builder-element.js"></script> -</dom-module> + <gr-ranged-comment-layer id="rangeLayer" comment-ranges="[[commentRanges]]"></gr-ranged-comment-layer> + <gr-coverage-layer id="coverageLayerLeft" coverage-ranges="[[_leftCoverageRanges]]" side="left"></gr-coverage-layer> + <gr-coverage-layer id="coverageLayerRight" coverage-ranges="[[_rightCoverageRanges]]" side="right"></gr-coverage-layer> + <gr-diff-processor id="processor" groups="{{_groups}}"></gr-diff-processor> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html index 3af5522..07edc7d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.html
@@ -19,23 +19,35 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-builder</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> -<script src="../gr-diff/gr-diff-line.js"></script> -<script src="../gr-diff/gr-diff-group.js"></script> -<script src="../gr-diff-highlight/gr-annotation.js"></script> -<script src="gr-diff-builder.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> +<script type="module" src="../gr-diff/gr-diff-line.js"></script> +<script type="module" src="../gr-diff/gr-diff-group.js"></script> +<script type="module" src="../gr-diff-highlight/gr-annotation.js"></script> +<script type="module" src="./gr-diff-builder.js"></script> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> -<link rel="import" href="gr-diff-builder-element.html"> +<script type="module" src="../../shared/gr-rest-api-interface/gr-rest-api-interface.js"></script> +<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script> +<script type="module" src="./gr-diff-builder-element.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff/gr-diff-group.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import './gr-diff-builder.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-diff-builder-element.js'; +void(0); +</script> <test-fixture id="basic"> <template is="dom-template"> @@ -59,985 +71,1024 @@ </template> </test-fixture> -<script> - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff/gr-diff-group.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import './gr-diff-builder.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-diff-builder-element.js'; +import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; - suite('gr-diff-builder tests', async () => { - await readyToTest(); - let prefs; - let element; - let builder; - let sandbox; - const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>'; +suite('gr-diff-builder tests', () => { + let prefs; + let element; + let builder; + let sandbox; + const LINE_FEED_HTML = '<span class="style-scope gr-diff br"></span>'; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - getProjectConfig() { return Promise.resolve({}); }, - }); - prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - }; - builder = new GrDiffBuilder({content: []}, prefs); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + getProjectConfig() { return Promise.resolve({}); }, }); + prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + }; + builder = new GrDiffBuilder({content: []}, prefs); + }); - teardown(() => { sandbox.restore(); }); + teardown(() => { sandbox.restore(); }); - test('_createElement classStr applies all classes', () => { - const node = builder._createElement('div', 'test classes'); - assert.isTrue(node.classList.contains('gr-diff')); - assert.isTrue(node.classList.contains('test')); - assert.isTrue(node.classList.contains('classes')); - }); + test('_createElement classStr applies all classes', () => { + const node = builder._createElement('div', 'test classes'); + assert.isTrue(node.classList.contains('gr-diff')); + assert.isTrue(node.classList.contains('test')); + assert.isTrue(node.classList.contains('classes')); + }); - test('context control buttons', () => { - // Create 10 lines. - const lines = []; - for (let i = 0; i < 10; i++) { - const line = new GrDiffLine(GrDiffLine.Type.BOTH); - line.beforeNumber = i + 1; - line.afterNumber = i + 1; - line.text = 'lorem upsum'; - lines.push(line); - } - - const contextLine = { - contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)], - }; - - const section = {}; - // Does not include +10 buttons when there are fewer than 11 lines. - let td = builder._createContextControl(section, contextLine); - let buttons = td.querySelectorAll('gr-button.showContext'); - - assert.equal(buttons.length, 1); - assert.equal(Polymer.dom(buttons[0]).textContent, 'Show 10 common lines'); - - // Add another line. + test('context control buttons', () => { + // Create 10 lines. + const lines = []; + for (let i = 0; i < 10; i++) { const line = new GrDiffLine(GrDiffLine.Type.BOTH); + line.beforeNumber = i + 1; + line.afterNumber = i + 1; line.text = 'lorem upsum'; - line.beforeNumber = 11; - line.afterNumber = 11; - contextLine.contextGroups[0].addLine(line); + lines.push(line); + } - // Includes +10 buttons when there are at least 11 lines. - td = builder._createContextControl(section, contextLine); - buttons = td.querySelectorAll('gr-button.showContext'); + const contextLine = { + contextGroups: [new GrDiffGroup(GrDiffGroup.Type.BOTH, lines)], + }; - assert.equal(buttons.length, 3); - assert.equal(Polymer.dom(buttons[0]).textContent, '+10 above'); - assert.equal(Polymer.dom(buttons[1]).textContent, 'Show 11 common lines'); - assert.equal(Polymer.dom(buttons[2]).textContent, '+10 below'); - }); + const section = {}; + // Does not include +10 buttons when there are fewer than 11 lines. + let td = builder._createContextControl(section, contextLine); + let buttons = td.querySelectorAll('gr-button.showContext'); - test('newlines 1', () => { - let text = 'abcdef'; + assert.equal(buttons.length, 1); + assert.equal(dom(buttons[0]).textContent, 'Show 10 common lines'); - assert.equal(builder._formatText(text, 4, 10).innerHTML, text); - text = 'a'.repeat(20); - assert.equal(builder._formatText(text, 4, 10).innerHTML, - 'a'.repeat(10) + - LINE_FEED_HTML + - 'a'.repeat(10)); - }); + // Add another line. + const line = new GrDiffLine(GrDiffLine.Type.BOTH); + line.text = 'lorem upsum'; + line.beforeNumber = 11; + line.afterNumber = 11; + contextLine.contextGroups[0].addLine(line); - test('newlines 2', () => { - const text = '<span class="thumbsup">👍</span>'; - assert.equal(builder._formatText(text, 4, 10).innerHTML, - '<span clas' + - LINE_FEED_HTML + - 's="thumbsu' + - LINE_FEED_HTML + - 'p">👍</span' + - LINE_FEED_HTML + - '>'); - }); + // Includes +10 buttons when there are at least 11 lines. + td = builder._createContextControl(section, contextLine); + buttons = td.querySelectorAll('gr-button.showContext'); - test('newlines 3', () => { - const text = '01234\t56789'; - assert.equal(builder._formatText(text, 4, 10).innerHTML, - '01234' + builder._getTabWrapper(3).outerHTML + '56' + - LINE_FEED_HTML + - '789'); - }); + assert.equal(buttons.length, 3); + assert.equal(dom(buttons[0]).textContent, '+10 above'); + assert.equal(dom(buttons[1]).textContent, 'Show 11 common lines'); + assert.equal(dom(buttons[2]).textContent, '+10 below'); + }); - test('newlines 4', () => { - const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'; - assert.equal(builder._formatText(text, 4, 20).innerHTML, - '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' + - LINE_FEED_HTML + - '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' + - LINE_FEED_HTML + - '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'); - }); + test('newlines 1', () => { + let text = 'abcdef'; - test('line_length ignored if line_wrapping is true', () => { - builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50}; - const text = 'a'.repeat(51); + assert.equal(builder._formatText(text, 4, 10).innerHTML, text); + text = 'a'.repeat(20); + assert.equal(builder._formatText(text, 4, 10).innerHTML, + 'a'.repeat(10) + + LINE_FEED_HTML + + 'a'.repeat(10)); + }); - const line = {text, highlights: []}; - const result = builder._createTextEl(undefined, line).firstChild.innerHTML; - assert.equal(result, text); - }); + test('newlines 2', () => { + const text = '<span class="thumbsup">👍</span>'; + assert.equal(builder._formatText(text, 4, 10).innerHTML, + '<span clas' + + LINE_FEED_HTML + + 's="thumbsu' + + LINE_FEED_HTML + + 'p">👍</span' + + LINE_FEED_HTML + + '>'); + }); - test('line_length applied if line_wrapping is false', () => { - builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50}; - const text = 'a'.repeat(51); + test('newlines 3', () => { + const text = '01234\t56789'; + assert.equal(builder._formatText(text, 4, 10).innerHTML, + '01234' + builder._getTabWrapper(3).outerHTML + '56' + + LINE_FEED_HTML + + '789'); + }); - const line = {text, highlights: []}; - const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a'; - const result = builder._createTextEl(undefined, line).firstChild.innerHTML; - assert.equal(result, expected); - }); + test('newlines 4', () => { + const text = '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'; + assert.equal(builder._formatText(text, 4, 20).innerHTML, + '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' + + LINE_FEED_HTML + + '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍' + + LINE_FEED_HTML + + '👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍'); + }); - [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE] - .forEach(mode => { - test(`line_length used for regular files under ${mode}`, () => { - element.path = '/a.txt'; - element.viewMode = mode; - builder = element._getDiffBuilder( - {}, {tab_size: 4, line_length: 50} - ); - assert.equal(builder._prefs.line_length, 50); - }); + test('line_length ignored if line_wrapping is true', () => { + builder._prefs = {line_wrapping: true, tab_size: 4, line_length: 50}; + const text = 'a'.repeat(51); - test(`line_length ignored for commit msg under ${mode}`, () => { - element.path = '/COMMIT_MSG'; - element.viewMode = mode; - builder = element._getDiffBuilder( - {}, {tab_size: 4, line_length: 50} - ); - assert.equal(builder._prefs.line_length, 72); - }); + const line = {text, highlights: []}; + const result = builder._createTextEl(undefined, line).firstChild.innerHTML; + assert.equal(result, text); + }); + + test('line_length applied if line_wrapping is false', () => { + builder._prefs = {line_wrapping: false, tab_size: 4, line_length: 50}; + const text = 'a'.repeat(51); + + const line = {text, highlights: []}; + const expected = 'a'.repeat(50) + LINE_FEED_HTML + 'a'; + const result = builder._createTextEl(undefined, line).firstChild.innerHTML; + assert.equal(result, expected); + }); + + [DiffViewMode.UNIFIED, DiffViewMode.SIDE_BY_SIDE] + .forEach(mode => { + test(`line_length used for regular files under ${mode}`, () => { + element.path = '/a.txt'; + element.viewMode = mode; + builder = element._getDiffBuilder( + {}, {tab_size: 4, line_length: 50} + ); + assert.equal(builder._prefs.line_length, 50); }); - test('_createTextEl linewrap with tabs', () => { - const text = '\t'.repeat(7) + '!'; - const line = {text, highlights: []}; - const el = builder._createTextEl(undefined, line); - assert.equal(el.innerText, text); - // With line length 10 and tab size 2, there should be a line break - // after every two tabs. - const newlineEl = el.querySelector('.contentText > .br'); - assert.isOk(newlineEl); - assert.equal( - el.querySelector('.contentText .tab:nth-child(2)').nextSibling, - newlineEl); - }); + test(`line_length ignored for commit msg under ${mode}`, () => { + element.path = '/COMMIT_MSG'; + element.viewMode = mode; + builder = element._getDiffBuilder( + {}, {tab_size: 4, line_length: 50} + ); + assert.equal(builder._prefs.line_length, 72); + }); + }); - test('text length with tabs and unicode', () => { - function expectTextLength(text, tabSize, expected) { - // Formatting to |expected| columns should not introduce line breaks. - const result = builder._formatText(text, tabSize, expected); - assert.isNotOk(result.querySelector('.contentText > .br'), + test('_createTextEl linewrap with tabs', () => { + const text = '\t'.repeat(7) + '!'; + const line = {text, highlights: []}; + const el = builder._createTextEl(undefined, line); + assert.equal(el.innerText, text); + // With line length 10 and tab size 2, there should be a line break + // after every two tabs. + const newlineEl = el.querySelector('.contentText > .br'); + assert.isOk(newlineEl); + assert.equal( + el.querySelector('.contentText .tab:nth-child(2)').nextSibling, + newlineEl); + }); + + test('text length with tabs and unicode', () => { + function expectTextLength(text, tabSize, expected) { + // Formatting to |expected| columns should not introduce line breaks. + const result = builder._formatText(text, tabSize, expected); + assert.isNotOk(result.querySelector('.contentText > .br'), + ` Expected the result of: \n` + + ` _formatText(${text}', ${tabSize}, ${expected})\n` + + ` to not contain a br. But the actual result HTML was:\n` + + ` '${result.innerHTML}'\nwhereupon`); + + // Increasing the line limit should produce the same markup. + assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML, + result.innerHTML); + assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML, + result.innerHTML); + + // Decreasing the line limit should introduce line breaks. + if (expected > 0) { + const tooSmall = builder._formatText(text, tabSize, expected - 1); + assert.isOk(tooSmall.querySelector('.contentText > .br'), ` Expected the result of: \n` + - ` _formatText(${text}', ${tabSize}, ${expected})\n` + - ` to not contain a br. But the actual result HTML was:\n` + - ` '${result.innerHTML}'\nwhereupon`); - - // Increasing the line limit should produce the same markup. - assert.equal(builder._formatText(text, tabSize, Infinity).innerHTML, - result.innerHTML); - assert.equal(builder._formatText(text, tabSize, expected + 1).innerHTML, - result.innerHTML); - - // Decreasing the line limit should introduce line breaks. - if (expected > 0) { - const tooSmall = builder._formatText(text, tabSize, expected - 1); - assert.isOk(tooSmall.querySelector('.contentText > .br'), - ` Expected the result of: \n` + - ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` + - ` to contain a br. But the actual result HTML was:\n` + - ` '${tooSmall.innerHTML}'\nwhereupon`); - } + ` _formatText(${text}', ${tabSize}, ${expected - 1})\n` + + ` to contain a br. But the actual result HTML was:\n` + + ` '${tooSmall.innerHTML}'\nwhereupon`); } - expectTextLength('12345', 4, 5); - expectTextLength('\t\t12', 4, 10); - expectTextLength('abc💢123', 4, 7); - expectTextLength('abc\t', 8, 8); - expectTextLength('abc\t\t', 10, 20); - expectTextLength('', 10, 0); - expectTextLength('', 10, 0); - // 17 Thai combining chars. - expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17); - expectTextLength('abc\tde', 10, 12); - expectTextLength('abc\tde\t', 10, 20); - expectTextLength('\t\t\t\t\t', 20, 100); - }); + } + expectTextLength('12345', 4, 5); + expectTextLength('\t\t12', 4, 10); + expectTextLength('abc💢123', 4, 7); + expectTextLength('abc\t', 8, 8); + expectTextLength('abc\t\t', 10, 20); + expectTextLength('', 10, 0); + expectTextLength('', 10, 0); + // 17 Thai combining chars. + expectTextLength('ก้้้้้้้้้้้้้้้้', 4, 17); + expectTextLength('abc\tde', 10, 12); + expectTextLength('abc\tde\t', 10, 20); + expectTextLength('\t\t\t\t\t', 20, 100); + }); - test('tab wrapper insertion', () => { - const html = 'abc\tdef'; - const tabSize = builder._prefs.tab_size; - const wrapper = builder._getTabWrapper(tabSize - 3); - assert.ok(wrapper); - assert.equal(wrapper.innerText, '\t'); - assert.equal( - builder._formatText(html, tabSize, Infinity).innerHTML, - 'abc' + wrapper.outerHTML + 'def'); - }); + test('tab wrapper insertion', () => { + const html = 'abc\tdef'; + const tabSize = builder._prefs.tab_size; + const wrapper = builder._getTabWrapper(tabSize - 3); + assert.ok(wrapper); + assert.equal(wrapper.innerText, '\t'); + assert.equal( + builder._formatText(html, tabSize, Infinity).innerHTML, + 'abc' + wrapper.outerHTML + 'def'); + }); - test('tab wrapper style', () => { - const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' + - 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$'); + test('tab wrapper style', () => { + const pattern = new RegExp('^<span class="style-scope gr-diff tab" ' + + 'style="(?:-moz-)?tab-size: (\\d+);">\\t<\\/span>$'); - for (const size of [1, 3, 8, 55]) { - const html = builder._getTabWrapper(size).outerHTML; - expect(html).to.match(pattern); - assert.equal(html.match(pattern)[1], size); + for (const size of [1, 3, 8, 55]) { + const html = builder._getTabWrapper(size).outerHTML; + expect(html).to.match(pattern); + assert.equal(html.match(pattern)[1], size); + } + }); + + test('_handlePreferenceError called with invalid preference', () => { + sandbox.stub(element, '_handlePreferenceError'); + const prefs = {tab_size: 0}; + element._getDiffBuilder(element.diff, prefs); + assert.isTrue(element._handlePreferenceError.lastCall + .calledWithExactly('tab size')); + }); + + test('_handlePreferenceError triggers alert and javascript error', () => { + const errorStub = sinon.stub(); + element.addEventListener('show-alert', errorStub); + assert.throws(element._handlePreferenceError.bind(element, 'tab size')); + assert.equal(errorStub.lastCall.args[0].detail.message, + `The value of the 'tab size' user preference is invalid. ` + + `Fix in diff preferences`); + }); + + suite('_isTotal', () => { + test('is total for add', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (let idx = 0; idx < 10; idx++) { + group.addLine(new GrDiffLine(GrDiffLine.Type.ADD)); } + assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); }); - test('_handlePreferenceError called with invalid preference', () => { - sandbox.stub(element, '_handlePreferenceError'); - const prefs = {tab_size: 0}; - element._getDiffBuilder(element.diff, prefs); - assert.isTrue(element._handlePreferenceError.lastCall - .calledWithExactly('tab size')); - }); - - test('_handlePreferenceError triggers alert and javascript error', () => { - const errorStub = sinon.stub(); - element.addEventListener('show-alert', errorStub); - assert.throws(element._handlePreferenceError.bind(element, 'tab size')); - assert.equal(errorStub.lastCall.args[0].detail.message, - `The value of the 'tab size' user preference is invalid. ` + - `Fix in diff preferences`); - }); - - suite('_isTotal', () => { - test('is total for add', () => { - const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - for (let idx = 0; idx < 10; idx++) { - group.addLine(new GrDiffLine(GrDiffLine.Type.ADD)); - } - assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); - }); - - test('is total for remove', () => { - const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - for (let idx = 0; idx < 10; idx++) { - group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE)); - } - assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); - }); - - test('not total for empty', () => { - const group = new GrDiffGroup(GrDiffGroup.Type.BOTH); - assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); - }); - - test('not total for non-delta', () => { - const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - for (let idx = 0; idx < 10; idx++) { - group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH)); - } - assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); - }); - }); - - suite('intraline differences', () => { - let el; - let str; - let annotateElementSpy; - let layer; - const lineNumberEl = document.createElement('td'); - - function slice(str, start, end) { - return Array.from(str).slice(start, end) - .join(''); + test('is total for remove', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (let idx = 0; idx < 10; idx++) { + group.addLine(new GrDiffLine(GrDiffLine.Type.REMOVE)); } - - setup(() => { - el = fixture('div-with-text'); - str = el.textContent; - annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - layer = document.createElement('gr-diff-builder') - ._createIntralineLayer(); - }); - - test('annotate no highlights', () => { - const line = { - text: str, - highlights: [], - }; - - layer.annotate(el, lineNumberEl, line); - - // The content is unchanged. - assert.isFalse(annotateElementSpy.called); - assert.equal(el.childNodes.length, 1); - assert.instanceOf(el.childNodes[0], Text); - assert.equal(str, el.childNodes[0].textContent); - }); - - test('annotate with highlights', () => { - const line = { - text: str, - highlights: [ - {startIndex: 6, endIndex: 12}, - {startIndex: 18, endIndex: 22}, - ], - }; - const str0 = slice(str, 0, 6); - const str1 = slice(str, 6, 12); - const str2 = slice(str, 12, 18); - const str3 = slice(str, 18, 22); - const str4 = slice(str, 22); - - layer.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementSpy.called); - assert.equal(el.childNodes.length, 5); - - assert.instanceOf(el.childNodes[0], Text); - assert.equal(el.childNodes[0].textContent, str0); - - assert.notInstanceOf(el.childNodes[1], Text); - assert.equal(el.childNodes[1].textContent, str1); - - assert.instanceOf(el.childNodes[2], Text); - assert.equal(el.childNodes[2].textContent, str2); - - assert.notInstanceOf(el.childNodes[3], Text); - assert.equal(el.childNodes[3].textContent, str3); - - assert.instanceOf(el.childNodes[4], Text); - assert.equal(el.childNodes[4].textContent, str4); - }); - - test('annotate without endIndex', () => { - const line = { - text: str, - highlights: [ - {startIndex: 28}, - ], - }; - - const str0 = slice(str, 0, 28); - const str1 = slice(str, 28); - - layer.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementSpy.called); - assert.equal(el.childNodes.length, 2); - - assert.instanceOf(el.childNodes[0], Text); - assert.equal(el.childNodes[0].textContent, str0); - - assert.notInstanceOf(el.childNodes[1], Text); - assert.equal(el.childNodes[1].textContent, str1); - }); - - test('annotate ignores empty highlights', () => { - const line = { - text: str, - highlights: [ - {startIndex: 28, endIndex: 28}, - ], - }; - - layer.annotate(el, lineNumberEl, line); - - assert.isFalse(annotateElementSpy.called); - assert.equal(el.childNodes.length, 1); - }); - - test('annotate handles unicode', () => { - // Put some unicode into the string: - str = str.replace(/\s/g, '💢'); - el.textContent = str; - const line = { - text: str, - highlights: [ - {startIndex: 6, endIndex: 12}, - ], - }; - - const str0 = slice(str, 0, 6); - const str1 = slice(str, 6, 12); - const str2 = slice(str, 12); - - layer.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementSpy.called); - assert.equal(el.childNodes.length, 3); - - assert.instanceOf(el.childNodes[0], Text); - assert.equal(el.childNodes[0].textContent, str0); - - assert.notInstanceOf(el.childNodes[1], Text); - assert.equal(el.childNodes[1].textContent, str1); - - assert.instanceOf(el.childNodes[2], Text); - assert.equal(el.childNodes[2].textContent, str2); - }); - - test('annotate handles unicode w/o endIndex', () => { - // Put some unicode into the string: - str = str.replace(/\s/g, '💢'); - el.textContent = str; - - const line = { - text: str, - highlights: [ - {startIndex: 6}, - ], - }; - - const str0 = slice(str, 0, 6); - const str1 = slice(str, 6); - - layer.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementSpy.called); - assert.equal(el.childNodes.length, 2); - - assert.instanceOf(el.childNodes[0], Text); - assert.equal(el.childNodes[0].textContent, str0); - - assert.notInstanceOf(el.childNodes[1], Text); - assert.equal(el.childNodes[1].textContent, str1); - }); + assert.isTrue(GrDiffBuilder.prototype._isTotal(group)); }); - suite('tab indicators', () => { - let element; - let layer; - const lineNumberEl = document.createElement('td'); - - setup(() => { - element = fixture('basic'); - element._showTabs = true; - layer = element._createTabIndicatorLayer(); - }); - - test('does nothing with empty line', () => { - const line = {text: ''}; - const el = document.createElement('div'); - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - - layer.annotate(el, lineNumberEl, line); - - assert.isFalse(annotateElementStub.called); - }); - - test('does nothing with no tabs', () => { - const str = 'lorem ipsum no tabs'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - - layer.annotate(el, lineNumberEl, line); - - assert.isFalse(annotateElementStub.called); - }); - - test('annotates tab at beginning', () => { - const str = '\tlorem upsum'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - - layer.annotate(el, lineNumberEl, line); - - assert.equal(annotateElementStub.callCount, 1); - const args = annotateElementStub.getCalls()[0].args; - assert.equal(args[0], el); - assert.equal(args[1], 0, 'offset of tab indicator'); - assert.equal(args[2], 1, 'length of tab indicator'); - assert.include(args[3], 'tab-indicator'); - }); - - test('does not annotate when disabled', () => { - element._showTabs = false; - - const str = '\tlorem upsum'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - - layer.annotate(el, lineNumberEl, line); - - assert.isFalse(annotateElementStub.called); - }); - - test('annotates multiple in beginning', () => { - const str = '\t\tlorem upsum'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - - layer.annotate(el, lineNumberEl, line); - - assert.equal(annotateElementStub.callCount, 2); - - let args = annotateElementStub.getCalls()[0].args; - assert.equal(args[0], el); - assert.equal(args[1], 0, 'offset of tab indicator'); - assert.equal(args[2], 1, 'length of tab indicator'); - assert.include(args[3], 'tab-indicator'); - - args = annotateElementStub.getCalls()[1].args; - assert.equal(args[0], el); - assert.equal(args[1], 1, 'offset of tab indicator'); - assert.equal(args[2], 1, 'length of tab indicator'); - assert.include(args[3], 'tab-indicator'); - }); - - test('annotates intermediate tabs', () => { - const str = 'lorem\tupsum'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - - layer.annotate(el, lineNumberEl, line); - - assert.equal(annotateElementStub.callCount, 1); - const args = annotateElementStub.getCalls()[0].args; - assert.equal(args[0], el); - assert.equal(args[1], 5, 'offset of tab indicator'); - assert.equal(args[2], 1, 'length of tab indicator'); - assert.include(args[3], 'tab-indicator'); - }); + test('not total for empty', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.BOTH); + assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); }); - suite('layers', () => { - let element; - let initialLayersCount; - let withLayerCount; + test('not total for non-delta', () => { + const group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + for (let idx = 0; idx < 10; idx++) { + group.addLine(new GrDiffLine(GrDiffLine.Type.BOTH)); + } + assert.isFalse(GrDiffBuilder.prototype._isTotal(group)); + }); + }); + + suite('intraline differences', () => { + let el; + let str; + let annotateElementSpy; + let layer; + const lineNumberEl = document.createElement('td'); + + function slice(str, start, end) { + return Array.from(str).slice(start, end) + .join(''); + } + + setup(() => { + el = fixture('div-with-text'); + str = el.textContent; + annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + layer = document.createElement('gr-diff-builder') + ._createIntralineLayer(); + }); + + test('annotate no highlights', () => { + const line = { + text: str, + highlights: [], + }; + + layer.annotate(el, lineNumberEl, line); + + // The content is unchanged. + assert.isFalse(annotateElementSpy.called); + assert.equal(el.childNodes.length, 1); + assert.instanceOf(el.childNodes[0], Text); + assert.equal(str, el.childNodes[0].textContent); + }); + + test('annotate with highlights', () => { + const line = { + text: str, + highlights: [ + {startIndex: 6, endIndex: 12}, + {startIndex: 18, endIndex: 22}, + ], + }; + const str0 = slice(str, 0, 6); + const str1 = slice(str, 6, 12); + const str2 = slice(str, 12, 18); + const str3 = slice(str, 18, 22); + const str4 = slice(str, 22); + + layer.annotate(el, lineNumberEl, line); + + assert.isTrue(annotateElementSpy.called); + assert.equal(el.childNodes.length, 5); + + assert.instanceOf(el.childNodes[0], Text); + assert.equal(el.childNodes[0].textContent, str0); + + assert.notInstanceOf(el.childNodes[1], Text); + assert.equal(el.childNodes[1].textContent, str1); + + assert.instanceOf(el.childNodes[2], Text); + assert.equal(el.childNodes[2].textContent, str2); + + assert.notInstanceOf(el.childNodes[3], Text); + assert.equal(el.childNodes[3].textContent, str3); + + assert.instanceOf(el.childNodes[4], Text); + assert.equal(el.childNodes[4].textContent, str4); + }); + + test('annotate without endIndex', () => { + const line = { + text: str, + highlights: [ + {startIndex: 28}, + ], + }; + + const str0 = slice(str, 0, 28); + const str1 = slice(str, 28); + + layer.annotate(el, lineNumberEl, line); + + assert.isTrue(annotateElementSpy.called); + assert.equal(el.childNodes.length, 2); + + assert.instanceOf(el.childNodes[0], Text); + assert.equal(el.childNodes[0].textContent, str0); + + assert.notInstanceOf(el.childNodes[1], Text); + assert.equal(el.childNodes[1].textContent, str1); + }); + + test('annotate ignores empty highlights', () => { + const line = { + text: str, + highlights: [ + {startIndex: 28, endIndex: 28}, + ], + }; + + layer.annotate(el, lineNumberEl, line); + + assert.isFalse(annotateElementSpy.called); + assert.equal(el.childNodes.length, 1); + }); + + test('annotate handles unicode', () => { + // Put some unicode into the string: + str = str.replace(/\s/g, '💢'); + el.textContent = str; + const line = { + text: str, + highlights: [ + {startIndex: 6, endIndex: 12}, + ], + }; + + const str0 = slice(str, 0, 6); + const str1 = slice(str, 6, 12); + const str2 = slice(str, 12); + + layer.annotate(el, lineNumberEl, line); + + assert.isTrue(annotateElementSpy.called); + assert.equal(el.childNodes.length, 3); + + assert.instanceOf(el.childNodes[0], Text); + assert.equal(el.childNodes[0].textContent, str0); + + assert.notInstanceOf(el.childNodes[1], Text); + assert.equal(el.childNodes[1].textContent, str1); + + assert.instanceOf(el.childNodes[2], Text); + assert.equal(el.childNodes[2].textContent, str2); + }); + + test('annotate handles unicode w/o endIndex', () => { + // Put some unicode into the string: + str = str.replace(/\s/g, '💢'); + el.textContent = str; + + const line = { + text: str, + highlights: [ + {startIndex: 6}, + ], + }; + + const str0 = slice(str, 0, 6); + const str1 = slice(str, 6); + + layer.annotate(el, lineNumberEl, line); + + assert.isTrue(annotateElementSpy.called); + assert.equal(el.childNodes.length, 2); + + assert.instanceOf(el.childNodes[0], Text); + assert.equal(el.childNodes[0].textContent, str0); + + assert.notInstanceOf(el.childNodes[1], Text); + assert.equal(el.childNodes[1].textContent, str1); + }); + }); + + suite('tab indicators', () => { + let element; + let layer; + const lineNumberEl = document.createElement('td'); + + setup(() => { + element = fixture('basic'); + element._showTabs = true; + layer = element._createTabIndicatorLayer(); + }); + + test('does nothing with empty line', () => { + const line = {text: ''}; + const el = document.createElement('div'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, lineNumberEl, line); + + assert.isFalse(annotateElementStub.called); + }); + + test('does nothing with no tabs', () => { + const str = 'lorem ipsum no tabs'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, lineNumberEl, line); + + assert.isFalse(annotateElementStub.called); + }); + + test('annotates tab at beginning', () => { + const str = '\tlorem upsum'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, lineNumberEl, line); + + assert.equal(annotateElementStub.callCount, 1); + const args = annotateElementStub.getCalls()[0].args; + assert.equal(args[0], el); + assert.equal(args[1], 0, 'offset of tab indicator'); + assert.equal(args[2], 1, 'length of tab indicator'); + assert.include(args[3], 'tab-indicator'); + }); + + test('does not annotate when disabled', () => { + element._showTabs = false; + + const str = '\tlorem upsum'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, lineNumberEl, line); + + assert.isFalse(annotateElementStub.called); + }); + + test('annotates multiple in beginning', () => { + const str = '\t\tlorem upsum'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, lineNumberEl, line); + + assert.equal(annotateElementStub.callCount, 2); + + let args = annotateElementStub.getCalls()[0].args; + assert.equal(args[0], el); + assert.equal(args[1], 0, 'offset of tab indicator'); + assert.equal(args[2], 1, 'length of tab indicator'); + assert.include(args[3], 'tab-indicator'); + + args = annotateElementStub.getCalls()[1].args; + assert.equal(args[0], el); + assert.equal(args[1], 1, 'offset of tab indicator'); + assert.equal(args[2], 1, 'length of tab indicator'); + assert.include(args[3], 'tab-indicator'); + }); + + test('annotates intermediate tabs', () => { + const str = 'lorem\tupsum'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + + layer.annotate(el, lineNumberEl, line); + + assert.equal(annotateElementStub.callCount, 1); + const args = annotateElementStub.getCalls()[0].args; + assert.equal(args[0], el); + assert.equal(args[1], 5, 'offset of tab indicator'); + assert.equal(args[2], 1, 'length of tab indicator'); + assert.include(args[3], 'tab-indicator'); + }); + }); + + suite('layers', () => { + let element; + let initialLayersCount; + let withLayerCount; + setup(() => { + const layers = []; + element = fixture('basic'); + element.layers = layers; + element._showTrailingWhitespace = true; + element._setupAnnotationLayers(); + initialLayersCount = element._layers.length; + }); + + test('no layers', () => { + element._setupAnnotationLayers(); + assert.equal(element._layers.length, initialLayersCount); + }); + + suite('with layers', () => { + const layers = [{}, {}]; setup(() => { - const layers = []; element = fixture('basic'); element.layers = layers; element._showTrailingWhitespace = true; element._setupAnnotationLayers(); - initialLayersCount = element._layers.length; + withLayerCount = element._layers.length; }); - - test('no layers', () => { + test('with layers', () => { element._setupAnnotationLayers(); - assert.equal(element._layers.length, initialLayersCount); + assert.equal(element._layers.length, withLayerCount); + assert.equal(initialLayersCount + layers.length, + withLayerCount); }); + }); + }); - suite('with layers', () => { - const layers = [{}, {}]; - setup(() => { - element = fixture('basic'); - element.layers = layers; - element._showTrailingWhitespace = true; - element._setupAnnotationLayers(); - withLayerCount = element._layers.length; - }); - test('with layers', () => { - element._setupAnnotationLayers(); - assert.equal(element._layers.length, withLayerCount); - assert.equal(initialLayersCount + layers.length, - withLayerCount); - }); + suite('trailing whitespace', () => { + let element; + let layer; + const lineNumberEl = document.createElement('td'); + + setup(() => { + element = fixture('basic'); + element._showTrailingWhitespace = true; + layer = element._createTrailingWhitespaceLayer(); + }); + + test('does nothing with empty line', () => { + const line = {text: ''}; + const el = document.createElement('div'); + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isFalse(annotateElementStub.called); + }); + + test('does nothing with no trailing whitespace', () => { + const str = 'lorem ipsum blah blah'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isFalse(annotateElementStub.called); + }); + + test('annotates trailing spaces', () => { + const str = 'lorem ipsum '; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 11); + assert.equal(annotateElementStub.lastCall.args[2], 3); + }); + + test('annotates trailing tabs', () => { + const str = 'lorem ipsum\t\t\t'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 11); + assert.equal(annotateElementStub.lastCall.args[2], 3); + }); + + test('annotates mixed trailing whitespace', () => { + const str = 'lorem ipsum\t \t'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 11); + assert.equal(annotateElementStub.lastCall.args[2], 3); + }); + + test('unicode preceding trailing whitespace', () => { + const str = '💢\t'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isTrue(annotateElementStub.called); + assert.equal(annotateElementStub.lastCall.args[1], 1); + assert.equal(annotateElementStub.lastCall.args[2], 1); + }); + + test('does not annotate when disabled', () => { + element._showTrailingWhitespace = false; + const str = 'lorem upsum\t \t '; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const annotateElementStub = + sandbox.stub(GrAnnotation, 'annotateElement'); + layer.annotate(el, lineNumberEl, line); + assert.isFalse(annotateElementStub.called); + }); + }); + + suite('rendering text, images and binary files', () => { + let processStub; + let keyLocations; + let prefs; + let content; + + setup(() => { + element = fixture('basic'); + element.viewMode = 'SIDE_BY_SIDE'; + processStub = sandbox.stub(element.$.processor, 'process') + .returns(Promise.resolve()); + keyLocations = {left: {}, right: {}}; + prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + syntax_highlighting: true, + }; + content = [{ + a: ['all work and no play make andybons a dull boy'], + b: ['elgoog elgoog elgoog'], + }, { + ab: [ + 'Non eram nescius, Brute, cum, quae summis ingeniis ', + 'exquisitaque doctrina philosophi Graeco sermone tractavissent', + ], + }]; + }); + + test('text', () => { + element.diff = {content}; + return element.render(keyLocations, prefs).then(() => { + assert.isTrue(processStub.calledOnce); + assert.isFalse(processStub.lastCall.args[1]); }); }); - suite('trailing whitespace', () => { - let element; - let layer; - const lineNumberEl = document.createElement('td'); - - setup(() => { - element = fixture('basic'); - element._showTrailingWhitespace = true; - layer = element._createTrailingWhitespaceLayer(); - }); - - test('does nothing with empty line', () => { - const line = {text: ''}; - const el = document.createElement('div'); - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isFalse(annotateElementStub.called); - }); - - test('does nothing with no trailing whitespace', () => { - const str = 'lorem ipsum blah blah'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isFalse(annotateElementStub.called); - }); - - test('annotates trailing spaces', () => { - const str = 'lorem ipsum '; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isTrue(annotateElementStub.called); - assert.equal(annotateElementStub.lastCall.args[1], 11); - assert.equal(annotateElementStub.lastCall.args[2], 3); - }); - - test('annotates trailing tabs', () => { - const str = 'lorem ipsum\t\t\t'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isTrue(annotateElementStub.called); - assert.equal(annotateElementStub.lastCall.args[1], 11); - assert.equal(annotateElementStub.lastCall.args[2], 3); - }); - - test('annotates mixed trailing whitespace', () => { - const str = 'lorem ipsum\t \t'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isTrue(annotateElementStub.called); - assert.equal(annotateElementStub.lastCall.args[1], 11); - assert.equal(annotateElementStub.lastCall.args[2], 3); - }); - - test('unicode preceding trailing whitespace', () => { - const str = '💢\t'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isTrue(annotateElementStub.called); - assert.equal(annotateElementStub.lastCall.args[1], 1); - assert.equal(annotateElementStub.lastCall.args[2], 1); - }); - - test('does not annotate when disabled', () => { - element._showTrailingWhitespace = false; - const str = 'lorem upsum\t \t '; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const annotateElementStub = - sandbox.stub(GrAnnotation, 'annotateElement'); - layer.annotate(el, lineNumberEl, line); - assert.isFalse(annotateElementStub.called); + test('image', () => { + element.diff = {content, binary: true}; + element.isImageDiff = true; + return element.render(keyLocations, prefs).then(() => { + assert.isTrue(processStub.calledOnce); + assert.isTrue(processStub.lastCall.args[1]); }); }); - suite('rendering text, images and binary files', () => { - let processStub; - let keyLocations; - let prefs; - let content; + test('binary', () => { + element.diff = {content, binary: true}; + return element.render(keyLocations, prefs).then(() => { + assert.isTrue(processStub.calledOnce); + assert.isTrue(processStub.lastCall.args[1]); + }); + }); + }); - setup(() => { - element = fixture('basic'); - element.viewMode = 'SIDE_BY_SIDE'; - processStub = sandbox.stub(element.$.processor, 'process') - .returns(Promise.resolve()); - keyLocations = {left: {}, right: {}}; - prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - context: -1, - syntax_highlighting: true, - }; - content = [{ + suite('rendering', () => { + let content; + let outputEl; + let keyLocations; + + setup(done => { + const prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + syntax_highlighting: true, + }; + content = [ + { a: ['all work and no play make andybons a dull boy'], b: ['elgoog elgoog elgoog'], - }, { + }, + { ab: [ 'Non eram nescius, Brute, cum, quae summis ingeniis ', 'exquisitaque doctrina philosophi Graeco sermone tractavissent', ], - }]; + }, + ]; + element = fixture('basic'); + outputEl = element.queryEffectiveChildren('#diffTable'); + keyLocations = {left: {}, right: {}}; + sandbox.stub(element, '_getDiffBuilder', () => { + const builder = new GrDiffBuilder({content}, prefs, outputEl); + sandbox.stub(builder, 'addColumns'); + builder.buildSectionElement = function(group) { + const section = document.createElement('stub'); + section.textContent = group.lines + .reduce((acc, line) => acc + line.text, ''); + return section; + }; + return builder; }); + element.diff = {content}; + element.render(keyLocations, prefs).then(done); + }); - test('text', () => { - element.diff = {content}; - return element.render(keyLocations, prefs).then(() => { - assert.isTrue(processStub.calledOnce); - assert.isFalse(processStub.lastCall.args[1]); - }); - }); + test('addColumns is called', done => { + element.render(keyLocations, {}).then(done); + assert.isTrue(element._builder.addColumns.called); + }); - test('image', () => { - element.diff = {content, binary: true}; - element.isImageDiff = true; - return element.render(keyLocations, prefs).then(() => { - assert.isTrue(processStub.calledOnce); - assert.isTrue(processStub.lastCall.args[1]); - }); - }); + test('getSectionsByLineRange one line', () => { + const section = outputEl.querySelector('stub:nth-of-type(2)'); + const sections = element._builder.getSectionsByLineRange(1, 1, 'left'); + assert.equal(sections.length, 1); + assert.strictEqual(sections[0], section); + }); - test('binary', () => { - element.diff = {content, binary: true}; - return element.render(keyLocations, prefs).then(() => { - assert.isTrue(processStub.calledOnce); - assert.isTrue(processStub.lastCall.args[1]); - }); + test('getSectionsByLineRange over diff', () => { + const section = [ + outputEl.querySelector('stub:nth-of-type(2)'), + outputEl.querySelector('stub:nth-of-type(3)'), + ]; + const sections = element._builder.getSectionsByLineRange(1, 2, 'left'); + assert.equal(sections.length, 2); + assert.strictEqual(sections[0], section[0]); + assert.strictEqual(sections[1], section[1]); + }); + + test('render-start and render-content are fired', done => { + const dispatchEventStub = sandbox.stub(element, 'dispatchEvent'); + element.render(keyLocations, {}).then(() => { + const firedEventTypes = dispatchEventStub.getCalls() + .map(c => c.args[0].type); + assert.include(firedEventTypes, 'render-start'); + assert.include(firedEventTypes, 'render-content'); + done(); }); }); - suite('rendering', () => { - let content; - let outputEl; - let keyLocations; + test('cancel', () => { + const processorCancelStub = sandbox.stub(element.$.processor, 'cancel'); + element.cancel(); + assert.isTrue(processorCancelStub.called); + }); + }); - setup(done => { - const prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - context: -1, - syntax_highlighting: true, - }; - content = [ - { - a: ['all work and no play make andybons a dull boy'], - b: ['elgoog elgoog elgoog'], - }, - { - ab: [ - 'Non eram nescius, Brute, cum, quae summis ingeniis ', - 'exquisitaque doctrina philosophi Graeco sermone tractavissent', - ], - }, - ]; - element = fixture('basic'); - outputEl = element.queryEffectiveChildren('#diffTable'); - keyLocations = {left: {}, right: {}}; - sandbox.stub(element, '_getDiffBuilder', () => { - const builder = new GrDiffBuilder({content}, prefs, outputEl); - sandbox.stub(builder, 'addColumns'); - builder.buildSectionElement = function(group) { - const section = document.createElement('stub'); - section.textContent = group.lines - .reduce((acc, line) => acc + line.text, ''); - return section; - }; - return builder; - }); - element.diff = {content}; - element.render(keyLocations, prefs).then(done); - }); + suite('mock-diff', () => { + let element; + let builder; + let diff; + let prefs; + let keyLocations; - test('addColumns is called', done => { - element.render(keyLocations, {}).then(done); - assert.isTrue(element._builder.addColumns.called); - }); + setup(done => { + element = fixture('mock-diff'); + diff = document.createElement('mock-diff-response').diffResponse; + element.diff = diff; - test('getSectionsByLineRange one line', () => { - const section = outputEl.querySelector('stub:nth-of-type(2)'); - const sections = element._builder.getSectionsByLineRange(1, 1, 'left'); - assert.equal(sections.length, 1); - assert.strictEqual(sections[0], section); - }); + prefs = { + line_length: 80, + show_tabs: true, + tab_size: 4, + }; + keyLocations = {left: {}, right: {}}; - test('getSectionsByLineRange over diff', () => { - const section = [ - outputEl.querySelector('stub:nth-of-type(2)'), - outputEl.querySelector('stub:nth-of-type(3)'), - ]; - const sections = element._builder.getSectionsByLineRange(1, 2, 'left'); - assert.equal(sections.length, 2); - assert.strictEqual(sections[0], section[0]); - assert.strictEqual(sections[1], section[1]); - }); - - test('render-start and render-content are fired', done => { - const dispatchEventStub = sandbox.stub(element, 'dispatchEvent'); - element.render(keyLocations, {}).then(() => { - const firedEventTypes = dispatchEventStub.getCalls() - .map(c => c.args[0].type); - assert.include(firedEventTypes, 'render-start'); - assert.include(firedEventTypes, 'render-content'); - done(); - }); - }); - - test('cancel', () => { - const processorCancelStub = sandbox.stub(element.$.processor, 'cancel'); - element.cancel(); - assert.isTrue(processorCancelStub.called); + element.render(keyLocations, prefs).then(() => { + builder = element._builder; + done(); }); }); - suite('mock-diff', () => { - let element; - let builder; - let diff; - let prefs; - let keyLocations; + test('getContentByLine', () => { + let actual; - setup(done => { - element = fixture('mock-diff'); - diff = document.createElement('mock-diff-response').diffResponse; - element.diff = diff; + actual = builder.getContentByLine(2, 'left'); + assert.equal(actual.textContent, diff.content[0].ab[1]); - prefs = { - line_length: 80, - show_tabs: true, - tab_size: 4, - }; - keyLocations = {left: {}, right: {}}; + actual = builder.getContentByLine(2, 'right'); + assert.equal(actual.textContent, diff.content[0].ab[1]); - element.render(keyLocations, prefs).then(() => { - builder = element._builder; - done(); - }); + actual = builder.getContentByLine(5, 'left'); + assert.equal(actual.textContent, diff.content[2].ab[0]); + + actual = builder.getContentByLine(5, 'right'); + assert.equal(actual.textContent, diff.content[1].b[0]); + }); + + test('findLinesByRange', () => { + const lines = []; + const elems = []; + const start = 6; + const end = 10; + const count = end - start + 1; + + builder.findLinesByRange(start, end, 'right', lines, elems); + + assert.equal(lines.length, count); + assert.equal(elems.length, count); + + for (let i = 0; i < 5; i++) { + assert.instanceOf(lines[i], GrDiffLine); + assert.equal(lines[i].afterNumber, start + i); + assert.instanceOf(elems[i], HTMLElement); + assert.equal(lines[i].text, elems[i].textContent); + } + }); + + test('_renderContentByRange', () => { + const spy = sandbox.spy(builder, '_createTextEl'); + const start = 9; + const end = 14; + const count = end - start + 1; + + builder._renderContentByRange(start, end, 'left'); + + assert.equal(spy.callCount, count); + spy.getCalls().forEach((call, i) => { + assert.equal(call.args[1].beforeNumber, start + i); }); + }); - test('getContentByLine', () => { - let actual; + test('_renderContentByRange notexistent elements', () => { + const spy = sandbox.spy(builder, '_createTextEl'); - actual = builder.getContentByLine(2, 'left'); - assert.equal(actual.textContent, diff.content[0].ab[1]); + sandbox.stub(builder, 'findLinesByRange', + (s, e, d, lines, elements) => { + // Add a line and a corresponding element. + lines.push(new GrDiffLine(GrDiffLine.Type.BOTH)); + const tr = document.createElement('tr'); + const td = document.createElement('td'); + const el = document.createElement('div'); + tr.appendChild(td); + td.appendChild(el); + elements.push(el); - actual = builder.getContentByLine(2, 'right'); - assert.equal(actual.textContent, diff.content[0].ab[1]); + // Add 2 lines without corresponding elements. + lines.push(new GrDiffLine(GrDiffLine.Type.BOTH)); + lines.push(new GrDiffLine(GrDiffLine.Type.BOTH)); + }); - actual = builder.getContentByLine(5, 'left'); - assert.equal(actual.textContent, diff.content[2].ab[0]); + builder._renderContentByRange(1, 10, 'left'); + // Should be called only once because only one line had a corresponding + // element. + assert.equal(spy.callCount, 1); + }); - actual = builder.getContentByLine(5, 'right'); - assert.equal(actual.textContent, diff.content[1].b[0]); - }); + test('_getLineNumberEl side-by-side left', () => { + const contentEl = builder.getContentByLine(5, 'left', + element.$.diffTable); + const lineNumberEl = builder._getLineNumberEl(contentEl, 'left'); + assert.isTrue(lineNumberEl.classList.contains('lineNum')); + assert.isTrue(lineNumberEl.classList.contains('left')); + }); - test('findLinesByRange', () => { - const lines = []; - const elems = []; - const start = 6; - const end = 10; - const count = end - start + 1; + test('_getLineNumberEl side-by-side right', () => { + const contentEl = builder.getContentByLine(5, 'right', + element.$.diffTable); + const lineNumberEl = builder._getLineNumberEl(contentEl, 'right'); + assert.isTrue(lineNumberEl.classList.contains('lineNum')); + assert.isTrue(lineNumberEl.classList.contains('right')); + }); - builder.findLinesByRange(start, end, 'right', lines, elems); + test('_getLineNumberEl unified left', done => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + element.render(keyLocations, prefs).then(() => { + builder = element._builder; - assert.equal(lines.length, count); - assert.equal(elems.length, count); - - for (let i = 0; i < 5; i++) { - assert.instanceOf(lines[i], GrDiffLine); - assert.equal(lines[i].afterNumber, start + i); - assert.instanceOf(elems[i], HTMLElement); - assert.equal(lines[i].text, elems[i].textContent); - } - }); - - test('_renderContentByRange', () => { - const spy = sandbox.spy(builder, '_createTextEl'); - const start = 9; - const end = 14; - const count = end - start + 1; - - builder._renderContentByRange(start, end, 'left'); - - assert.equal(spy.callCount, count); - spy.getCalls().forEach((call, i) => { - assert.equal(call.args[1].beforeNumber, start + i); - }); - }); - - test('_renderContentByRange notexistent elements', () => { - const spy = sandbox.spy(builder, '_createTextEl'); - - sandbox.stub(builder, 'findLinesByRange', - (s, e, d, lines, elements) => { - // Add a line and a corresponding element. - lines.push(new GrDiffLine(GrDiffLine.Type.BOTH)); - const tr = document.createElement('tr'); - const td = document.createElement('td'); - const el = document.createElement('div'); - tr.appendChild(td); - td.appendChild(el); - elements.push(el); - - // Add 2 lines without corresponding elements. - lines.push(new GrDiffLine(GrDiffLine.Type.BOTH)); - lines.push(new GrDiffLine(GrDiffLine.Type.BOTH)); - }); - - builder._renderContentByRange(1, 10, 'left'); - // Should be called only once because only one line had a corresponding - // element. - assert.equal(spy.callCount, 1); - }); - - test('_getLineNumberEl side-by-side left', () => { const contentEl = builder.getContentByLine(5, 'left', element.$.diffTable); const lineNumberEl = builder._getLineNumberEl(contentEl, 'left'); assert.isTrue(lineNumberEl.classList.contains('lineNum')); assert.isTrue(lineNumberEl.classList.contains('left')); + done(); }); + }); - test('_getLineNumberEl side-by-side right', () => { + test('_getLineNumberEl unified right', done => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + element.render(keyLocations, prefs).then(() => { + builder = element._builder; + const contentEl = builder.getContentByLine(5, 'right', element.$.diffTable); const lineNumberEl = builder._getLineNumberEl(contentEl, 'right'); assert.isTrue(lineNumberEl.classList.contains('lineNum')); assert.isTrue(lineNumberEl.classList.contains('right')); + done(); }); + }); - test('_getLineNumberEl unified left', done => { - // Re-render as unified: - element.viewMode = 'UNIFIED_DIFF'; - element.render(keyLocations, prefs).then(() => { - builder = element._builder; + test('_getNextContentOnSide side-by-side left', () => { + const startElem = builder.getContentByLine(5, 'left', + element.$.diffTable); + const expectedStartString = diff.content[2].ab[0]; + const expectedNextString = diff.content[2].ab[1]; + assert.equal(startElem.textContent, expectedStartString); - const contentEl = builder.getContentByLine(5, 'left', - element.$.diffTable); - const lineNumberEl = builder._getLineNumberEl(contentEl, 'left'); - assert.isTrue(lineNumberEl.classList.contains('lineNum')); - assert.isTrue(lineNumberEl.classList.contains('left')); - done(); - }); - }); + const nextElem = builder._getNextContentOnSide(startElem, + 'left'); + assert.equal(nextElem.textContent, expectedNextString); + }); - test('_getLineNumberEl unified right', done => { - // Re-render as unified: - element.viewMode = 'UNIFIED_DIFF'; - element.render(keyLocations, prefs).then(() => { - builder = element._builder; + test('_getNextContentOnSide side-by-side right', () => { + const startElem = builder.getContentByLine(5, 'right', + element.$.diffTable); + const expectedStartString = diff.content[1].b[0]; + const expectedNextString = diff.content[1].b[1]; + assert.equal(startElem.textContent, expectedStartString); - const contentEl = builder.getContentByLine(5, 'right', - element.$.diffTable); - const lineNumberEl = builder._getLineNumberEl(contentEl, 'right'); - assert.isTrue(lineNumberEl.classList.contains('lineNum')); - assert.isTrue(lineNumberEl.classList.contains('right')); - done(); - }); - }); + const nextElem = builder._getNextContentOnSide(startElem, + 'right'); + assert.equal(nextElem.textContent, expectedNextString); + }); - test('_getNextContentOnSide side-by-side left', () => { + test('_getNextContentOnSide unified left', done => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + element.render(keyLocations, prefs).then(() => { + builder = element._builder; + const startElem = builder.getContentByLine(5, 'left', element.$.diffTable); const expectedStartString = diff.content[2].ab[0]; @@ -1047,9 +1098,17 @@ const nextElem = builder._getNextContentOnSide(startElem, 'left'); assert.equal(nextElem.textContent, expectedNextString); - }); - test('_getNextContentOnSide side-by-side right', () => { + done(); + }); + }); + + test('_getNextContentOnSide unified right', done => { + // Re-render as unified: + element.viewMode = 'UNIFIED_DIFF'; + element.render(keyLocations, prefs).then(() => { + builder = element._builder; + const startElem = builder.getContentByLine(5, 'right', element.$.diffTable); const expectedStartString = diff.content[1].b[0]; @@ -1059,136 +1118,99 @@ const nextElem = builder._getNextContentOnSide(startElem, 'right'); assert.equal(nextElem.textContent, expectedNextString); - }); - test('_getNextContentOnSide unified left', done => { - // Re-render as unified: - element.viewMode = 'UNIFIED_DIFF'; - element.render(keyLocations, prefs).then(() => { - builder = element._builder; - - const startElem = builder.getContentByLine(5, 'left', - element.$.diffTable); - const expectedStartString = diff.content[2].ab[0]; - const expectedNextString = diff.content[2].ab[1]; - assert.equal(startElem.textContent, expectedStartString); - - const nextElem = builder._getNextContentOnSide(startElem, - 'left'); - assert.equal(nextElem.textContent, expectedNextString); - - done(); - }); - }); - - test('_getNextContentOnSide unified right', done => { - // Re-render as unified: - element.viewMode = 'UNIFIED_DIFF'; - element.render(keyLocations, prefs).then(() => { - builder = element._builder; - - const startElem = builder.getContentByLine(5, 'right', - element.$.diffTable); - const expectedStartString = diff.content[1].b[0]; - const expectedNextString = diff.content[1].b[1]; - assert.equal(startElem.textContent, expectedStartString); - - const nextElem = builder._getNextContentOnSide(startElem, - 'right'); - assert.equal(nextElem.textContent, expectedNextString); - - done(); - }); - }); - - test('escaping HTML', () => { - let input = '<script>alert("XSS");<' + '/script>'; - let expected = '<script>alert("XSS");</script>'; - let result = builder._formatText(input, 1, Infinity).innerHTML; - assert.equal(result, expected); - - input = '& < > " \' / `'; - expected = '& < > " \' / `'; - result = builder._formatText(input, 1, Infinity).innerHTML; - assert.equal(result, expected); + done(); }); }); - suite('blame', () => { - let mockBlame; + test('escaping HTML', () => { + let input = '<script>alert("XSS");<' + '/script>'; + let expected = '<script>alert("XSS");</script>'; + let result = builder._formatText(input, 1, Infinity).innerHTML; + assert.equal(result, expected); - setup(() => { - mockBlame = [ - {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]}, - {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]}, - ]; - }); - - test('setBlame attempts to render each blamed line', () => { - const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum') - .returns(null); - builder.setBlame(mockBlame); - assert.equal(getBlameStub.callCount, 32); - }); - - test('_getBlameCommitForBaseLine', () => { - builder.setBlame(mockBlame); - assert.isOk(builder._getBlameCommitForBaseLine(1)); - assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1'); - - assert.isOk(builder._getBlameCommitForBaseLine(11)); - assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1'); - - assert.isOk(builder._getBlameCommitForBaseLine(32)); - assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2'); - - assert.isNull(builder._getBlameCommitForBaseLine(33)); - }); - - test('_getBlameCommitForBaseLine w/o blame returns null', () => { - assert.isNull(builder._getBlameCommitForBaseLine(1)); - assert.isNull(builder._getBlameCommitForBaseLine(11)); - assert.isNull(builder._getBlameCommitForBaseLine(31)); - }); - - test('_createBlameCell', () => { - const mocbBlameCell = document.createElement('span'); - const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine') - .returns(mocbBlameCell); - const line = new GrDiffLine(GrDiffLine.Type.BOTH); - line.beforeNumber = 3; - line.afterNumber = 5; - - const result = builder._createBlameCell(line); - - assert.isTrue(getBlameStub.calledWithExactly(3)); - assert.equal(result.getAttribute('data-line-number'), '3'); - assert.equal(result.firstChild, mocbBlameCell); - }); - - test('_getBlameForBaseLine', () => { - const mockCommit = { - time: 1576105200, - id: 1234567890, - author: 'Clark Kent', - commit_msg: 'Testing Commit', - ranges: [1], - }; - const blameNode = builder._getBlameForBaseLine(1, mockCommit); - - const authors = blameNode.getElementsByClassName('blameAuthor'); - assert.equal(authors.length, 1); - assert.equal(authors[0].innerText, ' Clark'); - - const date = (new Date(mockCommit.time * 1000)).toLocaleDateString(); - Polymer.dom.flush(); - const cards = blameNode.getElementsByClassName('blameHoverCard'); - assert.equal(cards.length, 1); - assert.equal(cards[0].innerHTML, - `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}` - + '<br><br>Testing Commit' - ); - }); + input = '& < > " \' / `'; + expected = '& < > " \' / `'; + result = builder._formatText(input, 1, Infinity).innerHTML; + assert.equal(result, expected); }); }); + + suite('blame', () => { + let mockBlame; + + setup(() => { + mockBlame = [ + {id: 'commit 1', ranges: [{start: 1, end: 2}, {start: 10, end: 16}]}, + {id: 'commit 2', ranges: [{start: 4, end: 10}, {start: 17, end: 32}]}, + ]; + }); + + test('setBlame attempts to render each blamed line', () => { + const getBlameStub = sandbox.stub(builder, '_getBlameByLineNum') + .returns(null); + builder.setBlame(mockBlame); + assert.equal(getBlameStub.callCount, 32); + }); + + test('_getBlameCommitForBaseLine', () => { + builder.setBlame(mockBlame); + assert.isOk(builder._getBlameCommitForBaseLine(1)); + assert.equal(builder._getBlameCommitForBaseLine(1).id, 'commit 1'); + + assert.isOk(builder._getBlameCommitForBaseLine(11)); + assert.equal(builder._getBlameCommitForBaseLine(11).id, 'commit 1'); + + assert.isOk(builder._getBlameCommitForBaseLine(32)); + assert.equal(builder._getBlameCommitForBaseLine(32).id, 'commit 2'); + + assert.isNull(builder._getBlameCommitForBaseLine(33)); + }); + + test('_getBlameCommitForBaseLine w/o blame returns null', () => { + assert.isNull(builder._getBlameCommitForBaseLine(1)); + assert.isNull(builder._getBlameCommitForBaseLine(11)); + assert.isNull(builder._getBlameCommitForBaseLine(31)); + }); + + test('_createBlameCell', () => { + const mocbBlameCell = document.createElement('span'); + const getBlameStub = sinon.stub(builder, '_getBlameForBaseLine') + .returns(mocbBlameCell); + const line = new GrDiffLine(GrDiffLine.Type.BOTH); + line.beforeNumber = 3; + line.afterNumber = 5; + + const result = builder._createBlameCell(line); + + assert.isTrue(getBlameStub.calledWithExactly(3)); + assert.equal(result.getAttribute('data-line-number'), '3'); + assert.equal(result.firstChild, mocbBlameCell); + }); + + test('_getBlameForBaseLine', () => { + const mockCommit = { + time: 1576105200, + id: 1234567890, + author: 'Clark Kent', + commit_msg: 'Testing Commit', + ranges: [1], + }; + const blameNode = builder._getBlameForBaseLine(1, mockCommit); + + const authors = blameNode.getElementsByClassName('blameAuthor'); + assert.equal(authors.length, 1); + assert.equal(authors[0].innerText, ' Clark'); + + const date = (new Date(mockCommit.time * 1000)).toLocaleDateString(); + flush(); + const cards = blameNode.getElementsByClassName('blameHoverCard'); + assert.equal(cards.length, 1); + assert.equal(cards[0].innerHTML, + `Commit 1234567890<br>Author: Clark Kent<br>Date: ${date}` + + '<br><br>Testing Commit' + ); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html index 4f0a94f..0ce9a42 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-unified_test.html
@@ -19,189 +19,206 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>GrDiffBuilderUnified</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> -<script src="../gr-diff/gr-diff-line.js"></script> -<script src="../gr-diff/gr-diff-group.js"></script> -<script src="../gr-diff-highlight/gr-annotation.js"></script> -<script src="gr-diff-builder.js"></script> -<script src="gr-diff-builder-unified.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> +<script type="module" src="../gr-diff/gr-diff-line.js"></script> +<script type="module" src="../gr-diff/gr-diff-group.js"></script> +<script type="module" src="../gr-diff-highlight/gr-annotation.js"></script> +<script type="module" src="./gr-diff-builder.js"></script> +<script type="module" src="./gr-diff-builder-unified.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff/gr-diff-group.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import './gr-diff-builder.js'; +import './gr-diff-builder-unified.js'; +void(0); +</script> -<script> - suite('GrDiffBuilderUnified tests', async () => { - await readyToTest(); - let prefs; - let outputEl; - let diffBuilder; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff/gr-diff-group.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import './gr-diff-builder.js'; +import './gr-diff-builder-unified.js'; +suite('GrDiffBuilderUnified tests', () => { + let prefs; + let outputEl; + let diffBuilder; - setup(()=> { - prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - }; - outputEl = document.createElement('div'); - diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []); + setup(()=> { + prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + }; + outputEl = document.createElement('div'); + diffBuilder = new GrDiffBuilderUnified({}, prefs, outputEl, []); + }); + + suite('buildSectionElement for BOTH group', () => { + let lines; + let group; + + setup(() => { + lines = [ + new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2), + new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3), + new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4), + ]; + lines[0].text = 'def hello_world():'; + lines[1].text = ' print "Hello World";'; + lines[2].text = ' return True'; + + group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines); }); - suite('buildSectionElement for BOTH group', () => { - let lines; - let group; - - setup(() => { - lines = [ - new GrDiffLine(GrDiffLine.Type.BOTH, 1, 2), - new GrDiffLine(GrDiffLine.Type.BOTH, 2, 3), - new GrDiffLine(GrDiffLine.Type.BOTH, 3, 4), - ]; - lines[0].text = 'def hello_world():'; - lines[1].text = ' print "Hello World";'; - lines[2].text = ' return True'; - - group = new GrDiffGroup(GrDiffGroup.Type.BOTH, lines); - }); - - test('creates the section', () => { - const sectionEl = diffBuilder.buildSectionElement(group); - assert.isTrue(sectionEl.classList.contains('section')); - assert.isTrue(sectionEl.classList.contains('both')); - }); - - test('creates each unchanged row once', () => { - const sectionEl = diffBuilder.buildSectionElement(group); - const rowEls = sectionEl.querySelectorAll('.diff-row'); - - assert.equal(rowEls.length, 3); - - assert.equal( - rowEls[0].querySelector('.lineNum.left').textContent, - lines[0].beforeNumber); - assert.equal( - rowEls[0].querySelector('.lineNum.right').textContent, - lines[0].afterNumber); - assert.equal( - rowEls[0].querySelector('.content').textContent, lines[0].text); - - assert.equal( - rowEls[1].querySelector('.lineNum.left').textContent, - lines[1].beforeNumber); - assert.equal( - rowEls[1].querySelector('.lineNum.right').textContent, - lines[1].afterNumber); - assert.equal( - rowEls[1].querySelector('.content').textContent, lines[1].text); - - assert.equal( - rowEls[2].querySelector('.lineNum.left').textContent, - lines[2].beforeNumber); - assert.equal( - rowEls[2].querySelector('.lineNum.right').textContent, - lines[2].afterNumber); - assert.equal( - rowEls[2].querySelector('.content').textContent, lines[2].text); - }); + test('creates the section', () => { + const sectionEl = diffBuilder.buildSectionElement(group); + assert.isTrue(sectionEl.classList.contains('section')); + assert.isTrue(sectionEl.classList.contains('both')); }); - suite('buildSectionElement for DELTA group', () => { - let lines; - let group; + test('creates each unchanged row once', () => { + const sectionEl = diffBuilder.buildSectionElement(group); + const rowEls = sectionEl.querySelectorAll('.diff-row'); - setup(() => { - lines = [ - new GrDiffLine(GrDiffLine.Type.REMOVE, 1), - new GrDiffLine(GrDiffLine.Type.REMOVE, 2), - new GrDiffLine(GrDiffLine.Type.ADD, 2), - new GrDiffLine(GrDiffLine.Type.ADD, 3), - ]; - lines[0].text = 'def hello_world():'; - lines[1].text = ' print "Hello World"'; - lines[2].text = 'def hello_universe()'; - lines[3].text = ' print "Hello Universe"'; + assert.equal(rowEls.length, 3); - group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines); - }); + assert.equal( + rowEls[0].querySelector('.lineNum.left').textContent, + lines[0].beforeNumber); + assert.equal( + rowEls[0].querySelector('.lineNum.right').textContent, + lines[0].afterNumber); + assert.equal( + rowEls[0].querySelector('.content').textContent, lines[0].text); - test('creates the section', () => { - const sectionEl = diffBuilder.buildSectionElement(group); - assert.isTrue(sectionEl.classList.contains('section')); - assert.isTrue(sectionEl.classList.contains('delta')); - }); + assert.equal( + rowEls[1].querySelector('.lineNum.left').textContent, + lines[1].beforeNumber); + assert.equal( + rowEls[1].querySelector('.lineNum.right').textContent, + lines[1].afterNumber); + assert.equal( + rowEls[1].querySelector('.content').textContent, lines[1].text); - test('creates the section with class if ignoredWhitespaceOnly', () => { - group.ignoredWhitespaceOnly = true; - const sectionEl = diffBuilder.buildSectionElement(group); - assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly')); - }); - - test('creates the section with class if dueToRebase', () => { - group.dueToRebase = true; - const sectionEl = diffBuilder.buildSectionElement(group); - assert.isTrue(sectionEl.classList.contains('dueToRebase')); - }); - - test('creates first the removed and then the added rows', () => { - const sectionEl = diffBuilder.buildSectionElement(group); - const rowEls = sectionEl.querySelectorAll('.diff-row'); - - assert.equal(rowEls.length, 4); - - assert.equal( - rowEls[0].querySelector('.lineNum.left').textContent, - lines[0].beforeNumber); - assert.isNotOk(rowEls[0].querySelector('.lineNum.right')); - assert.equal( - rowEls[0].querySelector('.content').textContent, lines[0].text); - - assert.equal( - rowEls[1].querySelector('.lineNum.left').textContent, - lines[1].beforeNumber); - assert.isNotOk(rowEls[1].querySelector('.lineNum.right')); - assert.equal( - rowEls[1].querySelector('.content').textContent, lines[1].text); - - assert.isNotOk(rowEls[2].querySelector('.lineNum.left')); - assert.equal( - rowEls[2].querySelector('.lineNum.right').textContent, - lines[2].afterNumber); - assert.equal( - rowEls[2].querySelector('.content').textContent, lines[2].text); - - assert.isNotOk(rowEls[3].querySelector('.lineNum.left')); - assert.equal( - rowEls[3].querySelector('.lineNum.right').textContent, - lines[3].afterNumber); - assert.equal( - rowEls[3].querySelector('.content').textContent, lines[3].text); - }); - - test('creates only the added rows if only ignored whitespace', () => { - group.ignoredWhitespaceOnly = true; - const sectionEl = diffBuilder.buildSectionElement(group); - const rowEls = sectionEl.querySelectorAll('.diff-row'); - - assert.equal(rowEls.length, 2); - - assert.isNotOk(rowEls[0].querySelector('.lineNum.left')); - assert.equal( - rowEls[0].querySelector('.lineNum.right').textContent, - lines[2].afterNumber); - assert.equal( - rowEls[0].querySelector('.content').textContent, lines[2].text); - - assert.isNotOk(rowEls[1].querySelector('.lineNum.left')); - assert.equal( - rowEls[1].querySelector('.lineNum.right').textContent, - lines[3].afterNumber); - assert.equal( - rowEls[1].querySelector('.content').textContent, lines[3].text); - }); + assert.equal( + rowEls[2].querySelector('.lineNum.left').textContent, + lines[2].beforeNumber); + assert.equal( + rowEls[2].querySelector('.lineNum.right').textContent, + lines[2].afterNumber); + assert.equal( + rowEls[2].querySelector('.content').textContent, lines[2].text); }); }); + + suite('buildSectionElement for DELTA group', () => { + let lines; + let group; + + setup(() => { + lines = [ + new GrDiffLine(GrDiffLine.Type.REMOVE, 1), + new GrDiffLine(GrDiffLine.Type.REMOVE, 2), + new GrDiffLine(GrDiffLine.Type.ADD, 2), + new GrDiffLine(GrDiffLine.Type.ADD, 3), + ]; + lines[0].text = 'def hello_world():'; + lines[1].text = ' print "Hello World"'; + lines[2].text = 'def hello_universe()'; + lines[3].text = ' print "Hello Universe"'; + + group = new GrDiffGroup(GrDiffGroup.Type.DELTA, lines); + }); + + test('creates the section', () => { + const sectionEl = diffBuilder.buildSectionElement(group); + assert.isTrue(sectionEl.classList.contains('section')); + assert.isTrue(sectionEl.classList.contains('delta')); + }); + + test('creates the section with class if ignoredWhitespaceOnly', () => { + group.ignoredWhitespaceOnly = true; + const sectionEl = diffBuilder.buildSectionElement(group); + assert.isTrue(sectionEl.classList.contains('ignoredWhitespaceOnly')); + }); + + test('creates the section with class if dueToRebase', () => { + group.dueToRebase = true; + const sectionEl = diffBuilder.buildSectionElement(group); + assert.isTrue(sectionEl.classList.contains('dueToRebase')); + }); + + test('creates first the removed and then the added rows', () => { + const sectionEl = diffBuilder.buildSectionElement(group); + const rowEls = sectionEl.querySelectorAll('.diff-row'); + + assert.equal(rowEls.length, 4); + + assert.equal( + rowEls[0].querySelector('.lineNum.left').textContent, + lines[0].beforeNumber); + assert.isNotOk(rowEls[0].querySelector('.lineNum.right')); + assert.equal( + rowEls[0].querySelector('.content').textContent, lines[0].text); + + assert.equal( + rowEls[1].querySelector('.lineNum.left').textContent, + lines[1].beforeNumber); + assert.isNotOk(rowEls[1].querySelector('.lineNum.right')); + assert.equal( + rowEls[1].querySelector('.content').textContent, lines[1].text); + + assert.isNotOk(rowEls[2].querySelector('.lineNum.left')); + assert.equal( + rowEls[2].querySelector('.lineNum.right').textContent, + lines[2].afterNumber); + assert.equal( + rowEls[2].querySelector('.content').textContent, lines[2].text); + + assert.isNotOk(rowEls[3].querySelector('.lineNum.left')); + assert.equal( + rowEls[3].querySelector('.lineNum.right').textContent, + lines[3].afterNumber); + assert.equal( + rowEls[3].querySelector('.content').textContent, lines[3].text); + }); + + test('creates only the added rows if only ignored whitespace', () => { + group.ignoredWhitespaceOnly = true; + const sectionEl = diffBuilder.buildSectionElement(group); + const rowEls = sectionEl.querySelectorAll('.diff-row'); + + assert.equal(rowEls.length, 2); + + assert.isNotOk(rowEls[0].querySelector('.lineNum.left')); + assert.equal( + rowEls[0].querySelector('.lineNum.right').textContent, + lines[2].afterNumber); + assert.equal( + rowEls[0].querySelector('.content').textContent, lines[2].text); + + assert.isNotOk(rowEls[1].querySelector('.lineNum.left')); + assert.equal( + rowEls[1].querySelector('.lineNum.right').textContent, + lines[3].afterNumber); + assert.equal( + rowEls[1].querySelector('.content').textContent, lines[3].text); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js index 87152d8..92ea310 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.js
@@ -14,485 +14,494 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DiffSides = { - LEFT: 'left', - RIGHT: 'right', - }; +import '../../shared/gr-cursor-manager/gr-cursor-manager.js'; +import {afterNextRender} from '@polymer/polymer/lib/utils/render-status.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-cursor_html.js'; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +const DiffSides = { + LEFT: 'left', + RIGHT: 'right', +}; - const ScrollBehavior = { - KEEP_VISIBLE: 'keep-visible', - NEVER: 'never', - }; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; - const LEFT_SIDE_CLASS = 'target-side-left'; - const RIGHT_SIDE_CLASS = 'target-side-right'; +const ScrollBehavior = { + KEEP_VISIBLE: 'keep-visible', + NEVER: 'never', +}; - /** @extends Polymer.Element */ - class GrDiffCursor extends Polymer.mixinBehaviors([Gerrit.FireBehavior], - Polymer.GestureEventListeners( - Polymer.LegacyElementMixin(Polymer.Element))) { - static get is() { return 'gr-diff-cursor'; } +const LEFT_SIDE_CLASS = 'target-side-left'; +const RIGHT_SIDE_CLASS = 'target-side-right'; - static get properties() { - return { +/** @extends Polymer.Element */ +class GrDiffCursor extends mixinBehaviors([Gerrit.FireBehavior], + GestureEventListeners( + LegacyElementMixin(PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-cursor'; } + + static get properties() { + return { + /** + * Either DiffSides.LEFT or DiffSides.RIGHT. + */ + side: { + type: String, + value: DiffSides.RIGHT, + }, + /** @type {!HTMLElement|undefined} */ + diffRow: { + type: Object, + notify: true, + observer: '_rowChanged', + }, + /** - * Either DiffSides.LEFT or DiffSides.RIGHT. + * The diff views to cursor through and listen to. */ - side: { - type: String, - value: DiffSides.RIGHT, - }, - /** @type {!HTMLElement|undefined} */ - diffRow: { - type: Object, - notify: true, - observer: '_rowChanged', - }, + diffs: { + type: Array, + value() { return []; }, + }, - /** - * The diff views to cursor through and listen to. - */ - diffs: { - type: Array, - value() { return []; }, - }, + /** + * If set, the cursor will attempt to move to the line number (instead of + * the first chunk) the next time the diff renders. It is set back to null + * when used. It should be only used if you want the line to be focused + * after initialization of the component and page should scroll + * to that position. This parameter should be set at most for one gr-diff + * element in the page. + * + * @type {?number} + */ + initialLineNumber: { + type: Number, + value: null, + }, - /** - * If set, the cursor will attempt to move to the line number (instead of - * the first chunk) the next time the diff renders. It is set back to null - * when used. It should be only used if you want the line to be focused - * after initialization of the component and page should scroll - * to that position. This parameter should be set at most for one gr-diff - * element in the page. - * - * @type {?number} - */ - initialLineNumber: { - type: Number, - value: null, - }, + /** + * The scroll behavior for the cursor. Values are 'never' and + * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond + * the viewport. + */ + _scrollBehavior: { + type: String, + value: ScrollBehavior.KEEP_VISIBLE, + }, - /** - * The scroll behavior for the cursor. Values are 'never' and - * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond - * the viewport. - */ - _scrollBehavior: { - type: String, - value: ScrollBehavior.KEEP_VISIBLE, - }, + _focusOnMove: { + type: Boolean, + value: true, + }, - _focusOnMove: { - type: Boolean, - value: true, - }, + _listeningForScroll: Boolean, - _listeningForScroll: Boolean, + /** + * gr-diff-view has gr-fixed-panel on top. The panel can + * intersect a main element and partially hides a content of + * the main element. To correctly calculates visibility of an + * element, the cursor must know how much height occuped by a fixed + * panel. + * The scrollTopMargin defines margin occuped by fixed panel. + */ + scrollTopMargin: { + type: Number, + value: 0, + }, + }; + } - /** - * gr-diff-view has gr-fixed-panel on top. The panel can - * intersect a main element and partially hides a content of - * the main element. To correctly calculates visibility of an - * element, the cursor must know how much height occuped by a fixed - * panel. - * The scrollTopMargin defines margin occuped by fixed panel. - */ - scrollTopMargin: { - type: Number, - value: 0, - }, - }; + static get observers() { + return [ + '_updateSideClass(side)', + '_diffsChanged(diffs.splices)', + ]; + } + + /** @override */ + ready() { + super.ready(); + afterNextRender(this, () => { + /* + This represents the diff cursor is ready for interaction coming from + client components. It is more then Polymer "ready" lifecycle, as no + "ready" events are automatically fired by Polymer, it means + the cursor is completely interactable - in this case attached and + painted on the page. We name it "ready" instead of "rendered" as the + long-term goal is to make gr-diff-cursor a javascript class - not a DOM + element with an actual lifecycle. This will be triggered only once + per element. + */ + this.fire('ready', null, {bubbles: false}); + }); + } + + /** @override */ + attached() { + super.attached(); + // Catch when users are scrolling as the view loads. + this.listen(window, 'scroll', '_handleWindowScroll'); + } + + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'scroll', '_handleWindowScroll'); + } + + moveLeft() { + this.side = DiffSides.LEFT; + if (this._isTargetBlank()) { + this.moveUp(); + } + } + + moveRight() { + this.side = DiffSides.RIGHT; + if (this._isTargetBlank()) { + this.moveUp(); + } + } + + moveDown() { + if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { + this.$.cursorManager.next(this._rowHasSide.bind(this)); + } else { + this.$.cursorManager.next(); + } + } + + moveUp() { + if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { + this.$.cursorManager.previous(this._rowHasSide.bind(this)); + } else { + this.$.cursorManager.previous(); + } + } + + moveToVisibleArea() { + if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { + this.$.cursorManager.moveToVisibleArea( + this._rowHasSide.bind(this)); + } else { + this.$.cursorManager.moveToVisibleArea(); + } + } + + moveToNextChunk(opt_clipToTop) { + this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this), + target => target.parentNode.scrollHeight, opt_clipToTop); + this._fixSide(); + } + + moveToPreviousChunk() { + this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this)); + this._fixSide(); + } + + moveToNextCommentThread() { + this.$.cursorManager.next(this._rowHasThread.bind(this)); + this._fixSide(); + } + + moveToPreviousCommentThread() { + this.$.cursorManager.previous(this._rowHasThread.bind(this)); + this._fixSide(); + } + + /** + * @param {number} number + * @param {string} side + * @param {string=} opt_path + */ + moveToLineNumber(number, side, opt_path) { + const row = this._findRowByNumberAndFile(number, side, opt_path); + if (row) { + this.side = side; + this.$.cursorManager.setCursor(row); + } + } + + /** + * Get the line number element targeted by the cursor row and side. + * + * @return {?Element|undefined} + */ + getTargetLineElement() { + let lineElSelector = '.lineNum'; + + if (!this.diffRow) { + return; } - static get observers() { - return [ - '_updateSideClass(side)', - '_diffsChanged(diffs.splices)', - ]; + if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { + lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right'; } - /** @override */ - ready() { - super.ready(); - Polymer.RenderStatus.afterNextRender(this, () => { - /* - This represents the diff cursor is ready for interaction coming from - client components. It is more then Polymer "ready" lifecycle, as no - "ready" events are automatically fired by Polymer, it means - the cursor is completely interactable - in this case attached and - painted on the page. We name it "ready" instead of "rendered" as the - long-term goal is to make gr-diff-cursor a javascript class - not a DOM - element with an actual lifecycle. This will be triggered only once - per element. - */ - this.fire('ready', null, {bubbles: false}); - }); + return this.diffRow.querySelector(lineElSelector); + } + + getTargetDiffElement() { + if (!this.diffRow) return null; + + const hostOwner = dom( (this.diffRow)) + .getOwnerRoot(); + if (hostOwner && hostOwner.host && + hostOwner.host.tagName === 'GR-DIFF') { + return hostOwner.host; } + return null; + } - /** @override */ - attached() { - super.attached(); - // Catch when users are scrolling as the view loads. - this.listen(window, 'scroll', '_handleWindowScroll'); + moveToFirstChunk() { + this.$.cursorManager.moveToStart(); + this.moveToNextChunk(true); + } + + moveToLastChunk() { + this.$.cursorManager.moveToEnd(); + this.moveToPreviousChunk(); + } + + reInitCursor() { + this._updateStops(); + if (this.initialLineNumber) { + this.moveToLineNumber(this.initialLineNumber, this.side); + this.initialLineNumber = null; + } else { + this.moveToFirstChunk(); } + } - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'scroll', '_handleWindowScroll'); - } - - moveLeft() { - this.side = DiffSides.LEFT; - if (this._isTargetBlank()) { - this.moveUp(); - } - } - - moveRight() { - this.side = DiffSides.RIGHT; - if (this._isTargetBlank()) { - this.moveUp(); - } - } - - moveDown() { - if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { - this.$.cursorManager.next(this._rowHasSide.bind(this)); - } else { - this.$.cursorManager.next(); - } - } - - moveUp() { - if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { - this.$.cursorManager.previous(this._rowHasSide.bind(this)); - } else { - this.$.cursorManager.previous(); - } - } - - moveToVisibleArea() { - if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { - this.$.cursorManager.moveToVisibleArea( - this._rowHasSide.bind(this)); - } else { - this.$.cursorManager.moveToVisibleArea(); - } - } - - moveToNextChunk(opt_clipToTop) { - this.$.cursorManager.next(this._isFirstRowOfChunk.bind(this), - target => target.parentNode.scrollHeight, opt_clipToTop); - this._fixSide(); - } - - moveToPreviousChunk() { - this.$.cursorManager.previous(this._isFirstRowOfChunk.bind(this)); - this._fixSide(); - } - - moveToNextCommentThread() { - this.$.cursorManager.next(this._rowHasThread.bind(this)); - this._fixSide(); - } - - moveToPreviousCommentThread() { - this.$.cursorManager.previous(this._rowHasThread.bind(this)); - this._fixSide(); - } - - /** - * @param {number} number - * @param {string} side - * @param {string=} opt_path - */ - moveToLineNumber(number, side, opt_path) { - const row = this._findRowByNumberAndFile(number, side, opt_path); - if (row) { - this.side = side; - this.$.cursorManager.setCursor(row); - } - } - - /** - * Get the line number element targeted by the cursor row and side. - * - * @return {?Element|undefined} - */ - getTargetLineElement() { - let lineElSelector = '.lineNum'; - - if (!this.diffRow) { - return; - } - - if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE) { - lineElSelector += this.side === DiffSides.LEFT ? '.left' : '.right'; - } - - return this.diffRow.querySelector(lineElSelector); - } - - getTargetDiffElement() { - if (!this.diffRow) return null; - - const hostOwner = Polymer.dom(/** @type {Node} */ (this.diffRow)) - .getOwnerRoot(); - if (hostOwner && hostOwner.host && - hostOwner.host.tagName === 'GR-DIFF') { - return hostOwner.host; - } - return null; - } - - moveToFirstChunk() { - this.$.cursorManager.moveToStart(); - this.moveToNextChunk(true); - } - - moveToLastChunk() { - this.$.cursorManager.moveToEnd(); - this.moveToPreviousChunk(); - } - - reInitCursor() { - this._updateStops(); - if (this.initialLineNumber) { - this.moveToLineNumber(this.initialLineNumber, this.side); - this.initialLineNumber = null; - } else { - this.moveToFirstChunk(); - } - } - - _handleWindowScroll() { - if (this._listeningForScroll) { - this._scrollBehavior = ScrollBehavior.NEVER; - this._focusOnMove = false; - this._listeningForScroll = false; - } - } - - handleDiffUpdate() { - this._updateStops(); - if (!this.diffRow) { - // does not scroll during init unless requested - const scrollingBehaviorForInit = this.initialLineNumber ? - ScrollBehavior.KEEP_VISIBLE : - ScrollBehavior.NEVER; - this._scrollBehavior = scrollingBehaviorForInit; - this.reInitCursor(); - } - this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE; - this._focusOnMove = true; + _handleWindowScroll() { + if (this._listeningForScroll) { + this._scrollBehavior = ScrollBehavior.NEVER; + this._focusOnMove = false; this._listeningForScroll = false; } + } - _handleDiffRenderStart() { - this._listeningForScroll = true; + handleDiffUpdate() { + this._updateStops(); + if (!this.diffRow) { + // does not scroll during init unless requested + const scrollingBehaviorForInit = this.initialLineNumber ? + ScrollBehavior.KEEP_VISIBLE : + ScrollBehavior.NEVER; + this._scrollBehavior = scrollingBehaviorForInit; + this.reInitCursor(); } + this._scrollBehavior = ScrollBehavior.KEEP_VISIBLE; + this._focusOnMove = true; + this._listeningForScroll = false; + } - createCommentInPlace() { - const diffWithRangeSelected = this.diffs - .find(diff => diff.isRangeSelected()); - if (diffWithRangeSelected) { - diffWithRangeSelected.createRangeComment(); - } else { - const line = this.getTargetLineElement(); - if (line) { - this.getTargetDiffElement().addDraftAtLine(line); - } - } - } + _handleDiffRenderStart() { + this._listeningForScroll = true; + } - /** - * Get an object describing the location of the cursor. Such as - * {leftSide: false, number: 123} for line 123 of the revision, or - * {leftSide: true, number: 321} for line 321 of the base patch. - * Returns null if an address is not available. - * - * @return {?Object} - */ - getAddress() { - if (!this.diffRow) { return null; } - - // Get the line-number cell targeted by the cursor. If the mode is unified - // then prefer the revision cell if available. - let cell; - if (this._getViewMode() === DiffViewMode.UNIFIED) { - cell = this.diffRow.querySelector('.lineNum.right'); - if (!cell) { - cell = this.diffRow.querySelector('.lineNum.left'); - } - } else { - cell = this.diffRow.querySelector('.lineNum.' + this.side); - } - if (!cell) { return null; } - - const number = cell.getAttribute('data-value'); - if (!number || number === 'FILE') { return null; } - - return { - leftSide: cell.matches('.left'), - number: parseInt(number, 10), - }; - } - - _getViewMode() { - if (!this.diffRow) { - return null; - } - - if (this.diffRow.classList.contains('side-by-side')) { - return DiffViewMode.SIDE_BY_SIDE; - } else { - return DiffViewMode.UNIFIED; - } - } - - _rowHasSide(row) { - const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') + - ' + .content'; - return !!row.querySelector(selector); - } - - _isFirstRowOfChunk(row) { - const parentClassList = row.parentNode.classList; - return parentClassList.contains('section') && - parentClassList.contains('delta') && - !row.previousSibling; - } - - _rowHasThread(row) { - return row.querySelector('.thread-group'); - } - - /** - * If we jumped to a row where there is no content on the current side then - * switch to the alternate side. - */ - _fixSide() { - if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE && - this._isTargetBlank()) { - this.side = this.side === DiffSides.LEFT ? - DiffSides.RIGHT : DiffSides.LEFT; - } - } - - _isTargetBlank() { - if (!this.diffRow) { - return false; - } - - const actions = this._getActionsForRow(); - return (this.side === DiffSides.LEFT && !actions.left) || - (this.side === DiffSides.RIGHT && !actions.right); - } - - _rowChanged(newRow, oldRow) { - if (oldRow) { - oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS); - } - this._updateSideClass(); - } - - _updateSideClass() { - if (!this.diffRow) { - return; - } - this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT, - this.diffRow); - this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT, - this.diffRow); - } - - _isActionType(type) { - return type !== 'blank' && type !== 'contextControl'; - } - - _getActionsForRow() { - const actions = {left: false, right: false}; - if (this.diffRow) { - actions.left = this._isActionType( - this.diffRow.getAttribute('left-type')); - actions.right = this._isActionType( - this.diffRow.getAttribute('right-type')); - } - return actions; - } - - _getStops() { - return this.diffs.reduce( - (stops, diff) => stops.concat(diff.getCursorStops()), []); - } - - _updateStops() { - this.$.cursorManager.stops = this._getStops(); - } - - /** - * Setup and tear down on-render listeners for any diffs that are added or - * removed from the cursor. - * - * @private - */ - _diffsChanged(changeRecord) { - if (!changeRecord) { return; } - - this._updateStops(); - - let splice; - let i; - for (let spliceIdx = 0; - changeRecord.indexSplices && - spliceIdx < changeRecord.indexSplices.length; - spliceIdx++) { - splice = changeRecord.indexSplices[spliceIdx]; - - for (i = splice.index; - i < splice.index + splice.addedCount; - i++) { - this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart'); - this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate'); - } - - for (i = 0; - i < splice.removed && splice.removed.length; - i++) { - this.unlisten(splice.removed[i], - 'render-start', '_handleDiffRenderStart'); - this.unlisten(splice.removed[i], - 'render-content', 'handleDiffUpdate'); - } - } - } - - _findRowByNumberAndFile(targetNumber, side, opt_path) { - let stops; - if (opt_path) { - const diff = this.diffs.filter(diff => diff.path === opt_path)[0]; - stops = diff.getCursorStops(); - } else { - stops = this.$.cursorManager.stops; - } - let selector; - for (let i = 0; i < stops.length; i++) { - selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]'; - if (stops[i].querySelector(selector)) { - return stops[i]; - } + createCommentInPlace() { + const diffWithRangeSelected = this.diffs + .find(diff => diff.isRangeSelected()); + if (diffWithRangeSelected) { + diffWithRangeSelected.createRangeComment(); + } else { + const line = this.getTargetLineElement(); + if (line) { + this.getTargetDiffElement().addDraftAtLine(line); } } } - customElements.define(GrDiffCursor.is, GrDiffCursor); -})(); + /** + * Get an object describing the location of the cursor. Such as + * {leftSide: false, number: 123} for line 123 of the revision, or + * {leftSide: true, number: 321} for line 321 of the base patch. + * Returns null if an address is not available. + * + * @return {?Object} + */ + getAddress() { + if (!this.diffRow) { return null; } + + // Get the line-number cell targeted by the cursor. If the mode is unified + // then prefer the revision cell if available. + let cell; + if (this._getViewMode() === DiffViewMode.UNIFIED) { + cell = this.diffRow.querySelector('.lineNum.right'); + if (!cell) { + cell = this.diffRow.querySelector('.lineNum.left'); + } + } else { + cell = this.diffRow.querySelector('.lineNum.' + this.side); + } + if (!cell) { return null; } + + const number = cell.getAttribute('data-value'); + if (!number || number === 'FILE') { return null; } + + return { + leftSide: cell.matches('.left'), + number: parseInt(number, 10), + }; + } + + _getViewMode() { + if (!this.diffRow) { + return null; + } + + if (this.diffRow.classList.contains('side-by-side')) { + return DiffViewMode.SIDE_BY_SIDE; + } else { + return DiffViewMode.UNIFIED; + } + } + + _rowHasSide(row) { + const selector = (this.side === DiffSides.LEFT ? '.left' : '.right') + + ' + .content'; + return !!row.querySelector(selector); + } + + _isFirstRowOfChunk(row) { + const parentClassList = row.parentNode.classList; + return parentClassList.contains('section') && + parentClassList.contains('delta') && + !row.previousSibling; + } + + _rowHasThread(row) { + return row.querySelector('.thread-group'); + } + + /** + * If we jumped to a row where there is no content on the current side then + * switch to the alternate side. + */ + _fixSide() { + if (this._getViewMode() === DiffViewMode.SIDE_BY_SIDE && + this._isTargetBlank()) { + this.side = this.side === DiffSides.LEFT ? + DiffSides.RIGHT : DiffSides.LEFT; + } + } + + _isTargetBlank() { + if (!this.diffRow) { + return false; + } + + const actions = this._getActionsForRow(); + return (this.side === DiffSides.LEFT && !actions.left) || + (this.side === DiffSides.RIGHT && !actions.right); + } + + _rowChanged(newRow, oldRow) { + if (oldRow) { + oldRow.classList.remove(LEFT_SIDE_CLASS, RIGHT_SIDE_CLASS); + } + this._updateSideClass(); + } + + _updateSideClass() { + if (!this.diffRow) { + return; + } + this.toggleClass(LEFT_SIDE_CLASS, this.side === DiffSides.LEFT, + this.diffRow); + this.toggleClass(RIGHT_SIDE_CLASS, this.side === DiffSides.RIGHT, + this.diffRow); + } + + _isActionType(type) { + return type !== 'blank' && type !== 'contextControl'; + } + + _getActionsForRow() { + const actions = {left: false, right: false}; + if (this.diffRow) { + actions.left = this._isActionType( + this.diffRow.getAttribute('left-type')); + actions.right = this._isActionType( + this.diffRow.getAttribute('right-type')); + } + return actions; + } + + _getStops() { + return this.diffs.reduce( + (stops, diff) => stops.concat(diff.getCursorStops()), []); + } + + _updateStops() { + this.$.cursorManager.stops = this._getStops(); + } + + /** + * Setup and tear down on-render listeners for any diffs that are added or + * removed from the cursor. + * + * @private + */ + _diffsChanged(changeRecord) { + if (!changeRecord) { return; } + + this._updateStops(); + + let splice; + let i; + for (let spliceIdx = 0; + changeRecord.indexSplices && + spliceIdx < changeRecord.indexSplices.length; + spliceIdx++) { + splice = changeRecord.indexSplices[spliceIdx]; + + for (i = splice.index; + i < splice.index + splice.addedCount; + i++) { + this.listen(this.diffs[i], 'render-start', '_handleDiffRenderStart'); + this.listen(this.diffs[i], 'render-content', 'handleDiffUpdate'); + } + + for (i = 0; + i < splice.removed && splice.removed.length; + i++) { + this.unlisten(splice.removed[i], + 'render-start', '_handleDiffRenderStart'); + this.unlisten(splice.removed[i], + 'render-content', 'handleDiffUpdate'); + } + } + } + + _findRowByNumberAndFile(targetNumber, side, opt_path) { + let stops; + if (opt_path) { + const diff = this.diffs.filter(diff => diff.path === opt_path)[0]; + stops = diff.getCursorStops(); + } else { + stops = this.$.cursorManager.stops; + } + let selector; + for (let i = 0; i < stops.length; i++) { + selector = '.lineNum.' + side + '[data-value="' + targetNumber + '"]'; + if (stops[i].querySelector(selector)) { + return stops[i]; + } + } + } +} + +customElements.define(GrDiffCursor.is, GrDiffCursor);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js index 1e2d963..81e0c9b 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_html.js
@@ -1,33 +1,21 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> - -<dom-module id="gr-diff-cursor"> - <template> - <gr-cursor-manager - id="cursorManager" - scroll-behavior="[[_scrollBehavior]]" - cursor-target-class="target-row" - focus-on-move="[[_focusOnMove]]" - target="{{diffRow}}" - scroll-top-margin="[[scrollTopMargin]]" - ></gr-cursor-manager> - </template> - <script src="gr-diff-cursor.js"></script> -</dom-module> +export const htmlTemplate = html` + <gr-cursor-manager id="cursorManager" scroll-behavior="[[_scrollBehavior]]" cursor-target-class="target-row" focus-on-move="[[_focusOnMove]]" target="{{diffRow}}" scroll-top-margin="[[scrollTopMargin]]"></gr-cursor-manager> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html index 02b1d572..40507db 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.html
@@ -19,20 +19,29 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-cursor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="../gr-diff/gr-diff.html"> -<link rel="import" href="./gr-diff-cursor.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> +<script type="module" src="../gr-diff/gr-diff.js"></script> +<script type="module" src="./gr-diff-cursor.js"></script> +<script type="module" src="../../shared/gr-rest-api-interface/gr-rest-api-interface.js"></script> +<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff.js'; +import './gr-diff-cursor.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -49,54 +58,122 @@ </template> </test-fixture> -<script> - suite('gr-diff-cursor tests', async () => { - await readyToTest(); - let sandbox; - let cursorElement; - let diffElement; - let mockDiffResponse; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff.js'; +import './gr-diff-cursor.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-diff-cursor tests', () => { + let sandbox; + let cursorElement; + let diffElement; + let mockDiffResponse; + setup(done => { + sandbox = sinon.sandbox.create(); + + const fixtureElems = fixture('basic'); + mockDiffResponse = fixtureElems[0]; + diffElement = fixtureElems[1]; + cursorElement = fixtureElems[2]; + const restAPI = fixtureElems[3]; + + // Register the diff with the cursor. + cursorElement.push('diffs', diffElement); + + diffElement.loggedIn = false; + diffElement.patchRange = {basePatchNum: 1, patchNum: 2}; + diffElement.comments = { + left: [], + right: [], + meta: {patchRange: undefined}, + }; + const setupDone = () => { + cursorElement._updateStops(); + cursorElement.moveToFirstChunk(); + diffElement.removeEventListener('render', setupDone); + done(); + }; + diffElement.addEventListener('render', setupDone); + + restAPI.getDiffPreferences().then(prefs => { + diffElement.prefs = prefs; + diffElement.diff = mockDiffResponse.diffResponse; + }); + }); + + teardown(() => sandbox.restore()); + + test('diff cursor functionality (side-by-side)', () => { + // The cursor has been initialized to the first delta. + assert.isOk(cursorElement.diffRow); + + const firstDeltaRow = diffElement.shadowRoot + .querySelector('.section.delta .diff-row'); + assert.equal(cursorElement.diffRow, firstDeltaRow); + + cursorElement.moveDown(); + + assert.notEqual(cursorElement.diffRow, firstDeltaRow); + assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling); + + cursorElement.moveUp(); + + assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling); + assert.equal(cursorElement.diffRow, firstDeltaRow); + }); + + test('moveToLastChunk', () => { + const chunks = Array.from(dom(diffElement.root).querySelectorAll( + '.section.delta')); + assert.isAbove(chunks.length, 1); + assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0); + + cursorElement.moveToLastChunk(); + + assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), + chunks.length - 1); + }); + + test('cursor scroll behavior', () => { + cursorElement._handleDiffRenderStart(); + assert.equal(cursorElement._scrollBehavior, 'keep-visible'); + assert.isTrue(cursorElement._focusOnMove); + + cursorElement._handleWindowScroll(); + assert.equal(cursorElement._scrollBehavior, 'never'); + assert.isFalse(cursorElement._focusOnMove); + + cursorElement.handleDiffUpdate(); + assert.equal(cursorElement._scrollBehavior, 'keep-visible'); + assert.isTrue(cursorElement._focusOnMove); + }); + + suite('unified diff', () => { setup(done => { - sandbox = sinon.sandbox.create(); - - const fixtureElems = fixture('basic'); - mockDiffResponse = fixtureElems[0]; - diffElement = fixtureElems[1]; - cursorElement = fixtureElems[2]; - const restAPI = fixtureElems[3]; - - // Register the diff with the cursor. - cursorElement.push('diffs', diffElement); - - diffElement.loggedIn = false; - diffElement.patchRange = {basePatchNum: 1, patchNum: 2}; - diffElement.comments = { - left: [], - right: [], - meta: {patchRange: undefined}, - }; - const setupDone = () => { - cursorElement._updateStops(); - cursorElement.moveToFirstChunk(); - diffElement.removeEventListener('render', setupDone); + // We must allow the diff to re-render after setting the viewMode. + const renderHandler = function() { + diffElement.removeEventListener('render', renderHandler); + cursorElement.reInitCursor(); done(); }; - diffElement.addEventListener('render', setupDone); - - restAPI.getDiffPreferences().then(prefs => { - diffElement.prefs = prefs; - diffElement.diff = mockDiffResponse.diffResponse; - }); + diffElement.addEventListener('render', renderHandler); + diffElement.viewMode = 'UNIFIED_DIFF'; }); - teardown(() => sandbox.restore()); - - test('diff cursor functionality (side-by-side)', () => { + test('diff cursor functionality (unified)', () => { // The cursor has been initialized to the first delta. assert.isOk(cursorElement.diffRow); - const firstDeltaRow = diffElement.shadowRoot + let firstDeltaRow = diffElement.shadowRoot + .querySelector('.section.delta .diff-row'); + assert.equal(cursorElement.diffRow, firstDeltaRow); + + firstDeltaRow = diffElement.shadowRoot .querySelector('.section.delta .diff-row'); assert.equal(cursorElement.diffRow, firstDeltaRow); @@ -110,309 +187,248 @@ assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling); assert.equal(cursorElement.diffRow, firstDeltaRow); }); + }); - test('moveToLastChunk', () => { - const chunks = Array.from(Polymer.dom(diffElement.root).querySelectorAll( - '.section.delta')); - assert.isAbove(chunks.length, 1); - assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), 0); + test('cursor side functionality', () => { + // The side only applies to side-by-side mode, which should be the default + // mode. + assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE'); - cursorElement.moveToLastChunk(); + const firstDeltaSection = diffElement.shadowRoot + .querySelector('.section.delta'); + const firstDeltaRow = firstDeltaSection.querySelector('.diff-row'); - assert.equal(chunks.indexOf(cursorElement.diffRow.parentElement), - chunks.length - 1); - }); + // Because the first delta in this diff is on the right, it should be set + // to the right side. + assert.equal(cursorElement.side, 'right'); + assert.equal(cursorElement.diffRow, firstDeltaRow); + const firstIndex = cursorElement.$.cursorManager.index; - test('cursor scroll behavior', () => { - cursorElement._handleDiffRenderStart(); + // Move the side to the left. Because this delta only has a right side, we + // should be moved up to the previous line where there is content on the + // right. The previous row is part of the previous section. + cursorElement.moveLeft(); + + assert.equal(cursorElement.side, 'left'); + assert.notEqual(cursorElement.diffRow, firstDeltaRow); + assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1); + assert.equal(cursorElement.diffRow.parentElement, + firstDeltaSection.previousSibling); + + // If we move down, we should skip everything in the first delta because + // we are on the left side and the first delta has no content on the left. + cursorElement.moveDown(); + + assert.equal(cursorElement.side, 'left'); + assert.notEqual(cursorElement.diffRow, firstDeltaRow); + assert.isTrue(cursorElement.$.cursorManager.index > firstIndex); + assert.equal(cursorElement.diffRow.parentElement, + firstDeltaSection.nextSibling); + }); + + test('chunk skip functionality', () => { + const chunks = dom(diffElement.root).querySelectorAll( + '.section.delta'); + const indexOfChunk = function(chunk) { + return Array.prototype.indexOf.call(chunks, chunk); + }; + + // We should be initialized to the first chunk. Since this chunk only has + // content on the right side, our side should be right. + let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); + assert.equal(currentIndex, 0); + assert.equal(cursorElement.side, 'right'); + + // Move to the next chunk. + cursorElement.moveToNextChunk(); + + // Since this chunk only has content on the left side. we should have been + // automatically mvoed over. + const previousIndex = currentIndex; + currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); + assert.equal(currentIndex, previousIndex + 1); + assert.equal(cursorElement.side, 'left'); + }); + + test('initialLineNumber not provided', done => { + let scrollBehaviorDuringMove; + const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber'); + const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk', + () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; }); + + function renderHandler() { + diffElement.removeEventListener('render', renderHandler); + assert.isFalse(moveToNumStub.called); + assert.isTrue(moveToChunkStub.called); + assert.equal(scrollBehaviorDuringMove, 'never'); assert.equal(cursorElement._scrollBehavior, 'keep-visible'); - assert.isTrue(cursorElement._focusOnMove); + done(); + } + diffElement.addEventListener('render', renderHandler); + diffElement._diffChanged(mockDiffResponse.diffResponse); + }); - cursorElement._handleWindowScroll(); - assert.equal(cursorElement._scrollBehavior, 'never'); - assert.isFalse(cursorElement._focusOnMove); - - cursorElement.handleDiffUpdate(); + test('initialLineNumber provided', done => { + let scrollBehaviorDuringMove; + const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber', + () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; }); + const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk'); + function renderHandler() { + diffElement.removeEventListener('render', renderHandler); + assert.isFalse(moveToChunkStub.called); + assert.isTrue(moveToNumStub.called); + assert.equal(moveToNumStub.lastCall.args[0], 10); + assert.equal(moveToNumStub.lastCall.args[1], 'right'); + assert.equal(scrollBehaviorDuringMove, 'keep-visible'); assert.equal(cursorElement._scrollBehavior, 'keep-visible'); - assert.isTrue(cursorElement._focusOnMove); + done(); + } + diffElement.addEventListener('render', renderHandler); + cursorElement.initialLineNumber = 10; + cursorElement.side = 'right'; + + diffElement._diffChanged(mockDiffResponse.diffResponse); + }); + + test('getTargetDiffElement', () => { + cursorElement.initialLineNumber = 1; + assert.isTrue(!!cursorElement.diffRow); + assert.equal( + cursorElement.getTargetDiffElement(), + diffElement + ); + }); + + suite('createCommentInPlace', () => { + setup(() => { + diffElement.loggedIn = true; }); - suite('unified diff', () => { - setup(done => { - // We must allow the diff to re-render after setting the viewMode. - const renderHandler = function() { - diffElement.removeEventListener('render', renderHandler); - cursorElement.reInitCursor(); - done(); - }; - diffElement.addEventListener('render', renderHandler); - diffElement.viewMode = 'UNIFIED_DIFF'; + test('adds new draft for selected line on the left', done => { + cursorElement.moveToLineNumber(2, 'left'); + diffElement.addEventListener('create-comment', e => { + const {lineNum, range, side, patchNum} = e.detail; + assert.equal(lineNum, 2); + assert.equal(range, undefined); + assert.equal(patchNum, 1); + assert.equal(side, 'left'); + done(); }); + cursorElement.createCommentInPlace(); + }); - test('diff cursor functionality (unified)', () => { - // The cursor has been initialized to the first delta. - assert.isOk(cursorElement.diffRow); - - let firstDeltaRow = diffElement.shadowRoot - .querySelector('.section.delta .diff-row'); - assert.equal(cursorElement.diffRow, firstDeltaRow); - - firstDeltaRow = diffElement.shadowRoot - .querySelector('.section.delta .diff-row'); - assert.equal(cursorElement.diffRow, firstDeltaRow); - - cursorElement.moveDown(); - - assert.notEqual(cursorElement.diffRow, firstDeltaRow); - assert.equal(cursorElement.diffRow, firstDeltaRow.nextSibling); - - cursorElement.moveUp(); - - assert.notEqual(cursorElement.diffRow, firstDeltaRow.nextSibling); - assert.equal(cursorElement.diffRow, firstDeltaRow); + test('adds draft for selected line on the right', done => { + cursorElement.moveToLineNumber(4, 'right'); + diffElement.addEventListener('create-comment', e => { + const {lineNum, range, side, patchNum} = e.detail; + assert.equal(lineNum, 4); + assert.equal(range, undefined); + assert.equal(patchNum, 2); + assert.equal(side, 'right'); + done(); }); + cursorElement.createCommentInPlace(); }); - test('cursor side functionality', () => { - // The side only applies to side-by-side mode, which should be the default - // mode. - assert.equal(diffElement.viewMode, 'SIDE_BY_SIDE'); - - const firstDeltaSection = diffElement.shadowRoot - .querySelector('.section.delta'); - const firstDeltaRow = firstDeltaSection.querySelector('.diff-row'); - - // Because the first delta in this diff is on the right, it should be set - // to the right side. - assert.equal(cursorElement.side, 'right'); - assert.equal(cursorElement.diffRow, firstDeltaRow); - const firstIndex = cursorElement.$.cursorManager.index; - - // Move the side to the left. Because this delta only has a right side, we - // should be moved up to the previous line where there is content on the - // right. The previous row is part of the previous section. - cursorElement.moveLeft(); - - assert.equal(cursorElement.side, 'left'); - assert.notEqual(cursorElement.diffRow, firstDeltaRow); - assert.equal(cursorElement.$.cursorManager.index, firstIndex - 1); - assert.equal(cursorElement.diffRow.parentElement, - firstDeltaSection.previousSibling); - - // If we move down, we should skip everything in the first delta because - // we are on the left side and the first delta has no content on the left. - cursorElement.moveDown(); - - assert.equal(cursorElement.side, 'left'); - assert.notEqual(cursorElement.diffRow, firstDeltaRow); - assert.isTrue(cursorElement.$.cursorManager.index > firstIndex); - assert.equal(cursorElement.diffRow.parentElement, - firstDeltaSection.nextSibling); - }); - - test('chunk skip functionality', () => { - const chunks = Polymer.dom(diffElement.root).querySelectorAll( - '.section.delta'); - const indexOfChunk = function(chunk) { - return Array.prototype.indexOf.call(chunks, chunk); + test('createCommentInPlace creates comment for range if selected', done => { + const someRange = { + start_line: 2, + start_character: 3, + end_line: 6, + end_character: 1, }; - - // We should be initialized to the first chunk. Since this chunk only has - // content on the right side, our side should be right. - let currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); - assert.equal(currentIndex, 0); - assert.equal(cursorElement.side, 'right'); - - // Move to the next chunk. - cursorElement.moveToNextChunk(); - - // Since this chunk only has content on the left side. we should have been - // automatically mvoed over. - const previousIndex = currentIndex; - currentIndex = indexOfChunk(cursorElement.diffRow.parentElement); - assert.equal(currentIndex, previousIndex + 1); - assert.equal(cursorElement.side, 'left'); - }); - - test('initialLineNumber not provided', done => { - let scrollBehaviorDuringMove; - const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber'); - const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk', - () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; }); - - function renderHandler() { - diffElement.removeEventListener('render', renderHandler); - assert.isFalse(moveToNumStub.called); - assert.isTrue(moveToChunkStub.called); - assert.equal(scrollBehaviorDuringMove, 'never'); - assert.equal(cursorElement._scrollBehavior, 'keep-visible'); - done(); - } - diffElement.addEventListener('render', renderHandler); - diffElement._diffChanged(mockDiffResponse.diffResponse); - }); - - test('initialLineNumber provided', done => { - let scrollBehaviorDuringMove; - const moveToNumStub = sandbox.stub(cursorElement, 'moveToLineNumber', - () => { scrollBehaviorDuringMove = cursorElement._scrollBehavior; }); - const moveToChunkStub = sandbox.stub(cursorElement, 'moveToFirstChunk'); - function renderHandler() { - diffElement.removeEventListener('render', renderHandler); - assert.isFalse(moveToChunkStub.called); - assert.isTrue(moveToNumStub.called); - assert.equal(moveToNumStub.lastCall.args[0], 10); - assert.equal(moveToNumStub.lastCall.args[1], 'right'); - assert.equal(scrollBehaviorDuringMove, 'keep-visible'); - assert.equal(cursorElement._scrollBehavior, 'keep-visible'); - done(); - } - diffElement.addEventListener('render', renderHandler); - cursorElement.initialLineNumber = 10; - cursorElement.side = 'right'; - - diffElement._diffChanged(mockDiffResponse.diffResponse); - }); - - test('getTargetDiffElement', () => { - cursorElement.initialLineNumber = 1; - assert.isTrue(!!cursorElement.diffRow); - assert.equal( - cursorElement.getTargetDiffElement(), - diffElement - ); - }); - - suite('createCommentInPlace', () => { - setup(() => { - diffElement.loggedIn = true; - }); - - test('adds new draft for selected line on the left', done => { - cursorElement.moveToLineNumber(2, 'left'); - diffElement.addEventListener('create-comment', e => { - const {lineNum, range, side, patchNum} = e.detail; - assert.equal(lineNum, 2); - assert.equal(range, undefined); - assert.equal(patchNum, 1); - assert.equal(side, 'left'); - done(); - }); - cursorElement.createCommentInPlace(); - }); - - test('adds draft for selected line on the right', done => { - cursorElement.moveToLineNumber(4, 'right'); - diffElement.addEventListener('create-comment', e => { - const {lineNum, range, side, patchNum} = e.detail; - assert.equal(lineNum, 4); - assert.equal(range, undefined); - assert.equal(patchNum, 2); - assert.equal(side, 'right'); - done(); - }); - cursorElement.createCommentInPlace(); - }); - - test('createCommentInPlace creates comment for range if selected', done => { - const someRange = { - start_line: 2, - start_character: 3, - end_line: 6, - end_character: 1, - }; - diffElement.$.highlights.selectedRange = { - side: 'right', - range: someRange, - }; - diffElement.addEventListener('create-comment', e => { - const {lineNum, range, side, patchNum} = e.detail; - assert.equal(lineNum, 6); - assert.equal(range, someRange); - assert.equal(patchNum, 2); - assert.equal(side, 'right'); - done(); - }); - cursorElement.createCommentInPlace(); - }); - - test('createCommentInPlace ignores call if nothing is selected', () => { - const createRangeCommentStub = sandbox.stub(diffElement, - 'createRangeComment'); - const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine'); - cursorElement.diffRow = undefined; - cursorElement.createCommentInPlace(); - assert.isFalse(createRangeCommentStub.called); - assert.isFalse(addDraftAtLineStub.called); - }); - }); - - test('getAddress', () => { - // It should initialize to the first chunk: line 5 of the revision. - assert.deepEqual(cursorElement.getAddress(), - {leftSide: false, number: 5}); - - // Revision line 4 is up. - cursorElement.moveUp(); - assert.deepEqual(cursorElement.getAddress(), - {leftSide: false, number: 4}); - - // Base line 4 is left. - cursorElement.moveLeft(); - assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4}); - - // Moving to the next chunk takes it back to the start. - cursorElement.moveToNextChunk(); - assert.deepEqual(cursorElement.getAddress(), - {leftSide: false, number: 5}); - - // The following chunk is a removal starting on line 10 of the base. - cursorElement.moveToNextChunk(); - assert.deepEqual(cursorElement.getAddress(), - {leftSide: true, number: 10}); - - // Should be null if there is no selection. - cursorElement.$.cursorManager.unsetCursor(); - assert.isNotOk(cursorElement.getAddress()); - }); - - test('_findRowByNumberAndFile', () => { - // Get the first ab row after the first chunk. - const row = Polymer.dom(diffElement.root).querySelectorAll('tr')[8]; - - // It should be line 8 on the right, but line 5 on the left. - assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row); - assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row); - }); - - test('expand context updates stops', done => { - sandbox.spy(cursorElement, 'handleDiffUpdate'); - MockInteractions.tap(diffElement.shadowRoot - .querySelector('.showContext')); - flush(() => { - assert.isTrue(cursorElement.handleDiffUpdate.called); + diffElement.$.highlights.selectedRange = { + side: 'right', + range: someRange, + }; + diffElement.addEventListener('create-comment', e => { + const {lineNum, range, side, patchNum} = e.detail; + assert.equal(lineNum, 6); + assert.equal(range, someRange); + assert.equal(patchNum, 2); + assert.equal(side, 'right'); done(); }); + cursorElement.createCommentInPlace(); }); - suite('gr-diff-cursor event tests', () => { - let sandbox; - let someEmptyDiv; - - setup(() => { - sandbox = sinon.sandbox.create(); - someEmptyDiv = fixture('empty'); - }); - - teardown(() => sandbox.restore()); - - test('ready is fired after component is rendered', done => { - const cursorElement = document.createElement('gr-diff-cursor'); - cursorElement.addEventListener('ready', () => { - done(); - }); - someEmptyDiv.appendChild(cursorElement); - }); + test('createCommentInPlace ignores call if nothing is selected', () => { + const createRangeCommentStub = sandbox.stub(diffElement, + 'createRangeComment'); + const addDraftAtLineStub = sandbox.stub(diffElement, 'addDraftAtLine'); + cursorElement.diffRow = undefined; + cursorElement.createCommentInPlace(); + assert.isFalse(createRangeCommentStub.called); + assert.isFalse(addDraftAtLineStub.called); }); }); + + test('getAddress', () => { + // It should initialize to the first chunk: line 5 of the revision. + assert.deepEqual(cursorElement.getAddress(), + {leftSide: false, number: 5}); + + // Revision line 4 is up. + cursorElement.moveUp(); + assert.deepEqual(cursorElement.getAddress(), + {leftSide: false, number: 4}); + + // Base line 4 is left. + cursorElement.moveLeft(); + assert.deepEqual(cursorElement.getAddress(), {leftSide: true, number: 4}); + + // Moving to the next chunk takes it back to the start. + cursorElement.moveToNextChunk(); + assert.deepEqual(cursorElement.getAddress(), + {leftSide: false, number: 5}); + + // The following chunk is a removal starting on line 10 of the base. + cursorElement.moveToNextChunk(); + assert.deepEqual(cursorElement.getAddress(), + {leftSide: true, number: 10}); + + // Should be null if there is no selection. + cursorElement.$.cursorManager.unsetCursor(); + assert.isNotOk(cursorElement.getAddress()); + }); + + test('_findRowByNumberAndFile', () => { + // Get the first ab row after the first chunk. + const row = dom(diffElement.root).querySelectorAll('tr')[8]; + + // It should be line 8 on the right, but line 5 on the left. + assert.equal(cursorElement._findRowByNumberAndFile(8, 'right'), row); + assert.equal(cursorElement._findRowByNumberAndFile(5, 'left'), row); + }); + + test('expand context updates stops', done => { + sandbox.spy(cursorElement, 'handleDiffUpdate'); + MockInteractions.tap(diffElement.shadowRoot + .querySelector('.showContext')); + flush(() => { + assert.isTrue(cursorElement.handleDiffUpdate.called); + done(); + }); + }); + + suite('gr-diff-cursor event tests', () => { + let sandbox; + let someEmptyDiv; + + setup(() => { + sandbox = sinon.sandbox.create(); + someEmptyDiv = fixture('empty'); + }); + + teardown(() => sandbox.restore()); + + test('ready is fired after component is rendered', done => { + const cursorElement = document.createElement('gr-diff-cursor'); + cursorElement.addEventListener('ready', () => { + done(); + }); + someEmptyDiv.appendChild(cursorElement); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html index 79e4036..6db0836 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-annotation_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-annotation</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="gr-annotation.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-annotation.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-annotation.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,266 +41,269 @@ </template> </test-fixture> -<script> - suite('annotation', async () => { - await readyToTest(); - let str; - let parent; - let textNode; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-annotation.js'; +import {sanitizeDOMValue, setSanitizeDOMValue} from '@polymer/polymer/lib/utils/settings.js'; +suite('annotation', () => { + let str; + let parent; + let textNode; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + parent = fixture('basic'); + textNode = parent.childNodes[0]; + str = textNode.textContent; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_annotateText Case 1', () => { + GrAnnotation._annotateText(textNode, 0, str.length, 'foobar'); + + assert.equal(parent.childNodes.length, 1); + assert.instanceOf(parent.childNodes[0], HTMLElement); + assert.equal(parent.childNodes[0].className, 'foobar'); + assert.instanceOf(parent.childNodes[0].childNodes[0], Text); + assert.equal(parent.childNodes[0].childNodes[0].textContent, str); + }); + + test('_annotateText Case 2', () => { + const length = 12; + const substr = str.substr(0, length); + const remainder = str.substr(length); + + GrAnnotation._annotateText(textNode, 0, length, 'foobar'); + + assert.equal(parent.childNodes.length, 2); + + assert.instanceOf(parent.childNodes[0], HTMLElement); + assert.equal(parent.childNodes[0].className, 'foobar'); + assert.instanceOf(parent.childNodes[0].childNodes[0], Text); + assert.equal(parent.childNodes[0].childNodes[0].textContent, substr); + + assert.instanceOf(parent.childNodes[1], Text); + assert.equal(parent.childNodes[1].textContent, remainder); + }); + + test('_annotateText Case 3', () => { + const index = 12; + const length = str.length - index; + const remainder = str.substr(0, index); + const substr = str.substr(index); + + GrAnnotation._annotateText(textNode, index, length, 'foobar'); + + assert.equal(parent.childNodes.length, 2); + + assert.instanceOf(parent.childNodes[0], Text); + assert.equal(parent.childNodes[0].textContent, remainder); + + assert.instanceOf(parent.childNodes[1], HTMLElement); + assert.equal(parent.childNodes[1].className, 'foobar'); + assert.instanceOf(parent.childNodes[1].childNodes[0], Text); + assert.equal(parent.childNodes[1].childNodes[0].textContent, substr); + }); + + test('_annotateText Case 4', () => { + const index = str.indexOf('dolor'); + const length = 'dolor '.length; + + const remainderPre = str.substr(0, index); + const substr = str.substr(index, length); + const remainderPost = str.substr(index + length); + + GrAnnotation._annotateText(textNode, index, length, 'foobar'); + + assert.equal(parent.childNodes.length, 3); + + assert.instanceOf(parent.childNodes[0], Text); + assert.equal(parent.childNodes[0].textContent, remainderPre); + + assert.instanceOf(parent.childNodes[1], HTMLElement); + assert.equal(parent.childNodes[1].className, 'foobar'); + assert.instanceOf(parent.childNodes[1].childNodes[0], Text); + assert.equal(parent.childNodes[1].childNodes[0].textContent, substr); + + assert.instanceOf(parent.childNodes[2], Text); + assert.equal(parent.childNodes[2].textContent, remainderPost); + }); + + test('_annotateElement design doc example', () => { + const layers = [ + 'amet, ', + 'inceptos ', + 'amet, ', + 'et, suspendisse ince', + ]; + + // Apply the layers successively. + layers.forEach((layer, i) => { + GrAnnotation.annotateElement( + parent, str.indexOf(layer), layer.length, `layer-${i + 1}`); + }); + + assert.equal(parent.textContent, str); + + // Layer 1: + const layer1 = parent.querySelectorAll('.layer-1'); + assert.equal(layer1.length, 1); + assert.equal(layer1[0].textContent, layers[0]); + assert.equal(layer1[0].parentElement, parent); + + // Layer 2: + const layer2 = parent.querySelectorAll('.layer-2'); + assert.equal(layer2.length, 1); + assert.equal(layer2[0].textContent, layers[1]); + assert.equal(layer2[0].parentElement, parent); + + // Layer 3: + const layer3 = parent.querySelectorAll('.layer-3'); + assert.equal(layer3.length, 1); + assert.equal(layer3[0].textContent, layers[2]); + assert.equal(layer3[0].parentElement, layer1[0]); + + // Layer 4: + const layer4 = parent.querySelectorAll('.layer-4'); + assert.equal(layer4.length, 3); + + assert.equal(layer4[0].textContent, 'et, '); + assert.equal(layer4[0].parentElement, layer3[0]); + + assert.equal(layer4[1].textContent, 'suspendisse '); + assert.equal(layer4[1].parentElement, parent); + + assert.equal(layer4[2].textContent, 'ince'); + assert.equal(layer4[2].parentElement, layer2[0]); + + assert.equal(layer4[0].textContent + + layer4[1].textContent + + layer4[2].textContent, + layers[3]); + }); + + test('splitTextNode', () => { + const helloString = 'hello'; + const asciiString = 'ASCII'; + const unicodeString = 'Unic💢de'; + + let node; + let tail; + + // Non-unicode path: + node = document.createTextNode(helloString + asciiString); + tail = GrAnnotation.splitTextNode(node, helloString.length); + assert(node.textContent, helloString); + assert(tail.textContent, asciiString); + + // Unicdoe path: + node = document.createTextNode(helloString + unicodeString); + tail = GrAnnotation.splitTextNode(node, helloString.length); + assert(node.textContent, helloString); + assert(tail.textContent, unicodeString); + }); + + suite('annotateWithElement', () => { + const fullText = '01234567890123456789'; + let mockSanitize; + let originalSanitizeDOMValue; setup(() => { - sandbox = sinon.sandbox.create(); - parent = fixture('basic'); - textNode = parent.childNodes[0]; - str = textNode.textContent; + originalSanitizeDOMValue = sanitizeDOMValue; + assert.isDefined(originalSanitizeDOMValue); + mockSanitize = sandbox.spy(originalSanitizeDOMValue); + setSanitizeDOMValue(mockSanitize); }); teardown(() => { - sandbox.restore(); + setSanitizeDOMValue(originalSanitizeDOMValue); }); - test('_annotateText Case 1', () => { - GrAnnotation._annotateText(textNode, 0, str.length, 'foobar'); + test('annotates when fully contained', () => { + const length = 10; + const container = document.createElement('div'); + container.textContent = fullText; + GrAnnotation.annotateWithElement( + container, 1, length, {tagName: 'test-wrapper'}); - assert.equal(parent.childNodes.length, 1); - assert.instanceOf(parent.childNodes[0], HTMLElement); - assert.equal(parent.childNodes[0].className, 'foobar'); - assert.instanceOf(parent.childNodes[0].childNodes[0], Text); - assert.equal(parent.childNodes[0].childNodes[0].textContent, str); + assert.equal( + container.innerHTML, + '0<test-wrapper>1234567890</test-wrapper>123456789'); }); - test('_annotateText Case 2', () => { - const length = 12; - const substr = str.substr(0, length); - const remainder = str.substr(length); + test('annotates when spanning multiple nodes', () => { + const length = 10; + const container = document.createElement('div'); + container.textContent = fullText; + GrAnnotation.annotateElement(container, 5, length, 'testclass'); + GrAnnotation.annotateWithElement( + container, 1, length, {tagName: 'test-wrapper'}); - GrAnnotation._annotateText(textNode, 0, length, 'foobar'); - - assert.equal(parent.childNodes.length, 2); - - assert.instanceOf(parent.childNodes[0], HTMLElement); - assert.equal(parent.childNodes[0].className, 'foobar'); - assert.instanceOf(parent.childNodes[0].childNodes[0], Text); - assert.equal(parent.childNodes[0].childNodes[0].textContent, substr); - - assert.instanceOf(parent.childNodes[1], Text); - assert.equal(parent.childNodes[1].textContent, remainder); + assert.equal( + container.innerHTML, + '0' + + '<test-wrapper>' + + '1234' + + '<hl class="testclass">567890</hl>' + + '</test-wrapper>' + + '<hl class="testclass">1234</hl>' + + '56789'); }); - test('_annotateText Case 3', () => { - const index = 12; - const length = str.length - index; - const remainder = str.substr(0, index); - const substr = str.substr(index); + test('annotates text node', () => { + const length = 10; + const container = document.createElement('div'); + container.textContent = fullText; + GrAnnotation.annotateWithElement( + container.childNodes[0], 1, length, {tagName: 'test-wrapper'}); - GrAnnotation._annotateText(textNode, index, length, 'foobar'); - - assert.equal(parent.childNodes.length, 2); - - assert.instanceOf(parent.childNodes[0], Text); - assert.equal(parent.childNodes[0].textContent, remainder); - - assert.instanceOf(parent.childNodes[1], HTMLElement); - assert.equal(parent.childNodes[1].className, 'foobar'); - assert.instanceOf(parent.childNodes[1].childNodes[0], Text); - assert.equal(parent.childNodes[1].childNodes[0].textContent, substr); + assert.equal( + container.innerHTML, + '0<test-wrapper>1234567890</test-wrapper>123456789'); }); - test('_annotateText Case 4', () => { - const index = str.indexOf('dolor'); - const length = 'dolor '.length; + test('handles zero-length nodes', () => { + const container = document.createElement('div'); + container.appendChild(document.createTextNode('0123456789')); + container.appendChild(document.createElement('span')); + container.appendChild(document.createTextNode('0123456789')); + GrAnnotation.annotateWithElement( + container, 1, 10, {tagName: 'test-wrapper'}); - const remainderPre = str.substr(0, index); - const substr = str.substr(index, length); - const remainderPost = str.substr(index + length); - - GrAnnotation._annotateText(textNode, index, length, 'foobar'); - - assert.equal(parent.childNodes.length, 3); - - assert.instanceOf(parent.childNodes[0], Text); - assert.equal(parent.childNodes[0].textContent, remainderPre); - - assert.instanceOf(parent.childNodes[1], HTMLElement); - assert.equal(parent.childNodes[1].className, 'foobar'); - assert.instanceOf(parent.childNodes[1].childNodes[0], Text); - assert.equal(parent.childNodes[1].childNodes[0].textContent, substr); - - assert.instanceOf(parent.childNodes[2], Text); - assert.equal(parent.childNodes[2].textContent, remainderPost); + assert.equal( + container.innerHTML, + '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789'); }); - test('_annotateElement design doc example', () => { - const layers = [ - 'amet, ', - 'inceptos ', - 'amet, ', - 'et, suspendisse ince', - ]; - - // Apply the layers successively. - layers.forEach((layer, i) => { - GrAnnotation.annotateElement( - parent, str.indexOf(layer), layer.length, `layer-${i + 1}`); - }); - - assert.equal(parent.textContent, str); - - // Layer 1: - const layer1 = parent.querySelectorAll('.layer-1'); - assert.equal(layer1.length, 1); - assert.equal(layer1[0].textContent, layers[0]); - assert.equal(layer1[0].parentElement, parent); - - // Layer 2: - const layer2 = parent.querySelectorAll('.layer-2'); - assert.equal(layer2.length, 1); - assert.equal(layer2[0].textContent, layers[1]); - assert.equal(layer2[0].parentElement, parent); - - // Layer 3: - const layer3 = parent.querySelectorAll('.layer-3'); - assert.equal(layer3.length, 1); - assert.equal(layer3[0].textContent, layers[2]); - assert.equal(layer3[0].parentElement, layer1[0]); - - // Layer 4: - const layer4 = parent.querySelectorAll('.layer-4'); - assert.equal(layer4.length, 3); - - assert.equal(layer4[0].textContent, 'et, '); - assert.equal(layer4[0].parentElement, layer3[0]); - - assert.equal(layer4[1].textContent, 'suspendisse '); - assert.equal(layer4[1].parentElement, parent); - - assert.equal(layer4[2].textContent, 'ince'); - assert.equal(layer4[2].parentElement, layer2[0]); - - assert.equal(layer4[0].textContent + - layer4[1].textContent + - layer4[2].textContent, - layers[3]); - }); - - test('splitTextNode', () => { - const helloString = 'hello'; - const asciiString = 'ASCII'; - const unicodeString = 'Unic💢de'; - - let node; - let tail; - - // Non-unicode path: - node = document.createTextNode(helloString + asciiString); - tail = GrAnnotation.splitTextNode(node, helloString.length); - assert(node.textContent, helloString); - assert(tail.textContent, asciiString); - - // Unicdoe path: - node = document.createTextNode(helloString + unicodeString); - tail = GrAnnotation.splitTextNode(node, helloString.length); - assert(node.textContent, helloString); - assert(tail.textContent, unicodeString); - }); - - suite('annotateWithElement', () => { - const fullText = '01234567890123456789'; - let mockSanitize; - let originalSanitizeDOMValue; - - setup(() => { - originalSanitizeDOMValue = window.Polymer.sanitizeDOMValue; - assert.isDefined(originalSanitizeDOMValue); - mockSanitize = sandbox.spy(originalSanitizeDOMValue); - window.Polymer.sanitizeDOMValue = mockSanitize; - }); - - teardown(() => { - window.Polymer.sanitizeDOMValue = originalSanitizeDOMValue; - }); - - test('annotates when fully contained', () => { - const length = 10; - const container = document.createElement('div'); - container.textContent = fullText; - GrAnnotation.annotateWithElement( - container, 1, length, {tagName: 'test-wrapper'}); - - assert.equal( - container.innerHTML, - '0<test-wrapper>1234567890</test-wrapper>123456789'); - }); - - test('annotates when spanning multiple nodes', () => { - const length = 10; - const container = document.createElement('div'); - container.textContent = fullText; - GrAnnotation.annotateElement(container, 5, length, 'testclass'); - GrAnnotation.annotateWithElement( - container, 1, length, {tagName: 'test-wrapper'}); - - assert.equal( - container.innerHTML, - '0' + - '<test-wrapper>' + - '1234' + - '<hl class="testclass">567890</hl>' + - '</test-wrapper>' + - '<hl class="testclass">1234</hl>' + - '56789'); - }); - - test('annotates text node', () => { - const length = 10; - const container = document.createElement('div'); - container.textContent = fullText; - GrAnnotation.annotateWithElement( - container.childNodes[0], 1, length, {tagName: 'test-wrapper'}); - - assert.equal( - container.innerHTML, - '0<test-wrapper>1234567890</test-wrapper>123456789'); - }); - - test('handles zero-length nodes', () => { - const container = document.createElement('div'); - container.appendChild(document.createTextNode('0123456789')); - container.appendChild(document.createElement('span')); - container.appendChild(document.createTextNode('0123456789')); - GrAnnotation.annotateWithElement( - container, 1, 10, {tagName: 'test-wrapper'}); - - assert.equal( - container.innerHTML, - '0<test-wrapper>123456789<span></span>0</test-wrapper>123456789'); - }); - - test('sets sanitized attributes', () => { - const container = document.createElement('div'); - container.textContent = fullText; - const attributes = { - 'href': 'foo', - 'data-foo': 'bar', - 'class': 'hello world', - }; - GrAnnotation.annotateWithElement( - container, 1, length, {tagName: 'test-wrapper', attributes}); - assert(mockSanitize.calledWith( - 'foo', 'href', 'attribute', sinon.match.instanceOf(Element))); - assert(mockSanitize.calledWith( - 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element))); - assert(mockSanitize.calledWith( - 'hello world', - 'class', - 'attribute', - sinon.match.instanceOf(Element))); - const el = container.querySelector('test-wrapper'); - assert.equal(el.getAttribute('href'), 'foo'); - assert.equal(el.getAttribute('data-foo'), 'bar'); - assert.equal(el.getAttribute('class'), 'hello world'); - }); + test('sets sanitized attributes', () => { + const container = document.createElement('div'); + container.textContent = fullText; + const attributes = { + 'href': 'foo', + 'data-foo': 'bar', + 'class': 'hello world', + }; + GrAnnotation.annotateWithElement( + container, 1, length, {tagName: 'test-wrapper', attributes}); + assert(mockSanitize.calledWith( + 'foo', 'href', 'attribute', sinon.match.instanceOf(Element))); + assert(mockSanitize.calledWith( + 'bar', 'data-foo', 'attribute', sinon.match.instanceOf(Element))); + assert(mockSanitize.calledWith( + 'hello world', + 'class', + 'attribute', + sinon.match.instanceOf(Element))); + const el = container.querySelector('test-wrapper'); + assert.equal(el.getAttribute('href'), 'foo'); + assert.equal(el.getAttribute('data-foo'), 'bar'); + assert.equal(el.getAttribute('class'), 'hello world'); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js index 99bf1c8..4567c9e 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.js
@@ -14,484 +14,496 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-selection-action-box/gr-selection-action-box.js'; +import './gr-annotation.js'; +import './gr-range-normalizer.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-highlight_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrDiffHighlight extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-highlight'; } + + static get properties() { + return { + /** @type {!Array<!Gerrit.HoveredRange>} */ + commentRanges: { + type: Array, + notify: true, + }, + loggedIn: Boolean, + /** + * querySelector can return null, so needs to be nullable. + * + * @type {?HTMLElement} + * */ + _cachedDiffBuilder: Object, + + /** + * Which range is currently selected by the user. + * Stored in order to add a range-based comment + * later. + * undefined if no range is selected. + * + * @type {{side: string, range: Gerrit.Range}|undefined} + */ + selectedRange: { + type: Object, + notify: true, + }, + }; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('comment-thread-mouseleave', + e => this._handleCommentThreadMouseleave(e)); + this.addEventListener('comment-thread-mouseenter', + e => this._handleCommentThreadMouseenter(e)); + this.addEventListener('create-comment-requested', + e => this._handleRangeCommentRequest(e)); + } + + get diffBuilder() { + if (!this._cachedDiffBuilder) { + this._cachedDiffBuilder = + dom(this).querySelector('gr-diff-builder'); + } + return this._cachedDiffBuilder; + } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Determines side/line/range for a DOM selection and shows a tooltip. + * + * With native shadow DOM, gr-diff-highlight cannot access a selection that + * references the DOM elements making up the diff because they are in the + * shadow DOM the gr-diff element. For this reason, we listen to the + * selectionchange event and retrieve the selection in gr-diff, and then + * call this method to process the Selection. + * + * @param {Selection} selection A DOM Selection living in the shadow DOM of + * the diff element. + * @param {boolean} isMouseUp If true, this is called due to a mouseup + * event, in which case we might want to immediately create a comment, + * because isMouseUp === true combined with an existing selection must + * mean that this is the end of a double-click. */ - class GrDiffHighlight extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff-highlight'; } + handleSelectionChange(selection, isMouseUp) { + // Debounce is not just nice for waiting until the selection has settled, + // it is also vital for being able to click on the action box before it is + // 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 + // simple drag for select. + this.debounce( + 'selectionChange', () => this._handleSelection(selection, isMouseUp), + 10); + } - static get properties() { - return { - /** @type {!Array<!Gerrit.HoveredRange>} */ - commentRanges: { - type: Array, - notify: true, - }, - loggedIn: Boolean, - /** - * querySelector can return null, so needs to be nullable. - * - * @type {?HTMLElement} - * */ - _cachedDiffBuilder: Object, - - /** - * Which range is currently selected by the user. - * Stored in order to add a range-based comment - * later. - * undefined if no range is selected. - * - * @type {{side: string, range: Gerrit.Range}|undefined} - */ - selectedRange: { - type: Object, - notify: true, - }, - }; + _getThreadEl(e) { + const path = dom(e).path || []; + for (const pathEl of path) { + if (pathEl.classList.contains('comment-thread')) return pathEl; } + return null; + } - /** @override */ - created() { - super.created(); - this.addEventListener('comment-thread-mouseleave', - e => this._handleCommentThreadMouseleave(e)); - this.addEventListener('comment-thread-mouseenter', - e => this._handleCommentThreadMouseenter(e)); - this.addEventListener('create-comment-requested', - e => this._handleRangeCommentRequest(e)); - } + _handleCommentThreadMouseenter(e) { + const threadEl = this._getThreadEl(e); + const index = this._indexForThreadEl(threadEl); - get diffBuilder() { - if (!this._cachedDiffBuilder) { - this._cachedDiffBuilder = - Polymer.dom(this).querySelector('gr-diff-builder'); - } - return this._cachedDiffBuilder; - } - - /** - * Determines side/line/range for a DOM selection and shows a tooltip. - * - * With native shadow DOM, gr-diff-highlight cannot access a selection that - * references the DOM elements making up the diff because they are in the - * shadow DOM the gr-diff element. For this reason, we listen to the - * selectionchange event and retrieve the selection in gr-diff, and then - * call this method to process the Selection. - * - * @param {Selection} selection A DOM Selection living in the shadow DOM of - * the diff element. - * @param {boolean} isMouseUp If true, this is called due to a mouseup - * event, in which case we might want to immediately create a comment, - * because isMouseUp === true combined with an existing selection must - * mean that this is the end of a double-click. - */ - handleSelectionChange(selection, isMouseUp) { - // Debounce is not just nice for waiting until the selection has settled, - // it is also vital for being able to click on the action box before it is - // 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 - // simple drag for select. - this.debounce( - 'selectionChange', () => this._handleSelection(selection, isMouseUp), - 10); - } - - _getThreadEl(e) { - const path = Polymer.dom(e).path || []; - for (const pathEl of path) { - if (pathEl.classList.contains('comment-thread')) return pathEl; - } - return null; - } - - _handleCommentThreadMouseenter(e) { - const threadEl = this._getThreadEl(e); - const index = this._indexForThreadEl(threadEl); - - if (index !== undefined) { - this.set(['commentRanges', index, 'hovering'], true); - } - } - - _handleCommentThreadMouseleave(e) { - const threadEl = this._getThreadEl(e); - const index = this._indexForThreadEl(threadEl); - - if (index !== undefined) { - this.set(['commentRanges', index, 'hovering'], false); - } - } - - _indexForThreadEl(threadEl) { - const side = threadEl.getAttribute('comment-side'); - const range = JSON.parse(threadEl.getAttribute('range')); - - if (!range) return undefined; - - return this._indexOfCommentRange(side, range); - } - - _indexOfCommentRange(side, range) { - function rangesEqual(a, b) { - 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; - } - - return this.commentRanges.findIndex(commentRange => - commentRange.side === side && rangesEqual(commentRange.range, range)); - } - - /** - * 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). - * - * @param {Selection} selection - * @return {({ - * start: { - * node: Node, - * side: string, - * line: Number, - * column: Number - * }, - * end: { - * node: Node, - * side: string, - * line: Number, - * column: Number - * } - * })|null|!Object} - */ - _getNormalizedRange(selection) { - const rangeCount = selection.rangeCount; - if (rangeCount === 0) { - return null; - } else if (rangeCount === 1) { - return this._normalizeRange(selection.getRangeAt(0)); - } else { - const startRange = this._normalizeRange(selection.getRangeAt(0)); - const endRange = this._normalizeRange( - selection.getRangeAt(rangeCount - 1)); - return { - start: startRange.start, - end: endRange.end, - }; - } - } - - /** - * Normalize a specific DOM Range. - * - * @return {!Object} fixed normalized range - */ - _normalizeRange(domRange) { - const range = GrRangeNormalizer.normalize(domRange); - return this._fixTripleClickSelection({ - start: this._normalizeSelectionSide( - range.startContainer, range.startOffset), - end: this._normalizeSelectionSide( - range.endContainer, range.endOffset), - }, domRange); - } - - /** - * Adjust triple click selection for the whole line. - * A triple click always results in: - * - start.column == end.column == 0 - * - end.line == start.line + 1 - * - * @param {!Object} range Normalized range, ie column/line numbers - * @param {!Range} domRange DOM Range object - * @return {!Object} fixed normalized range - */ - _fixTripleClickSelection(range, domRange) { - if (!range.start) { - // Selection outside of current diff. - return range; - } - const start = range.start; - const end = range.end; - // Happens when triple click in side-by-side mode with other side empty. - const endsAtOtherEmptySide = !end && - domRange.endOffset === 0 && - domRange.endContainer.nodeName === 'TD' && - (domRange.endContainer.classList.contains('left') || - domRange.endContainer.classList.contains('right')); - const endsAtBeginningOfNextLine = end && - start.column === 0 && - end.column === 0 && - end.line === start.line + 1; - const content = domRange.cloneContents().querySelector('.contentText'); - const lineLength = content && this._getLength(content) || 0; - if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) { - // Move the selection to the end of the previous line. - range.end = { - node: start.node, - column: lineLength, - side: start.side, - line: start.line, - }; - } - return range; - } - - /** - * Convert DOM Range selection to concrete numbers (line, column, side). - * Moves range end if it's not inside td.content. - * Returns null if selection end is not valid (outside of diff). - * - * @param {Node} node td.content child - * @param {number} offset offset within node - * @return {({ - * node: Node, - * side: string, - * line: Number, - * column: Number - * }|undefined)} - */ - _normalizeSelectionSide(node, offset) { - let column; - if (!this.contains(node)) { - return; - } - const lineEl = this.diffBuilder.getLineElByChild(node); - if (!lineEl) { - return; - } - const side = this.diffBuilder.getSideByLineEl(lineEl); - if (!side) { - return; - } - const line = this.diffBuilder.getLineNumberByChild(lineEl); - if (!line) { - return; - } - const contentText = this.diffBuilder.getContentByLineEl(lineEl); - if (!contentText) { - return; - } - const contentTd = contentText.parentElement; - if (!contentTd.contains(node)) { - node = contentText; - column = 0; - } else { - const thread = contentTd.querySelector('.comment-thread'); - if (thread && thread.contains(node)) { - column = this._getLength(contentText); - node = contentText; - } else { - column = this._convertOffsetToColumn(node, offset); - } - } - - return { - node, - side, - line, - column, - }; - } - - /** - * The only line in which add a comment tooltip is cut off is the first - * line. Even if there is a collapsed section, The first visible line is - * in the position where the second line would have been, if not for the - * collapsed section, so don't need to worry about this case for - * positioning the tooltip. - */ - _positionActionBox(actionBox, startLine, range) { - if (startLine > 1) { - actionBox.placeAbove(range); - return; - } - actionBox.positionBelow = true; - actionBox.placeBelow(range); - } - - _isRangeValid(range) { - if (!range || !range.start || !range.end) { - return false; - } - const start = range.start; - const end = range.end; - if (start.side !== end.side || - end.line < start.line || - (start.line === end.line && start.column === end.column)) { - return false; - } - return true; - } - - _handleSelection(selection, isMouseUp) { - const normalizedRange = this._getNormalizedRange(selection); - if (!this._isRangeValid(normalizedRange)) { - this._removeActionBox(); - return; - } - const domRange = selection.getRangeAt(0); - const start = normalizedRange.start; - const end = normalizedRange.end; - - // TODO (viktard): Drop empty first and last lines from selection. - - // If the selection is from the end of one line to the start of the next - // line, then this must have been a double-click, or you have started - // dragging. Showing the action box is bad in the former case and not very - // useful in the latter, so never do that. - // If this was a mouse-up event, we create a comment immediately if - // the selection is from the end of a line to the start of the next line. - // In a perfect world we would only do this for double-click, but it is - // extremely rare that a user would drag from the end of one line to the - // start of the next and release the mouse, so we don't bother. - // TODO(brohlfs): This does not work, if the double-click is before a new - // diff chunk (start will be equal to end), and neither before an "expand - // the diff context" block (end line will match the first line of the new - // section and thus be greater than start line + 1). - if (start.line === end.line - 1 && end.column === 0) { - // Rather than trying to find the line contents (for comparing - // 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, { - start_line: start.line, - start_character: 0, - end_line: start.line, - end_character: start.column, - }); - } - return; - } - - let actionBox = this.shadowRoot.querySelector('gr-selection-action-box'); - if (!actionBox) { - actionBox = document.createElement('gr-selection-action-box'); - const root = Polymer.dom(this.root); - root.insertBefore(actionBox, root.firstElementChild); - } - this.selectedRange = { - range: { - start_line: start.line, - start_character: start.column, - end_line: end.line, - end_character: end.column, - }, - side: start.side, - }; - if (start.line === end.line) { - this._positionActionBox(actionBox, start.line, domRange); - } else if (start.node instanceof Text) { - if (start.column) { - this._positionActionBox(actionBox, start.line, - start.node.splitText(start.column)); - } - start.node.parentElement.normalize(); // Undo splitText from above. - } else if (start.node.classList.contains('content') && - start.node.firstChild) { - this._positionActionBox(actionBox, start.line, start.node.firstChild); - } else { - this._positionActionBox(actionBox, start.line, start.node); - } - } - - _fireCreateRangeComment(side, range) { - this.fire('create-range-comment', {side, range}); - this._removeActionBox(); - } - - _handleRangeCommentRequest(e) { - e.stopPropagation(); - if (!this.selectedRange) { - throw Error('Selected Range is needed for new range comment!'); - } - const {side, range} = this.selectedRange; - this._fireCreateRangeComment(side, range); - } - - _removeActionBox() { - this.selectedRange = undefined; - const actionBox = this.shadowRoot - .querySelector('gr-selection-action-box'); - if (actionBox) { - Polymer.dom(this.root).removeChild(actionBox); - } - } - - _convertOffsetToColumn(el, offset) { - if (el instanceof Element && el.classList.contains('content')) { - return offset; - } - while (el.previousSibling || - !el.parentElement.classList.contains('content')) { - if (el.previousSibling) { - el = el.previousSibling; - offset += this._getLength(el); - } else { - el = el.parentElement; - } - } - return offset; - } - - /** - * Traverse Element from right to left, call callback for each node. - * Stops if callback returns true. - * - * @param {!Element} startNode - * @param {function(Node):boolean} callback - * @param {Object=} opt_flags If flags.left is true, traverse left. - */ - _traverseContentSiblings(startNode, callback, opt_flags) { - const travelLeft = opt_flags && opt_flags.left; - let node = startNode; - while (node) { - if (node instanceof Element && - node.tagName !== 'HL' && - node.tagName !== 'SPAN') { - break; - } - const nextNode = travelLeft ? node.previousSibling : node.nextSibling; - if (callback(node)) { - break; - } - node = nextNode; - } - } - - /** - * Get length of a node. If the node is a content node, then only give the - * length of its .contentText child. - * - * @param {?Element} node this is sometimes passed as null. - * @return {number} - */ - _getLength(node) { - if (node instanceof Element && node.classList.contains('content')) { - return this._getLength(node.querySelector('.contentText')); - } else { - return GrAnnotation.getLength(node); - } + if (index !== undefined) { + this.set(['commentRanges', index, 'hovering'], true); } } - customElements.define(GrDiffHighlight.is, GrDiffHighlight); -})(); + _handleCommentThreadMouseleave(e) { + const threadEl = this._getThreadEl(e); + const index = this._indexForThreadEl(threadEl); + + if (index !== undefined) { + this.set(['commentRanges', index, 'hovering'], false); + } + } + + _indexForThreadEl(threadEl) { + const side = threadEl.getAttribute('comment-side'); + const range = JSON.parse(threadEl.getAttribute('range')); + + if (!range) return undefined; + + return this._indexOfCommentRange(side, range); + } + + _indexOfCommentRange(side, range) { + function rangesEqual(a, b) { + 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; + } + + return this.commentRanges.findIndex(commentRange => + commentRange.side === side && rangesEqual(commentRange.range, range)); + } + + /** + * 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). + * + * @param {Selection} selection + * @return {({ + * start: { + * node: Node, + * side: string, + * line: Number, + * column: Number + * }, + * end: { + * node: Node, + * side: string, + * line: Number, + * column: Number + * } + * })|null|!Object} + */ + _getNormalizedRange(selection) { + const rangeCount = selection.rangeCount; + if (rangeCount === 0) { + return null; + } else if (rangeCount === 1) { + return this._normalizeRange(selection.getRangeAt(0)); + } else { + const startRange = this._normalizeRange(selection.getRangeAt(0)); + const endRange = this._normalizeRange( + selection.getRangeAt(rangeCount - 1)); + return { + start: startRange.start, + end: endRange.end, + }; + } + } + + /** + * Normalize a specific DOM Range. + * + * @return {!Object} fixed normalized range + */ + _normalizeRange(domRange) { + const range = GrRangeNormalizer.normalize(domRange); + return this._fixTripleClickSelection({ + start: this._normalizeSelectionSide( + range.startContainer, range.startOffset), + end: this._normalizeSelectionSide( + range.endContainer, range.endOffset), + }, domRange); + } + + /** + * Adjust triple click selection for the whole line. + * A triple click always results in: + * - start.column == end.column == 0 + * - end.line == start.line + 1 + * + * @param {!Object} range Normalized range, ie column/line numbers + * @param {!Range} domRange DOM Range object + * @return {!Object} fixed normalized range + */ + _fixTripleClickSelection(range, domRange) { + if (!range.start) { + // Selection outside of current diff. + return range; + } + const start = range.start; + const end = range.end; + // Happens when triple click in side-by-side mode with other side empty. + const endsAtOtherEmptySide = !end && + domRange.endOffset === 0 && + domRange.endContainer.nodeName === 'TD' && + (domRange.endContainer.classList.contains('left') || + domRange.endContainer.classList.contains('right')); + const endsAtBeginningOfNextLine = end && + start.column === 0 && + end.column === 0 && + end.line === start.line + 1; + const content = domRange.cloneContents().querySelector('.contentText'); + const lineLength = content && this._getLength(content) || 0; + if (lineLength && (endsAtBeginningOfNextLine || endsAtOtherEmptySide)) { + // Move the selection to the end of the previous line. + range.end = { + node: start.node, + column: lineLength, + side: start.side, + line: start.line, + }; + } + return range; + } + + /** + * Convert DOM Range selection to concrete numbers (line, column, side). + * Moves range end if it's not inside td.content. + * Returns null if selection end is not valid (outside of diff). + * + * @param {Node} node td.content child + * @param {number} offset offset within node + * @return {({ + * node: Node, + * side: string, + * line: Number, + * column: Number + * }|undefined)} + */ + _normalizeSelectionSide(node, offset) { + let column; + if (!this.contains(node)) { + return; + } + const lineEl = this.diffBuilder.getLineElByChild(node); + if (!lineEl) { + return; + } + const side = this.diffBuilder.getSideByLineEl(lineEl); + if (!side) { + return; + } + const line = this.diffBuilder.getLineNumberByChild(lineEl); + if (!line) { + return; + } + const contentText = this.diffBuilder.getContentByLineEl(lineEl); + if (!contentText) { + return; + } + const contentTd = contentText.parentElement; + if (!contentTd.contains(node)) { + node = contentText; + column = 0; + } else { + const thread = contentTd.querySelector('.comment-thread'); + if (thread && thread.contains(node)) { + column = this._getLength(contentText); + node = contentText; + } else { + column = this._convertOffsetToColumn(node, offset); + } + } + + return { + node, + side, + line, + column, + }; + } + + /** + * The only line in which add a comment tooltip is cut off is the first + * line. Even if there is a collapsed section, The first visible line is + * in the position where the second line would have been, if not for the + * collapsed section, so don't need to worry about this case for + * positioning the tooltip. + */ + _positionActionBox(actionBox, startLine, range) { + if (startLine > 1) { + actionBox.placeAbove(range); + return; + } + actionBox.positionBelow = true; + actionBox.placeBelow(range); + } + + _isRangeValid(range) { + if (!range || !range.start || !range.end) { + return false; + } + const start = range.start; + const end = range.end; + if (start.side !== end.side || + end.line < start.line || + (start.line === end.line && start.column === end.column)) { + return false; + } + return true; + } + + _handleSelection(selection, isMouseUp) { + const normalizedRange = this._getNormalizedRange(selection); + if (!this._isRangeValid(normalizedRange)) { + this._removeActionBox(); + return; + } + const domRange = selection.getRangeAt(0); + const start = normalizedRange.start; + const end = normalizedRange.end; + + // TODO (viktard): Drop empty first and last lines from selection. + + // If the selection is from the end of one line to the start of the next + // line, then this must have been a double-click, or you have started + // dragging. Showing the action box is bad in the former case and not very + // useful in the latter, so never do that. + // If this was a mouse-up event, we create a comment immediately if + // the selection is from the end of a line to the start of the next line. + // In a perfect world we would only do this for double-click, but it is + // extremely rare that a user would drag from the end of one line to the + // start of the next and release the mouse, so we don't bother. + // TODO(brohlfs): This does not work, if the double-click is before a new + // diff chunk (start will be equal to end), and neither before an "expand + // the diff context" block (end line will match the first line of the new + // section and thus be greater than start line + 1). + if (start.line === end.line - 1 && end.column === 0) { + // Rather than trying to find the line contents (for comparing + // 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, { + start_line: start.line, + start_character: 0, + end_line: start.line, + end_character: start.column, + }); + } + return; + } + + let actionBox = this.shadowRoot.querySelector('gr-selection-action-box'); + if (!actionBox) { + actionBox = document.createElement('gr-selection-action-box'); + const root = dom(this.root); + root.insertBefore(actionBox, root.firstElementChild); + } + this.selectedRange = { + range: { + start_line: start.line, + start_character: start.column, + end_line: end.line, + end_character: end.column, + }, + side: start.side, + }; + if (start.line === end.line) { + this._positionActionBox(actionBox, start.line, domRange); + } else if (start.node instanceof Text) { + if (start.column) { + this._positionActionBox(actionBox, start.line, + start.node.splitText(start.column)); + } + start.node.parentElement.normalize(); // Undo splitText from above. + } else if (start.node.classList.contains('content') && + start.node.firstChild) { + this._positionActionBox(actionBox, start.line, start.node.firstChild); + } else { + this._positionActionBox(actionBox, start.line, start.node); + } + } + + _fireCreateRangeComment(side, range) { + this.fire('create-range-comment', {side, range}); + this._removeActionBox(); + } + + _handleRangeCommentRequest(e) { + e.stopPropagation(); + if (!this.selectedRange) { + throw Error('Selected Range is needed for new range comment!'); + } + const {side, range} = this.selectedRange; + this._fireCreateRangeComment(side, range); + } + + _removeActionBox() { + this.selectedRange = undefined; + const actionBox = this.shadowRoot + .querySelector('gr-selection-action-box'); + if (actionBox) { + dom(this.root).removeChild(actionBox); + } + } + + _convertOffsetToColumn(el, offset) { + if (el instanceof Element && el.classList.contains('content')) { + return offset; + } + while (el.previousSibling || + !el.parentElement.classList.contains('content')) { + if (el.previousSibling) { + el = el.previousSibling; + offset += this._getLength(el); + } else { + el = el.parentElement; + } + } + return offset; + } + + /** + * Traverse Element from right to left, call callback for each node. + * Stops if callback returns true. + * + * @param {!Element} startNode + * @param {function(Node):boolean} callback + * @param {Object=} opt_flags If flags.left is true, traverse left. + */ + _traverseContentSiblings(startNode, callback, opt_flags) { + const travelLeft = opt_flags && opt_flags.left; + let node = startNode; + while (node) { + if (node instanceof Element && + node.tagName !== 'HL' && + node.tagName !== 'SPAN') { + break; + } + const nextNode = travelLeft ? node.previousSibling : node.nextSibling; + if (callback(node)) { + break; + } + node = nextNode; + } + } + + /** + * Get length of a node. If the node is a content node, then only give the + * length of its .contentText child. + * + * @param {?Element} node this is sometimes passed as null. + * @return {number} + */ + _getLength(node) { + if (node instanceof Element && node.classList.contains('content')) { + return this._getLength(node.querySelector('.contentText')); + } else { + return GrAnnotation.getLength(node); + } + } +} + +customElements.define(GrDiffHighlight.is, GrDiffHighlight);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js index be72b05..10b4f2d 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-selection-action-box/gr-selection-action-box.html"> -<script src="gr-annotation.js"></script> -<script src="gr-range-normalizer.js"></script> - -<dom-module id="gr-diff-highlight"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { position: relative; @@ -39,6 +32,4 @@ <div class="contentWrapper"> <slot></slot> </div> - </template> - <script src="gr-diff-highlight.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html index 02c2033..ca1e2e2 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-highlight</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-diff-highlight.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-diff-highlight.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-highlight.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -147,486 +152,488 @@ </template> </test-fixture> -<script> - suite('gr-diff-highlight', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-highlight.js'; +suite('gr-diff-highlight', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic')[1]; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('comment events', () => { + let builder; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic')[1]; + builder = { + getContentsByLineRange: sandbox.stub().returns([]), + getLineElByChild: sandbox.stub().returns({}), + getSideByLineEl: sandbox.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('comment-side', 'right'); + threadEl.setAttribute('line-num', 3); + element.appendChild(threadEl); + element.commentRanges = [{side: 'right'}]; + + sandbox.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('comment-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, + }}]; + + sandbox.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('comment-side', 'right'); + threadEl.setAttribute('line-num', 3); + element.appendChild(threadEl); + element.commentRanges = [{side: 'right'}]; + + sandbox.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`, () => { + sandbox.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.getContentByLineEl.withArgs(lineEl).returns(contentText); + builder.getLineNumberByChild.withArgs(lineEl).returns(line); + builder.getContentByLine.withArgs(line, side).returns(contentText); + builder.getSideByLineEl.withArgs(lineEl).returns(side); + return contentText; + }; + + const emulateSelection = (startNode, startOffset, endNode, endOffset) => { + const selection = window.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: sandbox.stub(), + placeBelow: sandbox.stub(), + }); + diff = element.querySelector('#diffTable'); + builder = { + getContentByLine: sandbox.stub(), + getContentByLineEl: sandbox.stub(), + getLineElByChild, + getLineNumberByChild: sandbox.stub(), + getSideByLineEl: sandbox.stub(), + }; + element._cachedDiffBuilder = builder; }); teardown(() => { - sandbox.restore(); + contentStubs = null; + window.getSelection().removeAllRanges(); }); - suite('comment events', () => { - let builder; + test('single first line', () => { + const content = stubContent(1, 'right'); + sandbox.spy(element, '_positionActionBox'); + emulateSelection(content.firstChild, 5, content.firstChild, 12); + const actionBox = element.shadowRoot + .querySelector('gr-selection-action-box'); + assert.isTrue(actionBox.positionBelow); + }); - setup(() => { - builder = { - getContentsByLineRange: sandbox.stub().returns([]), - getLineElByChild: sandbox.stub().returns({}), - getSideByLineEl: sandbox.stub().returns('other-side'), - }; - element._cachedDiffBuilder = builder; + test('multiline starting on first line', () => { + const startContent = stubContent(1, 'right'); + const endContent = stubContent(2, 'right'); + sandbox.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'); + sandbox.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('comment-thread-mouseenter from line comments is ignored', () => { - const threadEl = document.createElement('div'); - threadEl.className = 'comment-thread'; - threadEl.setAttribute('comment-side', 'right'); - threadEl.setAttribute('line-num', 3); - element.appendChild(threadEl); - element.commentRanges = [{side: 'right'}]; - - sandbox.stub(element, 'set'); - threadEl.dispatchEvent(new CustomEvent( - 'comment-thread-mouseenter', {bubbles: true, composed: true})); - assert.isFalse(element.set.called); + test('multiline', () => { + const startContent = stubContent(119, 'right'); + const endContent = stubContent(120, 'right'); + sandbox.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('comment-thread-mouseenter from ranged comment causes set', () => { - const threadEl = document.createElement('div'); - threadEl.className = 'comment-thread'; - threadEl.setAttribute('comment-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, - }}]; + test('multiple ranges aka firefox implementation', () => { + const startContent = stubContent(119, 'right'); + const endContent = stubContent(120, 'right'); - sandbox.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); - }); + const startRange = document.createRange(); + startRange.setStart(startContent.firstChild, 10); + startRange.setEnd(startContent.firstChild, 11); - test('comment-thread-mouseleave from line comments is ignored', () => { - const threadEl = document.createElement('div'); - threadEl.className = 'comment-thread'; - threadEl.setAttribute('comment-side', 'right'); - threadEl.setAttribute('line-num', 3); - element.appendChild(threadEl); - element.commentRanges = [{side: 'right'}]; + const endRange = document.createRange(); + endRange.setStart(endContent.lastChild, 6); + endRange.setEnd(endContent.lastChild, 7); - sandbox.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`, () => { - sandbox.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); + const getRangeAtStub = sandbox.stub(); + getRangeAtStub + .onFirstCall().returns(startRange) + .onSecondCall() + .returns(endRange); + const selection = { + rangeCount: 2, + getRangeAt: getRangeAtStub, + removeAllRanges: sandbox.stub(), + }; + element._handleSelection(selection); + const {range} = element.selectedRange; + assert.deepEqual(range, { + start_line: 119, + start_character: 10, + end_line: 120, + end_character: 36, }); }); - 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.getContentByLineEl.withArgs(lineEl).returns(contentText); - builder.getLineNumberByChild.withArgs(lineEl).returns(line); - builder.getContentByLine.withArgs(line, side).returns(contentText); - builder.getSideByLineEl.withArgs(lineEl).returns(side); - return contentText; - }; - - const emulateSelection = (startNode, startOffset, endNode, endOffset) => { - const selection = window.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: sandbox.stub(), - placeBelow: sandbox.stub(), - }); - diff = element.querySelector('#diffTable'); - builder = { - getContentByLine: sandbox.stub(), - getContentByLineEl: sandbox.stub(), - getLineElByChild, - getLineNumberByChild: sandbox.stub(), - getSideByLineEl: sandbox.stub(), - }; - element._cachedDiffBuilder = builder; + 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'); + }); - teardown(() => { - contentStubs = null; - window.getSelection().removeAllRanges(); + test('collapsed', () => { + const content = stubContent(138, 'left'); + emulateSelection(content.firstChild, 5, content.firstChild, 5); + assert.isOk(window.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('single first line', () => { - const content = stubContent(1, 'right'); - sandbox.spy(element, '_positionActionBox'); - emulateSelection(content.firstChild, 5, content.firstChild, 12); - const actionBox = element.shadowRoot - .querySelector('gr-selection-action-box'); - assert.isTrue(actionBox.positionBelow); + 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('multiline starting on first line', () => { - const startContent = stubContent(1, 'right'); - const endContent = stubContent(2, 'right'); - sandbox.spy(element, '_positionActionBox'); - emulateSelection( - startContent.firstChild, 10, endContent.lastChild, 7); - const actionBox = element.shadowRoot - .querySelector('gr-selection-action-box'); - assert.isTrue(actionBox.positionBelow); + 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('single line', () => { - const content = stubContent(138, 'left'); - sandbox.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('starts outside of diff', () => { + const contentText = stubContent(140, 'left'); + const contentTd = contentText.parentElement; + + emulateSelection(contentTd.previousElementSibling, 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('multiline', () => { - const startContent = stubContent(119, 'right'); - const endContent = stubContent(120, 'right'); - sandbox.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('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('multiple ranges aka firefox implementation', () => { - const startContent = stubContent(119, 'right'); - const endContent = stubContent(120, 'right'); + 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); + }); - const startRange = document.createRange(); - startRange.setStart(startContent.firstChild, 10); - startRange.setEnd(startContent.firstChild, 11); + 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); + }); - const endRange = document.createRange(); - endRange.setStart(endContent.lastChild, 6); - endRange.setEnd(endContent.lastChild, 7); - - const getRangeAtStub = sandbox.stub(); - getRangeAtStub - .onFirstCall().returns(startRange) - .onSecondCall() - .returns(endRange); - const selection = { - rangeCount: 2, - getRangeAt: getRangeAtStub, - removeAllRanges: sandbox.stub(), - }; - element._handleSelection(selection); - const {range} = element.selectedRange; - assert.deepEqual(range, { - start_line: 119, - start_character: 10, - end_line: 120, - end_character: 36, - }); + 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('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('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('collapsed', () => { - const content = stubContent(138, 'left'); - emulateSelection(content.firstChild, 5, content.firstChild, 5); - assert.isOk(window.getSelection().getRangeAt(0).startContainer); - assert.isFalse(!!element.selectedRange); + 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('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('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 = window.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 = GrRangeNormalizer._getTextOffset(content, child); + assert.equal(result, 75); + content = stubContent(146, 'right'); + child = content.lastChild; + result = GrRangeNormalizer._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'); + }); - 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('_fixTripleClickSelection empty line', () => { + const startContent = stubContent(146, 'right'); + const endContent = stubContent(165, 'left'); + emulateSelection(startContent.firstChild, 0, + endContent.parentElement.previousElementSibling, 0); + const {range, side} = element.selectedRange; + assert.deepEqual(range, { + start_line: 146, + start_character: 0, + end_line: 146, + end_character: 84, }); - - 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.previousElementSibling, 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 = window.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 = GrRangeNormalizer._getTextOffset(content, child); - assert.equal(result, 75); - content = stubContent(146, 'right'); - child = content.lastChild; - result = GrRangeNormalizer._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'); - }); - - test('_fixTripleClickSelection empty line', () => { - const startContent = stubContent(146, 'right'); - const endContent = stubContent(165, 'left'); - emulateSelection(startContent.firstChild, 0, - endContent.parentElement.previousElementSibling, 0); - const {range, side} = element.selectedRange; - assert.deepEqual(range, { - start_line: 146, - start_character: 0, - end_line: 146, - end_character: 84, - }); - assert.equal(side, 'right'); - }); + assert.equal(side, 'right'); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js index a44e366..26a3a40 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.js
@@ -14,1097 +14,1112 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const MSG_EMPTY_BLAME = 'No blame information for this diff.'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-comment-thread/gr-comment-thread.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import '../gr-diff/gr-diff.js'; +import '../gr-syntax-layer/gr-syntax-layer.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-host_html.js'; - const EVENT_AGAINST_PARENT = 'diff-against-parent'; - const EVENT_ZERO_REBASE = 'rebase-percent-zero'; - const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero'; +const MSG_EMPTY_BLAME = 'No blame information for this diff.'; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +const EVENT_AGAINST_PARENT = 'diff-against-parent'; +const EVENT_ZERO_REBASE = 'rebase-percent-zero'; +const EVENT_NONZERO_REBASE = 'rebase-percent-nonzero'; - /** @enum {string} */ - const TimingLabel = { - TOTAL: 'Diff Total Render', - CONTENT: 'Diff Content Render', - SYNTAX: 'Diff Syntax Render', - }; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; - // Disable syntax highlighting if the overall diff is too large. - const SYNTAX_MAX_DIFF_LENGTH = 20000; +/** @enum {string} */ +const TimingLabel = { + TOTAL: 'Diff Total Render', + CONTENT: 'Diff Content Render', + SYNTAX: 'Diff Syntax Render', +}; - // If any line of the diff is more than the character limit, then disable - // syntax highlighting for the entire file. - const SYNTAX_MAX_LINE_LENGTH = 500; +// Disable syntax highlighting if the overall diff is too large. +const SYNTAX_MAX_DIFF_LENGTH = 20000; - // 120 lines is good enough threshold for full-sized window viewport - const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120; +// If any line of the diff is more than the character limit, then disable +// syntax highlighting for the entire file. +const SYNTAX_MAX_LINE_LENGTH = 500; - const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE'; +// 120 lines is good enough threshold for full-sized window viewport +const NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT = 120; + +const WHITESPACE_IGNORE_NONE = 'IGNORE_NONE'; + +/** + * @param {Object} diff + * @return {boolean} + */ +function isImageDiff(diff) { + if (!diff) { return false; } + + const isA = diff.meta_a && + diff.meta_a.content_type.startsWith('image/'); + const isB = diff.meta_b && + diff.meta_b.content_type.startsWith('image/'); + + return !!(diff.binary && (isA || isB)); +} + +/** @enum {string} */ +Gerrit.DiffSide = { + LEFT: 'left', + RIGHT: 'right', +}; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + */ +/** + * Wrapper around gr-diff. + * + * Webcomponent fetching diffs and related data from restAPI and passing them + * to the presentational gr-diff for rendering. + * + * @extends Polymer.Element + */ +class GrDiffHost extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-host'; } + /** + * Fired when the user selects a line. + * + * @event line-selected + */ + + /** + * Fired if being logged in is required. + * + * @event show-auth-required + */ + + /** + * Fired when a comment is saved or discarded + * + * @event diff-comments-modified + */ + + static get properties() { + return { + changeNum: String, + noAutoRender: { + type: Boolean, + value: false, + }, + /** @type {?} */ + patchRange: Object, + path: String, + prefs: { + type: Object, + }, + projectName: String, + displayLine: { + type: Boolean, + value: false, + }, + isImageDiff: { + type: Boolean, + computed: '_computeIsImageDiff(diff)', + notify: true, + }, + commitRange: Object, + filesWeblinks: { + type: Object, + value() { + return {}; + }, + notify: true, + }, + hidden: { + type: Boolean, + reflectToAttribute: true, + }, + noRenderOnPrefsChange: { + type: Boolean, + value: false, + }, + comments: { + type: Object, + observer: '_commentsChanged', + }, + lineWrapping: { + type: Boolean, + value: false, + }, + viewMode: { + type: String, + value: DiffViewMode.SIDE_BY_SIDE, + }, + + /** + * Special line number which should not be collapsed into a shared region. + * + * @type {{ + * number: number, + * leftSide: {boolean} + * }|null} + */ + lineOfInterest: Object, + + /** + * If the diff fails to load, show the failure message in the diff rather + * than bubbling the error up to the whole page. This is useful for when + * loading inline diffs because one diff failing need not mark the whole + * page with a failure. + */ + showLoadFailure: Boolean, + + isBlameLoaded: { + type: Boolean, + notify: true, + computed: '_computeIsBlameLoaded(_blame)', + }, + + _loggedIn: { + type: Boolean, + value: false, + }, + + _loading: { + type: Boolean, + value: false, + }, + + /** @type {?string} */ + _errorMessage: { + type: String, + value: null, + }, + + /** @type {?Object} */ + _baseImage: Object, + /** @type {?Object} */ + _revisionImage: Object, + /** + * This is a DiffInfo object. + */ + diff: { + type: Object, + notify: true, + }, + + /** @type {?Object} */ + _blame: { + type: Object, + value: null, + }, + + /** + * @type {!Array<!Gerrit.CoverageRange>} + */ + _coverageRanges: { + type: Array, + value: () => [], + }, + + _loadedWhitespaceLevel: String, + + _parentIndex: { + type: Number, + computed: '_computeParentIndex(patchRange.*)', + }, + + _syntaxHighlightingEnabled: { + type: Boolean, + computed: + '_isSyntaxHighlightingEnabled(prefs.*, diff)', + }, + + _layers: { + type: Array, + value: [], + }, + }; + } + + static get observers() { + return [ + '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' + + ' noRenderOnPrefsChange)', + '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)', + ]; + } + + /** @override */ + created() { + super.created(); + this.addEventListener( + // These are named inconsistently for a reason: + // The create-comment event is fired to indicate that we should + // create a comment. + // The comment-* events are just notifying that the comments did already + // change in some way, and that we should update any models we may want + // to keep in sync. + 'create-comment', + e => this._handleCreateComment(e)); + this.addEventListener('comment-discard', + e => this._handleCommentDiscard(e)); + this.addEventListener('comment-update', + e => this._handleCommentUpdate(e)); + this.addEventListener('comment-save', + e => this._handleCommentSave(e)); + this.addEventListener('render-start', + () => this._handleRenderStart()); + this.addEventListener('render-content', + () => this._handleRenderContent()); + this.addEventListener('normalize-range', + event => this._handleNormalizeRange(event)); + this.addEventListener('diff-context-expanded', + event => this._handleDiffContextExpanded(event)); + } + + /** @override */ + ready() { + super.ready(); + if (this._canReload()) { + this.reload(); + } + } + + /** @override */ + attached() { + super.attached(); + this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + }); + } + + /** + * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a + * signal to report metrics event that started on location change. + * @return {!Promise} + **/ + reload(shouldReportMetric) { + this._loading = true; + this._errorMessage = null; + const whitespaceLevel = this._getIgnoreWhitespace(); + + const layers = [this.$.syntaxLayer]; + // Get layers from plugins (if any). + for (const pluginLayer of this.$.jsAPI.getDiffLayers( + this.path, this.changeNum, this.patchNum)) { + layers.push(pluginLayer); + } + this._layers = layers; + + if (shouldReportMetric) { + // We listen on render viewport only on DiffPage (on paramsChanged) + this._listenToViewportRender(); + } + + this._coverageRanges = []; + this._getCoverageData(); + const diffRequest = this._getDiff() + .then(diff => { + this._loadedWhitespaceLevel = whitespaceLevel; + this._reportDiff(diff); + return diff; + }) + .catch(e => { + this._handleGetDiffError(e); + return null; + }); + + const assetRequest = diffRequest.then(diff => { + // If the diff is null, then it's failed to load. + if (!diff) { return null; } + + return this._loadDiffAssets(diff); + }); + + // Not waiting for coverage ranges intentionally as + // plugin loading should not block the content rendering + return Promise.all([diffRequest, assetRequest]) + .then(results => { + const diff = results[0]; + if (!diff) { + return Promise.resolve(); + } + this.filesWeblinks = this._getFilesWeblinks(diff); + return new Promise(resolve => { + const callback = event => { + const needsSyntaxHighlighting = event.detail && + event.detail.contentRendered; + if (needsSyntaxHighlighting) { + this.$.reporting.time(TimingLabel.SYNTAX); + this.$.syntaxLayer.process().then(() => { + this.$.reporting.timeEnd(TimingLabel.SYNTAX); + this.$.reporting.timeEnd(TimingLabel.TOTAL); + resolve(); + }); + } else { + this.$.reporting.timeEnd(TimingLabel.TOTAL); + resolve(); + } + this.removeEventListener('render', callback); + if (shouldReportMetric) { + // We report diffViewContentDisplayed only on reload caused + // by params changed - expected only on Diff Page. + this.$.reporting.diffViewContentDisplayed(); + } + }; + this.addEventListener('render', callback); + this.diff = diff; + }); + }) + .catch(err => { + console.warn('Error encountered loading diff:', err); + }) + .then(() => { this._loading = false; }); + } + + _getCoverageData() { + const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this; + this.$.jsAPI.getCoverageAnnotationApi(). + then(coverageAnnotationApi => { + if (!coverageAnnotationApi) return; + const provider = coverageAnnotationApi.getCoverageProvider(); + return provider(changeNum, path, basePatchNum, patchNum) + .then(coverageRanges => { + if (!coverageRanges || + changeNum !== this.changeNum || + path !== this.path || + basePatchNum !== this.patchRange.basePatchNum || + patchNum !== this.patchRange.patchNum) { + return; + } + + const existingCoverageRanges = this._coverageRanges; + this._coverageRanges = coverageRanges; + + // Notify with existing coverage ranges + // in case there is some existing coverage data that needs to be removed + existingCoverageRanges.forEach(range => { + coverageAnnotationApi.notify( + path, + range.code_range.start_line, + range.code_range.end_line, + range.side); + }); + + // Notify with new coverage data + coverageRanges.forEach(range => { + coverageAnnotationApi.notify( + path, + range.code_range.start_line, + range.code_range.end_line, + range.side); + }); + }); + }) + .catch(err => { + console.warn('Loading coverage ranges failed: ', err); + }); + } + + _getFilesWeblinks(diff) { + if (!this.commitRange) { + return {}; + } + return { + meta_a: Gerrit.Nav.getFileWebLinks( + this.projectName, this.commitRange.baseCommit, this.path, + {weblinks: diff && diff.meta_a && diff.meta_a.web_links}), + meta_b: Gerrit.Nav.getFileWebLinks( + this.projectName, this.commitRange.commit, this.path, + {weblinks: diff && diff.meta_b && diff.meta_b.web_links}), + }; + } + + /** Cancel any remaining diff builder rendering work. */ + cancel() { + this.$.diff.cancel(); + } + + /** @return {!Array<!HTMLElement>} */ + getCursorStops() { + return this.$.diff.getCursorStops(); + } + + /** @return {boolean} */ + isRangeSelected() { + return this.$.diff.isRangeSelected(); + } + + createRangeComment() { + return this.$.diff.createRangeComment(); + } + + toggleLeftDiff() { + this.$.diff.toggleLeftDiff(); + } + + /** + * Load and display blame information for the base of the diff. + * + * @return {Promise} A promise that resolves when blame finishes rendering. + */ + loadBlame() { + return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum, + this.path, true) + .then(blame => { + if (!blame.length) { + this.fire('show-alert', {message: MSG_EMPTY_BLAME}); + return Promise.reject(MSG_EMPTY_BLAME); + } + + this._blame = blame; + }); + } + + /** Unload blame information for the diff. */ + clearBlame() { + this._blame = null; + } + + /** + * The thread elements in this diff, in no particular order. + * + * @return {!Array<!HTMLElement>} + */ + getThreadEls() { + return Array.from( + dom(this.$.diff).querySelectorAll('.comment-thread')); + } + + /** @param {HTMLElement} el */ + addDraftAtLine(el) { + this.$.diff.addDraftAtLine(el); + } + + clearDiffContent() { + this.$.diff.clearDiffContent(); + } + + expandAllContext() { + this.$.diff.expandAllContext(); + } + + /** @return {!Promise} */ + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + /** @return {boolean}} */ + _canReload() { + return !!this.changeNum && !!this.patchRange && !!this.path && + !this.noAutoRender; + } + + /** @return {!Promise<!Object>} */ + _getDiff() { + // Wrap the diff request in a new promise so that the error handler + // rejects the promise, allowing the error to be handled in the .catch. + return new Promise((resolve, reject) => { + this.$.restAPI.getDiff( + this.changeNum, + this.patchRange.basePatchNum, + this.patchRange.patchNum, + this.path, + this._getIgnoreWhitespace(), + reject) + .then(resolve); + }); + } + + _handleGetDiffError(response) { + // Loading the diff may respond with 409 if the file is too large. In this + // case, use a toast error.. + if (response.status === 409) { + this.fire('server-error', {response}); + return; + } + + if (this.showLoadFailure) { + this._errorMessage = [ + 'Encountered error when loading the diff:', + response.status, + response.statusText, + ].join(' '); + return; + } + + this.fire('page-error', {response}); + } + + /** + * Report info about the diff response. + */ + _reportDiff(diff) { + if (!diff || !diff.content) { + return; + } + + // Count the delta lines stemming from normal deltas, and from + // due_to_rebase deltas. + let nonRebaseDelta = 0; + let rebaseDelta = 0; + diff.content.forEach(chunk => { + if (chunk.ab) { return; } + const deltaSize = Math.max( + chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0); + if (chunk.due_to_rebase) { + rebaseDelta += deltaSize; + } else { + nonRebaseDelta += deltaSize; + } + }); + + // Find the percent of the delta from due_to_rebase chunks rounded to two + // digits. Diffs with no delta are considered 0%. + const totalDelta = rebaseDelta + nonRebaseDelta; + const percentRebaseDelta = !totalDelta ? 0 : + Math.round(100 * rebaseDelta / totalDelta); + + // Report the due_to_rebase percentage in the "diff" category when + // applicable. + if (this.patchRange.basePatchNum === 'PARENT') { + this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT); + } else if (percentRebaseDelta === 0) { + this.$.reporting.reportInteraction(EVENT_ZERO_REBASE); + } else { + this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE, + {percentRebaseDelta}); + } + } + + /** + * @param {Object} diff + * @return {!Promise} + */ + _loadDiffAssets(diff) { + if (isImageDiff(diff)) { + return this._getImages(diff).then(images => { + this._baseImage = images.baseImage; + this._revisionImage = images.revisionImage; + }); + } else { + this._baseImage = null; + this._revisionImage = null; + return Promise.resolve(); + } + } /** * @param {Object} diff * @return {boolean} */ - function isImageDiff(diff) { - if (!diff) { return false; } - - const isA = diff.meta_a && - diff.meta_a.content_type.startsWith('image/'); - const isB = diff.meta_b && - diff.meta_b.content_type.startsWith('image/'); - - return !!(diff.binary && (isA || isB)); + _computeIsImageDiff(diff) { + return isImageDiff(diff); } - /** @enum {string} */ - Gerrit.DiffSide = { - LEFT: 'left', - RIGHT: 'right', - }; + _commentsChanged(newComments) { + const allComments = []; + for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) { + // This is needed by the threading. + for (const comment of newComments[side]) { + comment.__commentSide = side; + } + allComments.push(...newComments[side]); + } + // Currently, the only way this is ever changed here is when the initial + // comments are loaded, so it's okay performance wise to clear the threads + // and recreate them. If this changes in future, we might want to reuse + // some DOM nodes here. + this._clearThreads(); + const threads = this._createThreads(allComments); + for (const thread of threads) { + const threadEl = this._createThreadElement(thread); + this._attachThreadElement(threadEl); + } + } + + _sortComments(comments) { + return comments.slice(0).sort((a, b) => { + if (b.__draft && !a.__draft ) { return -1; } + if (a.__draft && !b.__draft ) { return 1; } + return util.parseDate(a.updated) - util.parseDate(b.updated); + }); + } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin + * @param {!Array<!Object>} comments + * @return {!Array<!Object>} Threads for the given comments. */ + _createThreads(comments) { + const sortedComments = this._sortComments(comments); + const threads = []; + for (const comment of sortedComments) { + // If the comment is in reply to another comment, find that comment's + // thread and append to it. + if (comment.in_reply_to) { + const thread = threads.find(thread => + thread.comments.some(c => c.id === comment.in_reply_to)); + if (thread) { + thread.comments.push(comment); + continue; + } + } + + // Otherwise, this comment starts its own thread. + const newThread = { + start_datetime: comment.updated, + comments: [comment], + commentSide: comment.__commentSide, + patchNum: comment.patch_set, + rootId: comment.id || comment.__draftID, + lineNum: comment.line, + isOnParent: comment.side === 'PARENT', + }; + if (comment.range) { + newThread.range = Object.assign({}, comment.range); + } + threads.push(newThread); + } + return threads; + } + /** - * Wrapper around gr-diff. - * - * Webcomponent fetching diffs and related data from restAPI and passing them - * to the presentational gr-diff for rendering. - * - * @extends Polymer.Element + * @param {Object} blame + * @return {boolean} */ - class GrDiffHost extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff-host'; } - /** - * Fired when the user selects a line. - * - * @event line-selected - */ + _computeIsBlameLoaded(blame) { + return !!blame; + } - /** - * Fired if being logged in is required. - * - * @event show-auth-required - */ + /** + * @param {Object} diff + * @return {!Promise} + */ + _getImages(diff) { + return this.$.restAPI.getImagesForDiff(this.changeNum, diff, + this.patchRange); + } - /** - * Fired when a comment is saved or discarded - * - * @event diff-comments-modified - */ + /** @param {CustomEvent} e */ + _handleCreateComment(e) { + const {lineNum, side, patchNum, isOnParent, range} = e.detail; + const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range, + isOnParent); + threadEl.addOrEditDraft(lineNum, range); - static get properties() { - return { - changeNum: String, - noAutoRender: { - type: Boolean, - value: false, - }, - /** @type {?} */ - patchRange: Object, - path: String, - prefs: { - type: Object, - }, - projectName: String, - displayLine: { - type: Boolean, - value: false, - }, - isImageDiff: { - type: Boolean, - computed: '_computeIsImageDiff(diff)', - notify: true, - }, - commitRange: Object, - filesWeblinks: { - type: Object, - value() { - return {}; - }, - notify: true, - }, - hidden: { - type: Boolean, - reflectToAttribute: true, - }, - noRenderOnPrefsChange: { - type: Boolean, - value: false, - }, - comments: { - type: Object, - observer: '_commentsChanged', - }, - lineWrapping: { - type: Boolean, - value: false, - }, - viewMode: { - type: String, - value: DiffViewMode.SIDE_BY_SIDE, - }, + this.$.reporting.recordDraftInteraction(); + } - /** - * Special line number which should not be collapsed into a shared region. - * - * @type {{ - * number: number, - * leftSide: {boolean} - * }|null} - */ - lineOfInterest: Object, - - /** - * If the diff fails to load, show the failure message in the diff rather - * than bubbling the error up to the whole page. This is useful for when - * loading inline diffs because one diff failing need not mark the whole - * page with a failure. - */ - showLoadFailure: Boolean, - - isBlameLoaded: { - type: Boolean, - notify: true, - computed: '_computeIsBlameLoaded(_blame)', - }, - - _loggedIn: { - type: Boolean, - value: false, - }, - - _loading: { - type: Boolean, - value: false, - }, - - /** @type {?string} */ - _errorMessage: { - type: String, - value: null, - }, - - /** @type {?Object} */ - _baseImage: Object, - /** @type {?Object} */ - _revisionImage: Object, - /** - * This is a DiffInfo object. - */ - diff: { - type: Object, - notify: true, - }, - - /** @type {?Object} */ - _blame: { - type: Object, - value: null, - }, - - /** - * @type {!Array<!Gerrit.CoverageRange>} - */ - _coverageRanges: { - type: Array, - value: () => [], - }, - - _loadedWhitespaceLevel: String, - - _parentIndex: { - type: Number, - computed: '_computeParentIndex(patchRange.*)', - }, - - _syntaxHighlightingEnabled: { - type: Boolean, - computed: - '_isSyntaxHighlightingEnabled(prefs.*, diff)', - }, - - _layers: { - type: Array, - value: [], - }, - }; - } - - static get observers() { - return [ - '_whitespaceChanged(prefs.ignore_whitespace, _loadedWhitespaceLevel,' + - ' noRenderOnPrefsChange)', - '_syntaxHighlightingChanged(noRenderOnPrefsChange, prefs.*)', - ]; - } - - /** @override */ - created() { - super.created(); - this.addEventListener( - // These are named inconsistently for a reason: - // The create-comment event is fired to indicate that we should - // create a comment. - // The comment-* events are just notifying that the comments did already - // change in some way, and that we should update any models we may want - // to keep in sync. - 'create-comment', - e => this._handleCreateComment(e)); - this.addEventListener('comment-discard', - e => this._handleCommentDiscard(e)); - this.addEventListener('comment-update', - e => this._handleCommentUpdate(e)); - this.addEventListener('comment-save', - e => this._handleCommentSave(e)); - this.addEventListener('render-start', - () => this._handleRenderStart()); - this.addEventListener('render-content', - () => this._handleRenderContent()); - this.addEventListener('normalize-range', - event => this._handleNormalizeRange(event)); - this.addEventListener('diff-context-expanded', - event => this._handleDiffContextExpanded(event)); - } - - /** @override */ - ready() { - super.ready(); - if (this._canReload()) { - this.reload(); - } - } - - /** @override */ - attached() { - super.attached(); - this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; + /** + * Gets or creates a comment thread at a given location. + * May provide a range, to get/create a range comment. + * + * @param {string} patchNum + * @param {?number} lineNum + * @param {string} commentSide + * @param {Gerrit.Range|undefined} range + * @param {boolean} isOnParent + * @return {!Object} + */ + _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) { + let threadEl = this._getThreadEl(lineNum, commentSide, range); + if (!threadEl) { + threadEl = this._createThreadElement({ + comments: [], + commentSide, + patchNum, + lineNum, + range, + isOnParent, }); + this._attachThreadElement(threadEl); + } + return threadEl; + } + + _attachThreadElement(threadEl) { + dom(this.$.diff).appendChild(threadEl); + } + + _clearThreads() { + for (const threadEl of this.getThreadEls()) { + const parent = dom(threadEl).parentNode; + dom(parent).removeChild(threadEl); + } + } + + _createThreadElement(thread) { + const threadEl = document.createElement('gr-comment-thread'); + threadEl.className = 'comment-thread'; + threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`); + threadEl.comments = thread.comments; + threadEl.commentSide = thread.commentSide; + threadEl.isOnParent = !!thread.isOnParent; + threadEl.parentIndex = this._parentIndex; + threadEl.changeNum = this.changeNum; + threadEl.patchNum = thread.patchNum; + threadEl.lineNum = thread.lineNum; + const rootIdChangedListener = changeEvent => { + thread.rootId = changeEvent.detail.value; + }; + threadEl.addEventListener('root-id-changed', rootIdChangedListener); + threadEl.path = this.path; + threadEl.projectName = this.projectName; + threadEl.range = thread.range; + const threadDiscardListener = e => { + const threadEl = /** @type {!Node} */ (e.currentTarget); + + const parent = dom(threadEl).parentNode; + dom(parent).removeChild(threadEl); + + threadEl.removeEventListener('root-id-changed', rootIdChangedListener); + threadEl.removeEventListener('thread-discard', threadDiscardListener); + }; + threadEl.addEventListener('thread-discard', threadDiscardListener); + return threadEl; + } + + /** + * Gets a comment thread element at a given location. + * May provide a range, to get a range comment. + * + * @param {?number} lineNum + * @param {string} commentSide + * @param {!Gerrit.Range=} range + * @return {?Node} + */ + _getThreadEl(lineNum, commentSide, range = undefined) { + let line; + if (commentSide === GrDiffBuilder.Side.LEFT) { + line = {beforeNumber: lineNum}; + } else if (commentSide === GrDiffBuilder.Side.RIGHT) { + line = {afterNumber: lineNum}; + } else { + throw new Error(`Unknown side: ${commentSide}`); + } + function matchesRange(threadEl) { + const threadRange = /** @type {!Gerrit.Range} */( + JSON.parse(threadEl.getAttribute('range'))); + return Gerrit.rangesEqual(threadRange, range); } - /** - * @param {boolean=} shouldReportMetric indicate a new Diff Page. This is a - * signal to report metrics event that started on location change. - * @return {!Promise} - **/ - reload(shouldReportMetric) { - this._loading = true; - this._errorMessage = null; - const whitespaceLevel = this._getIgnoreWhitespace(); + const filteredThreadEls = this._filterThreadElsForLocation( + this.getThreadEls(), line, commentSide).filter(matchesRange); + return filteredThreadEls.length ? filteredThreadEls[0] : null; + } - const layers = [this.$.syntaxLayer]; - // Get layers from plugins (if any). - for (const pluginLayer of this.$.jsAPI.getDiffLayers( - this.path, this.changeNum, this.patchNum)) { - layers.push(pluginLayer); - } - this._layers = layers; - - if (shouldReportMetric) { - // We listen on render viewport only on DiffPage (on paramsChanged) - this._listenToViewportRender(); - } - - this._coverageRanges = []; - this._getCoverageData(); - const diffRequest = this._getDiff() - .then(diff => { - this._loadedWhitespaceLevel = whitespaceLevel; - this._reportDiff(diff); - return diff; - }) - .catch(e => { - this._handleGetDiffError(e); - return null; - }); - - const assetRequest = diffRequest.then(diff => { - // If the diff is null, then it's failed to load. - if (!diff) { return null; } - - return this._loadDiffAssets(diff); - }); - - // Not waiting for coverage ranges intentionally as - // plugin loading should not block the content rendering - return Promise.all([diffRequest, assetRequest]) - .then(results => { - const diff = results[0]; - if (!diff) { - return Promise.resolve(); - } - this.filesWeblinks = this._getFilesWeblinks(diff); - return new Promise(resolve => { - const callback = event => { - const needsSyntaxHighlighting = event.detail && - event.detail.contentRendered; - if (needsSyntaxHighlighting) { - this.$.reporting.time(TimingLabel.SYNTAX); - this.$.syntaxLayer.process().then(() => { - this.$.reporting.timeEnd(TimingLabel.SYNTAX); - this.$.reporting.timeEnd(TimingLabel.TOTAL); - resolve(); - }); - } else { - this.$.reporting.timeEnd(TimingLabel.TOTAL); - resolve(); - } - this.removeEventListener('render', callback); - if (shouldReportMetric) { - // We report diffViewContentDisplayed only on reload caused - // by params changed - expected only on Diff Page. - this.$.reporting.diffViewContentDisplayed(); - } - }; - this.addEventListener('render', callback); - this.diff = diff; - }); - }) - .catch(err => { - console.warn('Error encountered loading diff:', err); - }) - .then(() => { this._loading = false; }); + /** + * @param {!Array<!HTMLElement>} threadEls + * @param {!{beforeNumber: (number|string|undefined|null), + * afterNumber: (number|string|undefined|null)}} + * lineInfo + * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for + * which to return the threads. + * @return {!Array<!HTMLElement>} The thread elements matching the given + * location. + */ + _filterThreadElsForLocation(threadEls, lineInfo, side) { + function matchesLeftLine(threadEl) { + return threadEl.getAttribute('comment-side') == + Gerrit.DiffSide.LEFT && + threadEl.getAttribute('line-num') == lineInfo.beforeNumber; + } + function matchesRightLine(threadEl) { + return threadEl.getAttribute('comment-side') == + Gerrit.DiffSide.RIGHT && + threadEl.getAttribute('line-num') == lineInfo.afterNumber; + } + function matchesFileComment(threadEl) { + return threadEl.getAttribute('comment-side') == side && + // line/range comments have 1-based line set, if line is falsy it's + // a file comment + !threadEl.getAttribute('line-num'); } - _getCoverageData() { - const {changeNum, path, patchRange: {basePatchNum, patchNum}} = this; - this.$.jsAPI.getCoverageAnnotationApi(). - then(coverageAnnotationApi => { - if (!coverageAnnotationApi) return; - const provider = coverageAnnotationApi.getCoverageProvider(); - return provider(changeNum, path, basePatchNum, patchNum) - .then(coverageRanges => { - if (!coverageRanges || - changeNum !== this.changeNum || - path !== this.path || - basePatchNum !== this.patchRange.basePatchNum || - patchNum !== this.patchRange.patchNum) { - return; - } + // Select the appropriate matchers for the desired side and line + // If side is BOTH, we want both the left and right matcher. + const matchers = []; + if (side !== Gerrit.DiffSide.RIGHT) { + matchers.push(matchesLeftLine); + } + if (side !== Gerrit.DiffSide.LEFT) { + matchers.push(matchesRightLine); + } + if (lineInfo.afterNumber === 'FILE' || + lineInfo.beforeNumber === 'FILE') { + matchers.push(matchesFileComment); + } + return threadEls.filter(threadEl => + matchers.some(matcher => matcher(threadEl))); + } - const existingCoverageRanges = this._coverageRanges; - this._coverageRanges = coverageRanges; + _getIgnoreWhitespace() { + if (!this.prefs || !this.prefs.ignore_whitespace) { + return WHITESPACE_IGNORE_NONE; + } + return this.prefs.ignore_whitespace; + } - // Notify with existing coverage ranges - // in case there is some existing coverage data that needs to be removed - existingCoverageRanges.forEach(range => { - coverageAnnotationApi.notify( - path, - range.code_range.start_line, - range.code_range.end_line, - range.side); - }); - - // Notify with new coverage data - coverageRanges.forEach(range => { - coverageAnnotationApi.notify( - path, - range.code_range.start_line, - range.code_range.end_line, - range.side); - }); - }); - }) - .catch(err => { - console.warn('Loading coverage ranges failed: ', err); - }); + _whitespaceChanged( + preferredWhitespaceLevel, loadedWhitespaceLevel, + noRenderOnPrefsChange) { + // Polymer 2: check for undefined + if ([ + preferredWhitespaceLevel, + loadedWhitespaceLevel, + noRenderOnPrefsChange, + ].some(arg => arg === undefined)) { + return; } - _getFilesWeblinks(diff) { - if (!this.commitRange) { - return {}; - } - return { - meta_a: Gerrit.Nav.getFileWebLinks( - this.projectName, this.commitRange.baseCommit, this.path, - {weblinks: diff && diff.meta_a && diff.meta_a.web_links}), - meta_b: Gerrit.Nav.getFileWebLinks( - this.projectName, this.commitRange.commit, this.path, - {weblinks: diff && diff.meta_b && diff.meta_b.web_links}), - }; + if (preferredWhitespaceLevel !== loadedWhitespaceLevel && + !noRenderOnPrefsChange) { + this.reload(); + } + } + + _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) { + // Polymer 2: check for undefined + if ([ + noRenderOnPrefsChange, + prefsChangeRecord, + ].some(arg => arg === undefined)) { + return; } - /** Cancel any remaining diff builder rendering work. */ - cancel() { - this.$.diff.cancel(); + if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') { + return; } - /** @return {!Array<!HTMLElement>} */ - getCursorStops() { - return this.$.diff.getCursorStops(); + if (!noRenderOnPrefsChange) { + this.reload(); } + } - /** @return {boolean} */ - isRangeSelected() { - return this.$.diff.isRangeSelected(); + /** + * @param {Object} patchRangeRecord + * @return {number|null} + */ + _computeParentIndex(patchRangeRecord) { + return this.isMergeParent(patchRangeRecord.base.basePatchNum) ? + this.getParentIndex(patchRangeRecord.base.basePatchNum) : null; + } + + _handleCommentSave(e) { + const comment = e.detail.comment; + const side = e.detail.comment.__commentSide; + const idx = this._findDraftIndex(comment, side); + this.set(['comments', side, idx], comment); + this._handleCommentSaveOrDiscard(); + } + + _handleCommentDiscard(e) { + const comment = e.detail.comment; + this._removeComment(comment); + this._handleCommentSaveOrDiscard(); + } + + /** + * Closure annotation for Polymer.prototype.push is off. Submitted PR: + * https://github.com/Polymer/polymer/pull/4776 + * but for not supressing annotations. + * + * @suppress {checkTypes} + */ + _handleCommentUpdate(e) { + const comment = e.detail.comment; + const side = e.detail.comment.__commentSide; + let idx = this._findCommentIndex(comment, side); + if (idx === -1) { + idx = this._findDraftIndex(comment, side); } - - createRangeComment() { - return this.$.diff.createRangeComment(); - } - - toggleLeftDiff() { - this.$.diff.toggleLeftDiff(); - } - - /** - * Load and display blame information for the base of the diff. - * - * @return {Promise} A promise that resolves when blame finishes rendering. - */ - loadBlame() { - return this.$.restAPI.getBlame(this.changeNum, this.patchRange.patchNum, - this.path, true) - .then(blame => { - if (!blame.length) { - this.fire('show-alert', {message: MSG_EMPTY_BLAME}); - return Promise.reject(MSG_EMPTY_BLAME); - } - - this._blame = blame; - }); - } - - /** Unload blame information for the diff. */ - clearBlame() { - this._blame = null; - } - - /** - * The thread elements in this diff, in no particular order. - * - * @return {!Array<!HTMLElement>} - */ - getThreadEls() { - return Array.from( - Polymer.dom(this.$.diff).querySelectorAll('.comment-thread')); - } - - /** @param {HTMLElement} el */ - addDraftAtLine(el) { - this.$.diff.addDraftAtLine(el); - } - - clearDiffContent() { - this.$.diff.clearDiffContent(); - } - - expandAllContext() { - this.$.diff.expandAllContext(); - } - - /** @return {!Promise} */ - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - /** @return {boolean}} */ - _canReload() { - return !!this.changeNum && !!this.patchRange && !!this.path && - !this.noAutoRender; - } - - /** @return {!Promise<!Object>} */ - _getDiff() { - // Wrap the diff request in a new promise so that the error handler - // rejects the promise, allowing the error to be handled in the .catch. - return new Promise((resolve, reject) => { - this.$.restAPI.getDiff( - this.changeNum, - this.patchRange.basePatchNum, - this.patchRange.patchNum, - this.path, - this._getIgnoreWhitespace(), - reject) - .then(resolve); - }); - } - - _handleGetDiffError(response) { - // Loading the diff may respond with 409 if the file is too large. In this - // case, use a toast error.. - if (response.status === 409) { - this.fire('server-error', {response}); - return; - } - - if (this.showLoadFailure) { - this._errorMessage = [ - 'Encountered error when loading the diff:', - response.status, - response.statusText, - ].join(' '); - return; - } - - this.fire('page-error', {response}); - } - - /** - * Report info about the diff response. - */ - _reportDiff(diff) { - if (!diff || !diff.content) { - return; - } - - // Count the delta lines stemming from normal deltas, and from - // due_to_rebase deltas. - let nonRebaseDelta = 0; - let rebaseDelta = 0; - diff.content.forEach(chunk => { - if (chunk.ab) { return; } - const deltaSize = Math.max( - chunk.a ? chunk.a.length : 0, chunk.b ? chunk.b.length : 0); - if (chunk.due_to_rebase) { - rebaseDelta += deltaSize; - } else { - nonRebaseDelta += deltaSize; - } - }); - - // Find the percent of the delta from due_to_rebase chunks rounded to two - // digits. Diffs with no delta are considered 0%. - const totalDelta = rebaseDelta + nonRebaseDelta; - const percentRebaseDelta = !totalDelta ? 0 : - Math.round(100 * rebaseDelta / totalDelta); - - // Report the due_to_rebase percentage in the "diff" category when - // applicable. - if (this.patchRange.basePatchNum === 'PARENT') { - this.$.reporting.reportInteraction(EVENT_AGAINST_PARENT); - } else if (percentRebaseDelta === 0) { - this.$.reporting.reportInteraction(EVENT_ZERO_REBASE); - } else { - this.$.reporting.reportInteraction(EVENT_NONZERO_REBASE, - {percentRebaseDelta}); - } - } - - /** - * @param {Object} diff - * @return {!Promise} - */ - _loadDiffAssets(diff) { - if (isImageDiff(diff)) { - return this._getImages(diff).then(images => { - this._baseImage = images.baseImage; - this._revisionImage = images.revisionImage; - }); - } else { - this._baseImage = null; - this._revisionImage = null; - return Promise.resolve(); - } - } - - /** - * @param {Object} diff - * @return {boolean} - */ - _computeIsImageDiff(diff) { - return isImageDiff(diff); - } - - _commentsChanged(newComments) { - const allComments = []; - for (const side of [GrDiffBuilder.Side.LEFT, GrDiffBuilder.Side.RIGHT]) { - // This is needed by the threading. - for (const comment of newComments[side]) { - comment.__commentSide = side; - } - allComments.push(...newComments[side]); - } - // Currently, the only way this is ever changed here is when the initial - // comments are loaded, so it's okay performance wise to clear the threads - // and recreate them. If this changes in future, we might want to reuse - // some DOM nodes here. - this._clearThreads(); - const threads = this._createThreads(allComments); - for (const thread of threads) { - const threadEl = this._createThreadElement(thread); - this._attachThreadElement(threadEl); - } - } - - _sortComments(comments) { - return comments.slice(0).sort((a, b) => { - if (b.__draft && !a.__draft ) { return -1; } - if (a.__draft && !b.__draft ) { return 1; } - return util.parseDate(a.updated) - util.parseDate(b.updated); - }); - } - - /** - * @param {!Array<!Object>} comments - * @return {!Array<!Object>} Threads for the given comments. - */ - _createThreads(comments) { - const sortedComments = this._sortComments(comments); - const threads = []; - for (const comment of sortedComments) { - // If the comment is in reply to another comment, find that comment's - // thread and append to it. - if (comment.in_reply_to) { - const thread = threads.find(thread => - thread.comments.some(c => c.id === comment.in_reply_to)); - if (thread) { - thread.comments.push(comment); - continue; - } - } - - // Otherwise, this comment starts its own thread. - const newThread = { - start_datetime: comment.updated, - comments: [comment], - commentSide: comment.__commentSide, - patchNum: comment.patch_set, - rootId: comment.id || comment.__draftID, - lineNum: comment.line, - isOnParent: comment.side === 'PARENT', - }; - if (comment.range) { - newThread.range = Object.assign({}, comment.range); - } - threads.push(newThread); - } - return threads; - } - - /** - * @param {Object} blame - * @return {boolean} - */ - _computeIsBlameLoaded(blame) { - return !!blame; - } - - /** - * @param {Object} diff - * @return {!Promise} - */ - _getImages(diff) { - return this.$.restAPI.getImagesForDiff(this.changeNum, diff, - this.patchRange); - } - - /** @param {CustomEvent} e */ - _handleCreateComment(e) { - const {lineNum, side, patchNum, isOnParent, range} = e.detail; - const threadEl = this._getOrCreateThread(patchNum, lineNum, side, range, - isOnParent); - threadEl.addOrEditDraft(lineNum, range); - - this.$.reporting.recordDraftInteraction(); - } - - /** - * Gets or creates a comment thread at a given location. - * May provide a range, to get/create a range comment. - * - * @param {string} patchNum - * @param {?number} lineNum - * @param {string} commentSide - * @param {Gerrit.Range|undefined} range - * @param {boolean} isOnParent - * @return {!Object} - */ - _getOrCreateThread(patchNum, lineNum, commentSide, range, isOnParent) { - let threadEl = this._getThreadEl(lineNum, commentSide, range); - if (!threadEl) { - threadEl = this._createThreadElement({ - comments: [], - commentSide, - patchNum, - lineNum, - range, - isOnParent, - }); - this._attachThreadElement(threadEl); - } - return threadEl; - } - - _attachThreadElement(threadEl) { - Polymer.dom(this.$.diff).appendChild(threadEl); - } - - _clearThreads() { - for (const threadEl of this.getThreadEls()) { - const parent = Polymer.dom(threadEl).parentNode; - Polymer.dom(parent).removeChild(threadEl); - } - } - - _createThreadElement(thread) { - const threadEl = document.createElement('gr-comment-thread'); - threadEl.className = 'comment-thread'; - threadEl.setAttribute('slot', `${thread.commentSide}-${thread.lineNum}`); - threadEl.comments = thread.comments; - threadEl.commentSide = thread.commentSide; - threadEl.isOnParent = !!thread.isOnParent; - threadEl.parentIndex = this._parentIndex; - threadEl.changeNum = this.changeNum; - threadEl.patchNum = thread.patchNum; - threadEl.lineNum = thread.lineNum; - const rootIdChangedListener = changeEvent => { - thread.rootId = changeEvent.detail.value; - }; - threadEl.addEventListener('root-id-changed', rootIdChangedListener); - threadEl.path = this.path; - threadEl.projectName = this.projectName; - threadEl.range = thread.range; - const threadDiscardListener = e => { - const threadEl = /** @type {!Node} */ (e.currentTarget); - - const parent = Polymer.dom(threadEl).parentNode; - Polymer.dom(parent).removeChild(threadEl); - - threadEl.removeEventListener('root-id-changed', rootIdChangedListener); - threadEl.removeEventListener('thread-discard', threadDiscardListener); - }; - threadEl.addEventListener('thread-discard', threadDiscardListener); - return threadEl; - } - - /** - * Gets a comment thread element at a given location. - * May provide a range, to get a range comment. - * - * @param {?number} lineNum - * @param {string} commentSide - * @param {!Gerrit.Range=} range - * @return {?Node} - */ - _getThreadEl(lineNum, commentSide, range = undefined) { - let line; - if (commentSide === GrDiffBuilder.Side.LEFT) { - line = {beforeNumber: lineNum}; - } else if (commentSide === GrDiffBuilder.Side.RIGHT) { - line = {afterNumber: lineNum}; - } else { - throw new Error(`Unknown side: ${commentSide}`); - } - function matchesRange(threadEl) { - const threadRange = /** @type {!Gerrit.Range} */( - JSON.parse(threadEl.getAttribute('range'))); - return Gerrit.rangesEqual(threadRange, range); - } - - const filteredThreadEls = this._filterThreadElsForLocation( - this.getThreadEls(), line, commentSide).filter(matchesRange); - return filteredThreadEls.length ? filteredThreadEls[0] : null; - } - - /** - * @param {!Array<!HTMLElement>} threadEls - * @param {!{beforeNumber: (number|string|undefined|null), - * afterNumber: (number|string|undefined|null)}} - * lineInfo - * @param {!Gerrit.DiffSide=} side The side (LEFT, RIGHT) for - * which to return the threads. - * @return {!Array<!HTMLElement>} The thread elements matching the given - * location. - */ - _filterThreadElsForLocation(threadEls, lineInfo, side) { - function matchesLeftLine(threadEl) { - return threadEl.getAttribute('comment-side') == - Gerrit.DiffSide.LEFT && - threadEl.getAttribute('line-num') == lineInfo.beforeNumber; - } - function matchesRightLine(threadEl) { - return threadEl.getAttribute('comment-side') == - Gerrit.DiffSide.RIGHT && - threadEl.getAttribute('line-num') == lineInfo.afterNumber; - } - function matchesFileComment(threadEl) { - return threadEl.getAttribute('comment-side') == side && - // line/range comments have 1-based line set, if line is falsy it's - // a file comment - !threadEl.getAttribute('line-num'); - } - - // Select the appropriate matchers for the desired side and line - // If side is BOTH, we want both the left and right matcher. - const matchers = []; - if (side !== Gerrit.DiffSide.RIGHT) { - matchers.push(matchesLeftLine); - } - if (side !== Gerrit.DiffSide.LEFT) { - matchers.push(matchesRightLine); - } - if (lineInfo.afterNumber === 'FILE' || - lineInfo.beforeNumber === 'FILE') { - matchers.push(matchesFileComment); - } - return threadEls.filter(threadEl => - matchers.some(matcher => matcher(threadEl))); - } - - _getIgnoreWhitespace() { - if (!this.prefs || !this.prefs.ignore_whitespace) { - return WHITESPACE_IGNORE_NONE; - } - return this.prefs.ignore_whitespace; - } - - _whitespaceChanged( - preferredWhitespaceLevel, loadedWhitespaceLevel, - noRenderOnPrefsChange) { - // Polymer 2: check for undefined - if ([ - preferredWhitespaceLevel, - loadedWhitespaceLevel, - noRenderOnPrefsChange, - ].some(arg => arg === undefined)) { - return; - } - - if (preferredWhitespaceLevel !== loadedWhitespaceLevel && - !noRenderOnPrefsChange) { - this.reload(); - } - } - - _syntaxHighlightingChanged(noRenderOnPrefsChange, prefsChangeRecord) { - // Polymer 2: check for undefined - if ([ - noRenderOnPrefsChange, - prefsChangeRecord, - ].some(arg => arg === undefined)) { - return; - } - - if (prefsChangeRecord.path !== 'prefs.syntax_highlighting') { - return; - } - - if (!noRenderOnPrefsChange) { - this.reload(); - } - } - - /** - * @param {Object} patchRangeRecord - * @return {number|null} - */ - _computeParentIndex(patchRangeRecord) { - return this.isMergeParent(patchRangeRecord.base.basePatchNum) ? - this.getParentIndex(patchRangeRecord.base.basePatchNum) : null; - } - - _handleCommentSave(e) { - const comment = e.detail.comment; - const side = e.detail.comment.__commentSide; - const idx = this._findDraftIndex(comment, side); + if (idx !== -1) { // Update draft or comment. this.set(['comments', side, idx], comment); - this._handleCommentSaveOrDiscard(); - } - - _handleCommentDiscard(e) { - const comment = e.detail.comment; - this._removeComment(comment); - this._handleCommentSaveOrDiscard(); - } - - /** - * Closure annotation for Polymer.prototype.push is off. Submitted PR: - * https://github.com/Polymer/polymer/pull/4776 - * but for not supressing annotations. - * - * @suppress {checkTypes} - */ - _handleCommentUpdate(e) { - const comment = e.detail.comment; - const side = e.detail.comment.__commentSide; - let idx = this._findCommentIndex(comment, side); - if (idx === -1) { - idx = this._findDraftIndex(comment, side); - } - if (idx !== -1) { // Update draft or comment. - this.set(['comments', side, idx], comment); - } else { // Create new draft. - this.push(['comments', side], comment); - } - } - - _handleCommentSaveOrDiscard() { - this.dispatchEvent(new CustomEvent( - 'diff-comments-modified', {bubbles: true, composed: true})); - } - - _removeComment(comment) { - const side = comment.__commentSide; - this._removeCommentFromSide(comment, side); - } - - _removeCommentFromSide(comment, side) { - let idx = this._findCommentIndex(comment, side); - if (idx === -1) { - idx = this._findDraftIndex(comment, side); - } - if (idx !== -1) { - this.splice('comments.' + side, idx, 1); - } - } - - /** @return {number} */ - _findCommentIndex(comment, side) { - if (!comment.id || !this.comments[side]) { - return -1; - } - return this.comments[side].findIndex(item => item.id === comment.id); - } - - /** @return {number} */ - _findDraftIndex(comment, side) { - if (!comment.__draftID || !this.comments[side]) { - return -1; - } - return this.comments[side].findIndex( - item => item.__draftID === comment.__draftID); - } - - _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) { - if (!preferenceChangeRecord || - !preferenceChangeRecord.base || - !preferenceChangeRecord.base.syntax_highlighting || - !diff) { - return false; - } - return !this._anyLineTooLong(diff) && - this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH; - } - - /** - * @return {boolean} whether any of the lines in diff are longer - * than SYNTAX_MAX_LINE_LENGTH. - */ - _anyLineTooLong(diff) { - if (!diff) return false; - return diff.content.some(section => { - const lines = section.ab ? - section.ab : - (section.a || []).concat(section.b || []); - return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH); - }); - } - - _listenToViewportRender() { - const renderUpdateListener = start => { - if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) { - this.$.reporting.diffViewDisplayed(); - this.$.syntaxLayer.removeListener(renderUpdateListener); - } - }; - - this.$.syntaxLayer.addListener(renderUpdateListener); - } - - _handleRenderStart() { - this.$.reporting.time(TimingLabel.TOTAL); - this.$.reporting.time(TimingLabel.CONTENT); - } - - _handleRenderContent() { - this.$.reporting.timeEnd(TimingLabel.CONTENT); - } - - _handleNormalizeRange(event) { - this.$.reporting.reportInteraction('normalize-range', - { - side: event.detail.side, - lineNum: event.detail.lineNum, - }); - } - - _handleDiffContextExpanded(event) { - this.$.reporting.reportInteraction( - 'diff-context-expanded', {numLines: event.detail.numLines} - ); - } - - /** - * Find the last chunk for the given side. - * - * @param {!Object} diff - * @param {boolean} leftSide true if checking the base of the diff, - * false if testing the revision. - * @return {Object|null} returns the chunk object or null if there was - * no chunk for that side. - */ - _lastChunkForSide(diff, leftSide) { - if (!diff.content.length) { return null; } - - let chunkIndex = diff.content.length; - let chunk; - - // Walk backwards until we find a chunk for the given side. - do { - chunkIndex--; - chunk = diff.content[chunkIndex]; - } while ( - // We haven't reached the beginning. - chunkIndex >= 0 && - - // The chunk doesn't have both sides. - !chunk.ab && - - // The chunk doesn't have the given side. - ((leftSide && (!chunk.a || !chunk.a.length)) || - (!leftSide && (!chunk.b || !chunk.b.length)))); - - // If we reached the beginning of the diff and failed to find a chunk - // with the given side, return null. - if (chunkIndex === -1) { return null; } - - return chunk; - } - - /** - * Check whether the specified side of the diff has a trailing newline. - * - * @param {!Object} diff - * @param {boolean} leftSide true if checking the base of the diff, - * false if testing the revision. - * @return {boolean|null} Return true if the side has a trailing newline. - * Return false if it doesn't. Return null if not applicable (for - * example, if the diff has no content on the specified side). - */ - _hasTrailingNewlines(diff, leftSide) { - const chunk = this._lastChunkForSide(diff, leftSide); - if (!chunk) { return null; } - let lines; - if (chunk.ab) { - lines = chunk.ab; - } else { - lines = leftSide ? chunk.a : chunk.b; - } - return lines[lines.length - 1] === ''; - } - - _showNewlineWarningLeft(diff) { - return this._hasTrailingNewlines(diff, true) === false; - } - - _showNewlineWarningRight(diff) { - return this._hasTrailingNewlines(diff, false) === false; + } else { // Create new draft. + this.push(['comments', side], comment); } } - customElements.define(GrDiffHost.is, GrDiffHost); -})(); + _handleCommentSaveOrDiscard() { + this.dispatchEvent(new CustomEvent( + 'diff-comments-modified', {bubbles: true, composed: true})); + } + + _removeComment(comment) { + const side = comment.__commentSide; + this._removeCommentFromSide(comment, side); + } + + _removeCommentFromSide(comment, side) { + let idx = this._findCommentIndex(comment, side); + if (idx === -1) { + idx = this._findDraftIndex(comment, side); + } + if (idx !== -1) { + this.splice('comments.' + side, idx, 1); + } + } + + /** @return {number} */ + _findCommentIndex(comment, side) { + if (!comment.id || !this.comments[side]) { + return -1; + } + return this.comments[side].findIndex(item => item.id === comment.id); + } + + /** @return {number} */ + _findDraftIndex(comment, side) { + if (!comment.__draftID || !this.comments[side]) { + return -1; + } + return this.comments[side].findIndex( + item => item.__draftID === comment.__draftID); + } + + _isSyntaxHighlightingEnabled(preferenceChangeRecord, diff) { + if (!preferenceChangeRecord || + !preferenceChangeRecord.base || + !preferenceChangeRecord.base.syntax_highlighting || + !diff) { + return false; + } + return !this._anyLineTooLong(diff) && + this.$.diff.getDiffLength(diff) <= SYNTAX_MAX_DIFF_LENGTH; + } + + /** + * @return {boolean} whether any of the lines in diff are longer + * than SYNTAX_MAX_LINE_LENGTH. + */ + _anyLineTooLong(diff) { + if (!diff) return false; + return diff.content.some(section => { + const lines = section.ab ? + section.ab : + (section.a || []).concat(section.b || []); + return lines.some(line => line.length >= SYNTAX_MAX_LINE_LENGTH); + }); + } + + _listenToViewportRender() { + const renderUpdateListener = start => { + if (start > NUM_OF_LINES_THRESHOLD_FOR_VIEWPORT) { + this.$.reporting.diffViewDisplayed(); + this.$.syntaxLayer.removeListener(renderUpdateListener); + } + }; + + this.$.syntaxLayer.addListener(renderUpdateListener); + } + + _handleRenderStart() { + this.$.reporting.time(TimingLabel.TOTAL); + this.$.reporting.time(TimingLabel.CONTENT); + } + + _handleRenderContent() { + this.$.reporting.timeEnd(TimingLabel.CONTENT); + } + + _handleNormalizeRange(event) { + this.$.reporting.reportInteraction('normalize-range', + { + side: event.detail.side, + lineNum: event.detail.lineNum, + }); + } + + _handleDiffContextExpanded(event) { + this.$.reporting.reportInteraction( + 'diff-context-expanded', {numLines: event.detail.numLines} + ); + } + + /** + * Find the last chunk for the given side. + * + * @param {!Object} diff + * @param {boolean} leftSide true if checking the base of the diff, + * false if testing the revision. + * @return {Object|null} returns the chunk object or null if there was + * no chunk for that side. + */ + _lastChunkForSide(diff, leftSide) { + if (!diff.content.length) { return null; } + + let chunkIndex = diff.content.length; + let chunk; + + // Walk backwards until we find a chunk for the given side. + do { + chunkIndex--; + chunk = diff.content[chunkIndex]; + } while ( + // We haven't reached the beginning. + chunkIndex >= 0 && + + // The chunk doesn't have both sides. + !chunk.ab && + + // The chunk doesn't have the given side. + ((leftSide && (!chunk.a || !chunk.a.length)) || + (!leftSide && (!chunk.b || !chunk.b.length)))); + + // If we reached the beginning of the diff and failed to find a chunk + // with the given side, return null. + if (chunkIndex === -1) { return null; } + + return chunk; + } + + /** + * Check whether the specified side of the diff has a trailing newline. + * + * @param {!Object} diff + * @param {boolean} leftSide true if checking the base of the diff, + * false if testing the revision. + * @return {boolean|null} Return true if the side has a trailing newline. + * Return false if it doesn't. Return null if not applicable (for + * example, if the diff has no content on the specified side). + */ + _hasTrailingNewlines(diff, leftSide) { + const chunk = this._lastChunkForSide(diff, leftSide); + if (!chunk) { return null; } + let lines; + if (chunk.ab) { + lines = chunk.ab; + } else { + lines = leftSide ? chunk.a : chunk.b; + } + return lines[lines.length - 1] === ''; + } + + _showNewlineWarningLeft(diff) { + return this._hasTrailingNewlines(diff, true) === false; + } + + _showNewlineWarningRight(diff) { + return this._hasTrailingNewlines(diff, false) === false; + } +} + +customElements.define(GrDiffHost.is, GrDiffHost);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js index 2d9369f..d48531b 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_html.js
@@ -1,67 +1,26 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-comment-thread/gr-comment-thread.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../gr-diff/gr-diff.html"> -<link rel="import" href="../gr-syntax-layer/gr-syntax-layer.html"> - -<dom-module id="gr-diff-host"> - <template> - <gr-diff - id="diff" - change-num="[[changeNum]]" - no-auto-render=[[noAutoRender]] - patch-range="[[patchRange]]" - path="[[path]]" - prefs="[[prefs]]" - project-name="[[projectName]]" - display-line="[[displayLine]]" - is-image-diff="[[isImageDiff]]" - commit-range="[[commitRange]]" - hidden$="[[hidden]]" - no-render-on-prefs-change="[[noRenderOnPrefsChange]]" - line-wrapping="[[lineWrapping]]" - view-mode="[[viewMode]]" - line-of-interest="[[lineOfInterest]]" - logged-in="[[_loggedIn]]" - loading="[[_loading]]" - error-message="[[_errorMessage]]" - base-image="[[_baseImage]]" - revision-image=[[_revisionImage]] - coverage-ranges="[[_coverageRanges]]" - blame="[[_blame]]" - layers="[[_layers]]" - diff="[[diff]]" - show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]" - show-newline-warning-right="[[_showNewlineWarningRight(diff)]]"> +export const htmlTemplate = html` + <gr-diff id="diff" change-num="[[changeNum]]" no-auto-render="[[noAutoRender]]" patch-range="[[patchRange]]" path="[[path]]" prefs="[[prefs]]" project-name="[[projectName]]" display-line="[[displayLine]]" is-image-diff="[[isImageDiff]]" commit-range="[[commitRange]]" hidden\$="[[hidden]]" no-render-on-prefs-change="[[noRenderOnPrefsChange]]" line-wrapping="[[lineWrapping]]" view-mode="[[viewMode]]" line-of-interest="[[lineOfInterest]]" logged-in="[[_loggedIn]]" loading="[[_loading]]" error-message="[[_errorMessage]]" base-image="[[_baseImage]]" revision-image="[[_revisionImage]]" coverage-ranges="[[_coverageRanges]]" blame="[[_blame]]" layers="[[_layers]]" diff="[[diff]]" show-newline-warning-left="[[_showNewlineWarningLeft(diff)]]" show-newline-warning-right="[[_showNewlineWarningRight(diff)]]"> </gr-diff> - <gr-syntax-layer - id="syntaxLayer" - enabled="[[_syntaxHighlightingEnabled]]" - diff="[[diff]]"></gr-syntax-layer> + <gr-syntax-layer id="syntaxLayer" enabled="[[_syntaxHighlightingEnabled]]" diff="[[diff]]"></gr-syntax-layer> <gr-js-api-interface id="jsAPI"></gr-js-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting" category="diff"></gr-reporting> - </template> - <script src="gr-diff-host.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html index be2101c..d2097d3 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-diff-host.html"> +<script type="module" src="./gr-diff-host.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-host.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,125 +41,50 @@ </template> </test-fixture> -<script> - suite('gr-diff-host tests', async () => { - await readyToTest(); - let element; - let sandbox; - let getLoggedIn; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-host.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-diff-host tests', () => { + let element; + let sandbox; + let getLoggedIn; + setup(() => { + sandbox = sinon.sandbox.create(); + getLoggedIn = false; + stub('gr-rest-api-interface', { + async getLoggedIn() { return getLoggedIn; }, + }); + stub('gr-reporting', { + time: sandbox.stub(), + timeEnd: sandbox.stub(), + }); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('plugin layers', () => { + const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}]; setup(() => { - sandbox = sinon.sandbox.create(); - getLoggedIn = false; - stub('gr-rest-api-interface', { - async getLoggedIn() { return getLoggedIn; }, - }); - stub('gr-reporting', { - time: sandbox.stub(), - timeEnd: sandbox.stub(), + stub('gr-js-api-interface', { + getDiffLayers() { return pluginLayers; }, }); element = fixture('basic'); }); - - teardown(() => { - sandbox.restore(); + test('plugin layers requested', () => { + element.patchRange = {}; + element.reload(); + assert(element.$.jsAPI.getDiffLayers.called); }); + }); - suite('plugin layers', () => { - const pluginLayers = [{annotate: () => {}}, {annotate: () => {}}]; - setup(() => { - stub('gr-js-api-interface', { - getDiffLayers() { return pluginLayers; }, - }); - element = fixture('basic'); - }); - test('plugin layers requested', () => { - element.patchRange = {}; - element.reload(); - assert(element.$.jsAPI.getDiffLayers.called); - }); - }); - - suite('handle comment-update', () => { - setup(() => { - sandbox.stub(element, '_commentsChanged'); - element.comments = { - meta: { - changeNum: '42', - patchRange: { - basePatchNum: 'PARENT', - patchNum: 3, - }, - path: '/path/to/foo', - projectConfig: {foo: 'bar'}, - }, - left: [ - {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, - {id: 'bc2', side: 'PARENT', __commentSide: 'left'}, - {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, - {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, - ], - right: [ - {id: 'c1', __commentSide: 'right'}, - {id: 'c2', __commentSide: 'right'}, - {id: 'd1', __draft: true, __commentSide: 'right'}, - {id: 'd2', __draft: true, __commentSide: 'right'}, - ], - }; - }); - - test('creating a draft', () => { - const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT', - __commentSide: 'left'}; - element.fire('comment-update', {comment}); - assert.include(element.comments.left, comment); - }); - - test('discarding a draft', () => { - const draftID = 'tempID'; - const id = 'savedID'; - const comment = { - __draft: true, - __draftID: draftID, - side: 'PARENT', - __commentSide: 'left', - }; - const diffCommentsModifiedStub = sandbox.stub(); - element.addEventListener('diff-comments-modified', - diffCommentsModifiedStub); - element.comments.left.push(comment); - comment.id = id; - element.fire('comment-discard', {comment}); - const drafts = element.comments.left - .filter(item => item.__draftID === draftID); - assert.equal(drafts.length, 0); - assert.isTrue(diffCommentsModifiedStub.called); - }); - - test('saving a draft', () => { - const draftID = 'tempID'; - const id = 'savedID'; - const comment = { - __draft: true, - __draftID: draftID, - side: 'PARENT', - __commentSide: 'left', - }; - const diffCommentsModifiedStub = sandbox.stub(); - element.addEventListener('diff-comments-modified', - diffCommentsModifiedStub); - element.comments.left.push(comment); - comment.id = id; - element.fire('comment-save', {comment}); - const drafts = element.comments.left - .filter(item => item.__draftID === draftID); - assert.equal(drafts.length, 1); - assert.equal(drafts[0].id, id); - assert.isTrue(diffCommentsModifiedStub.called); - }); - }); - - test('remove comment', () => { + suite('handle comment-update', () => { + setup(() => { sandbox.stub(element, '_commentsChanged'); element.comments = { meta: { @@ -179,1453 +109,1531 @@ {id: 'd2', __draft: true, __commentSide: 'right'}, ], }; - - element._removeComment({}); - // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem - // to believe that one object deepEquals another even when they do :-/. - assert.equal(JSON.stringify(element.comments), JSON.stringify({ - meta: { - changeNum: '42', - patchRange: { - basePatchNum: 'PARENT', - patchNum: 3, - }, - path: '/path/to/foo', - projectConfig: {foo: 'bar'}, - }, - left: [ - {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, - {id: 'bc2', side: 'PARENT', __commentSide: 'left'}, - {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, - {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, - ], - right: [ - {id: 'c1', __commentSide: 'right'}, - {id: 'c2', __commentSide: 'right'}, - {id: 'd1', __draft: true, __commentSide: 'right'}, - {id: 'd2', __draft: true, __commentSide: 'right'}, - ], - })); - - element._removeComment({id: 'bc2', side: 'PARENT', - __commentSide: 'left'}); - assert.deepEqual(element.comments, { - meta: { - changeNum: '42', - patchRange: { - basePatchNum: 'PARENT', - patchNum: 3, - }, - path: '/path/to/foo', - projectConfig: {foo: 'bar'}, - }, - left: [ - {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, - {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, - {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, - ], - right: [ - {id: 'c1', __commentSide: 'right'}, - {id: 'c2', __commentSide: 'right'}, - {id: 'd1', __draft: true, __commentSide: 'right'}, - {id: 'd2', __draft: true, __commentSide: 'right'}, - ], - }); - - element._removeComment({id: 'd2', __commentSide: 'right'}); - assert.deepEqual(element.comments, { - meta: { - changeNum: '42', - patchRange: { - basePatchNum: 'PARENT', - patchNum: 3, - }, - path: '/path/to/foo', - projectConfig: {foo: 'bar'}, - }, - left: [ - {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, - {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, - {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, - ], - right: [ - {id: 'c1', __commentSide: 'right'}, - {id: 'c2', __commentSide: 'right'}, - {id: 'd1', __draft: true, __commentSide: 'right'}, - ], - }); }); - test('thread-discard handling', () => { - const threads = [ - {comments: [{id: 4711}]}, - {comments: [{id: 42}]}, - ]; - element._parentIndex = 1; - element.changeNum = '2'; - element.path = 'some/path'; - element.projectName = 'Some project'; - const threadEls = threads.map( - thread => { - const threadEl = element._createThreadElement(thread); - // Polymer 2 doesn't fire ready events and doesn't execute - // observers if element is not added to the Dom. - // See https://github.com/Polymer/old-docs-site/issues/2322 - // and https://github.com/Polymer/polymer/issues/4526 - element._attachThreadElement(threadEl); - return threadEl; - }); - assert.equal(threadEls.length, 2); - assert.equal(threadEls[0].rootId, 4711); - assert.equal(threadEls[1].rootId, 42); - for (const threadEl of threadEls) { - Polymer.dom(element).appendChild(threadEl); - } - - threadEls[0].dispatchEvent( - new CustomEvent('thread-discard', {detail: {rootId: 4711}})); - const attachedThreads = element.queryAllEffectiveChildren( - 'gr-comment-thread'); - assert.equal(attachedThreads.length, 1); - assert.equal(attachedThreads[0].rootId, 42); + test('creating a draft', () => { + const comment = {__draft: true, __draftID: 'tempID', side: 'PARENT', + __commentSide: 'left'}; + element.fire('comment-update', {comment}); + assert.include(element.comments.left, comment); }); - suite('render reporting', () => { - test('starts total and content timer on render-start', done => { - element.dispatchEvent( - new CustomEvent('render-start', {bubbles: true, composed: true})); - assert.isTrue(element.$.reporting.time.calledWithExactly( - 'Diff Total Render')); - assert.isTrue(element.$.reporting.time.calledWithExactly( - 'Diff Content Render')); - done(); - }); + test('discarding a draft', () => { + const draftID = 'tempID'; + const id = 'savedID'; + const comment = { + __draft: true, + __draftID: draftID, + side: 'PARENT', + __commentSide: 'left', + }; + const diffCommentsModifiedStub = sandbox.stub(); + element.addEventListener('diff-comments-modified', + diffCommentsModifiedStub); + element.comments.left.push(comment); + comment.id = id; + element.fire('comment-discard', {comment}); + const drafts = element.comments.left + .filter(item => item.__draftID === draftID); + assert.equal(drafts.length, 0); + assert.isTrue(diffCommentsModifiedStub.called); + }); - test('ends content timer on render-content', () => { - element.dispatchEvent( - new CustomEvent('render-content', {bubbles: true, composed: true})); - assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( - 'Diff Content Render')); - }); + test('saving a draft', () => { + const draftID = 'tempID'; + const id = 'savedID'; + const comment = { + __draft: true, + __draftID: draftID, + side: 'PARENT', + __commentSide: 'left', + }; + const diffCommentsModifiedStub = sandbox.stub(); + element.addEventListener('diff-comments-modified', + diffCommentsModifiedStub); + element.comments.left.push(comment); + comment.id = id; + element.fire('comment-save', {comment}); + const drafts = element.comments.left + .filter(item => item.__draftID === draftID); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].id, id); + assert.isTrue(diffCommentsModifiedStub.called); + }); + }); - test('ends total and syntax timer after syntax layer processing', done => { - let notifySyntaxProcessed; - sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise( - resolve => { - notifySyntaxProcessed = resolve; - })); - sandbox.stub(element.$.restAPI, 'getDiff').returns( - Promise.resolve({content: []})); - element.patchRange = {}; - element.$.restAPI.getDiffPreferences().then(prefs => { - element.prefs = prefs; - return element.reload(true); + test('remove comment', () => { + sandbox.stub(element, '_commentsChanged'); + element.comments = { + meta: { + changeNum: '42', + patchRange: { + basePatchNum: 'PARENT', + patchNum: 3, + }, + path: '/path/to/foo', + projectConfig: {foo: 'bar'}, + }, + left: [ + {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, + {id: 'bc2', side: 'PARENT', __commentSide: 'left'}, + {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, + {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, + ], + right: [ + {id: 'c1', __commentSide: 'right'}, + {id: 'c2', __commentSide: 'right'}, + {id: 'd1', __draft: true, __commentSide: 'right'}, + {id: 'd2', __draft: true, __commentSide: 'right'}, + ], + }; + + element._removeComment({}); + // Using JSON.stringify because Safari 9.1 (11601.5.17.1) doesn’t seem + // to believe that one object deepEquals another even when they do :-/. + assert.equal(JSON.stringify(element.comments), JSON.stringify({ + meta: { + changeNum: '42', + patchRange: { + basePatchNum: 'PARENT', + patchNum: 3, + }, + path: '/path/to/foo', + projectConfig: {foo: 'bar'}, + }, + left: [ + {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, + {id: 'bc2', side: 'PARENT', __commentSide: 'left'}, + {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, + {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, + ], + right: [ + {id: 'c1', __commentSide: 'right'}, + {id: 'c2', __commentSide: 'right'}, + {id: 'd1', __draft: true, __commentSide: 'right'}, + {id: 'd2', __draft: true, __commentSide: 'right'}, + ], + })); + + element._removeComment({id: 'bc2', side: 'PARENT', + __commentSide: 'left'}); + assert.deepEqual(element.comments, { + meta: { + changeNum: '42', + patchRange: { + basePatchNum: 'PARENT', + patchNum: 3, + }, + path: '/path/to/foo', + projectConfig: {foo: 'bar'}, + }, + left: [ + {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, + {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, + {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, + ], + right: [ + {id: 'c1', __commentSide: 'right'}, + {id: 'c2', __commentSide: 'right'}, + {id: 'd1', __draft: true, __commentSide: 'right'}, + {id: 'd2', __draft: true, __commentSide: 'right'}, + ], + }); + + element._removeComment({id: 'd2', __commentSide: 'right'}); + assert.deepEqual(element.comments, { + meta: { + changeNum: '42', + patchRange: { + basePatchNum: 'PARENT', + patchNum: 3, + }, + path: '/path/to/foo', + projectConfig: {foo: 'bar'}, + }, + left: [ + {id: 'bc1', side: 'PARENT', __commentSide: 'left'}, + {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'}, + {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'}, + ], + right: [ + {id: 'c1', __commentSide: 'right'}, + {id: 'c2', __commentSide: 'right'}, + {id: 'd1', __draft: true, __commentSide: 'right'}, + ], + }); + }); + + test('thread-discard handling', () => { + const threads = [ + {comments: [{id: 4711}]}, + {comments: [{id: 42}]}, + ]; + element._parentIndex = 1; + element.changeNum = '2'; + element.path = 'some/path'; + element.projectName = 'Some project'; + const threadEls = threads.map( + thread => { + const threadEl = element._createThreadElement(thread); + // Polymer 2 doesn't fire ready events and doesn't execute + // observers if element is not added to the Dom. + // See https://github.com/Polymer/old-docs-site/issues/2322 + // and https://github.com/Polymer/polymer/issues/4526 + element._attachThreadElement(threadEl); + return threadEl; }); - // Multiple cascading microtasks are scheduled. - setTimeout(() => { - notifySyntaxProcessed(); - // Assert after the notification task is processed. - Promise.resolve().then(() => { - assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( - 'Diff Total Render')); - assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( - 'Diff Syntax Render')); - assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( - 'StartupDiffViewOnlyContent')); - done(); - }); - }); - }); + assert.equal(threadEls.length, 2); + assert.equal(threadEls[0].rootId, 4711); + assert.equal(threadEls[1].rootId, 42); + for (const threadEl of threadEls) { + dom(element).appendChild(threadEl); + } - test('ends total timer w/ no syntax layer processing', done => { - sandbox.stub(element.$.restAPI, 'getDiff').returns( - Promise.resolve({content: []})); - element.patchRange = {}; - element.reload(); - // Multiple cascading microtasks are scheduled. - setTimeout(() => { - assert.isTrue(element.$.reporting.timeEnd.calledOnce); + threadEls[0].dispatchEvent( + new CustomEvent('thread-discard', {detail: {rootId: 4711}})); + const attachedThreads = element.queryAllEffectiveChildren( + 'gr-comment-thread'); + assert.equal(attachedThreads.length, 1); + assert.equal(attachedThreads[0].rootId, 42); + }); + + suite('render reporting', () => { + test('starts total and content timer on render-start', done => { + element.dispatchEvent( + new CustomEvent('render-start', {bubbles: true, composed: true})); + assert.isTrue(element.$.reporting.time.calledWithExactly( + 'Diff Total Render')); + assert.isTrue(element.$.reporting.time.calledWithExactly( + 'Diff Content Render')); + done(); + }); + + test('ends content timer on render-content', () => { + element.dispatchEvent( + new CustomEvent('render-content', {bubbles: true, composed: true})); + assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( + 'Diff Content Render')); + }); + + test('ends total and syntax timer after syntax layer processing', done => { + let notifySyntaxProcessed; + sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise( + resolve => { + notifySyntaxProcessed = resolve; + })); + sandbox.stub(element.$.restAPI, 'getDiff').returns( + Promise.resolve({content: []})); + element.patchRange = {}; + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + return element.reload(true); + }); + // Multiple cascading microtasks are scheduled. + setTimeout(() => { + notifySyntaxProcessed(); + // Assert after the notification task is processed. + Promise.resolve().then(() => { assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( 'Diff Total Render')); + assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( + 'Diff Syntax Render')); + assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( + 'StartupDiffViewOnlyContent')); done(); }); }); + }); - test('completes reload promise after syntax layer processing', done => { - let notifySyntaxProcessed; - sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise( - resolve => { - notifySyntaxProcessed = resolve; - })); - sandbox.stub(element.$.restAPI, 'getDiff').returns( - Promise.resolve({content: []})); - element.patchRange = {}; - let reloadComplete = false; - element.$.restAPI.getDiffPreferences() - .then(prefs => { - element.prefs = prefs; - return element.reload(); - }) - .then(() => { - reloadComplete = true; - }); - // Multiple cascading microtasks are scheduled. + test('ends total timer w/ no syntax layer processing', done => { + sandbox.stub(element.$.restAPI, 'getDiff').returns( + Promise.resolve({content: []})); + element.patchRange = {}; + element.reload(); + // Multiple cascading microtasks are scheduled. + setTimeout(() => { + assert.isTrue(element.$.reporting.timeEnd.calledOnce); + assert.isTrue(element.$.reporting.timeEnd.calledWithExactly( + 'Diff Total Render')); + done(); + }); + }); + + test('completes reload promise after syntax layer processing', done => { + let notifySyntaxProcessed; + sandbox.stub(element.$.syntaxLayer, 'process').returns(new Promise( + resolve => { + notifySyntaxProcessed = resolve; + })); + sandbox.stub(element.$.restAPI, 'getDiff').returns( + Promise.resolve({content: []})); + element.patchRange = {}; + let reloadComplete = false; + element.$.restAPI.getDiffPreferences() + .then(prefs => { + element.prefs = prefs; + return element.reload(); + }) + .then(() => { + reloadComplete = true; + }); + // Multiple cascading microtasks are scheduled. + setTimeout(() => { + assert.isFalse(reloadComplete); + notifySyntaxProcessed(); + // Assert after the notification task is processed. setTimeout(() => { - assert.isFalse(reloadComplete); - notifySyntaxProcessed(); - // Assert after the notification task is processed. - setTimeout(() => { - assert.isTrue(reloadComplete); - done(); - }); + assert.isTrue(reloadComplete); + done(); }); }); }); + }); - test('reload() cancels before network resolves', () => { - const cancelStub = sandbox.stub(element.$.diff, 'cancel'); + test('reload() cancels before network resolves', () => { + const cancelStub = sandbox.stub(element.$.diff, 'cancel'); - // Stub the network calls into requests that never resolve. - sandbox.stub(element, '_getDiff', () => new Promise(() => {})); + // Stub the network calls into requests that never resolve. + sandbox.stub(element, '_getDiff', () => new Promise(() => {})); + element.patchRange = {}; + + element.reload(); + assert.isTrue(cancelStub.called); + }); + + suite('not logged in', () => { + setup(() => { + getLoggedIn = false; + element = fixture('basic'); + }); + + test('reload() loads files weblinks', () => { + const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks') + .returns({name: 'stubb', url: '#s'}); + sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({ + content: [], + })); + element.projectName = 'test-project'; + element.path = 'test-path'; + element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'}; element.patchRange = {}; - - element.reload(); - assert.isTrue(cancelStub.called); + return element.reload().then(() => { + assert.isTrue(weblinksStub.calledTwice); + assert.isTrue(weblinksStub.firstCall.calledWith({ + commit: 'test-base', + file: 'test-path', + options: { + weblinks: undefined, + }, + repo: 'test-project', + type: Gerrit.Nav.WeblinkType.FILE})); + assert.isTrue(weblinksStub.secondCall.calledWith({ + commit: 'test-commit', + file: 'test-path', + options: { + weblinks: undefined, + }, + repo: 'test-project', + type: Gerrit.Nav.WeblinkType.FILE})); + assert.deepEqual(element.filesWeblinks, { + meta_a: [{name: 'stubb', url: '#s'}], + meta_b: [{name: 'stubb', url: '#s'}], + }); + }); }); - suite('not logged in', () => { + test('_getDiff handles null diff responses', done => { + stub('gr-rest-api-interface', { + getDiff() { return Promise.resolve(null); }, + }); + element.changeNum = 123; + element.patchRange = {basePatchNum: 1, patchNum: 2}; + element.path = 'file.txt'; + element._getDiff().then(done); + }); + + test('reload resolves on error', () => { + const onErrStub = sandbox.stub(element, '_handleGetDiffError'); + const error = {ok: false, status: 500}; + sandbox.stub(element.$.restAPI, 'getDiff', + (changeNum, basePatchNum, patchNum, path, onErr) => { + onErr(error); + }); + element.patchRange = {}; + return element.reload().then(() => { + assert.isTrue(onErrStub.calledOnce); + }); + }); + + suite('_handleGetDiffError', () => { + let serverErrorStub; + let pageErrorStub; + setup(() => { - getLoggedIn = false; - element = fixture('basic'); + serverErrorStub = sinon.stub(); + element.addEventListener('server-error', serverErrorStub); + pageErrorStub = sinon.stub(); + element.addEventListener('page-error', pageErrorStub); }); - test('reload() loads files weblinks', () => { - const weblinksStub = sandbox.stub(Gerrit.Nav, '_generateWeblinks') - .returns({name: 'stubb', url: '#s'}); - sandbox.stub(element.$.restAPI, 'getDiff').returns(Promise.resolve({ - content: [], - })); - element.projectName = 'test-project'; - element.path = 'test-path'; - element.commitRange = {baseCommit: 'test-base', commit: 'test-commit'}; - element.patchRange = {}; - return element.reload().then(() => { - assert.isTrue(weblinksStub.calledTwice); - assert.isTrue(weblinksStub.firstCall.calledWith({ - commit: 'test-base', - file: 'test-path', - options: { - weblinks: undefined, - }, - repo: 'test-project', - type: Gerrit.Nav.WeblinkType.FILE})); - assert.isTrue(weblinksStub.secondCall.calledWith({ - commit: 'test-commit', - file: 'test-path', - options: { - weblinks: undefined, - }, - repo: 'test-project', - type: Gerrit.Nav.WeblinkType.FILE})); - assert.deepEqual(element.filesWeblinks, { - meta_a: [{name: 'stubb', url: '#s'}], - meta_b: [{name: 'stubb', url: '#s'}], - }); - }); + test('page error on HTTP-409', () => { + element._handleGetDiffError({status: 409}); + assert.isTrue(serverErrorStub.calledOnce); + assert.isFalse(pageErrorStub.called); + assert.isNotOk(element._errorMessage); }); - test('_getDiff handles null diff responses', done => { - stub('gr-rest-api-interface', { - getDiff() { return Promise.resolve(null); }, - }); - element.changeNum = 123; - element.patchRange = {basePatchNum: 1, patchNum: 2}; - element.path = 'file.txt'; - element._getDiff().then(done); + test('server error on non-HTTP-409', () => { + element._handleGetDiffError({status: 500}); + assert.isFalse(serverErrorStub.called); + assert.isTrue(pageErrorStub.calledOnce); + assert.isNotOk(element._errorMessage); }); - test('reload resolves on error', () => { - const onErrStub = sandbox.stub(element, '_handleGetDiffError'); - const error = {ok: false, status: 500}; - sandbox.stub(element.$.restAPI, 'getDiff', - (changeNum, basePatchNum, patchNum, path, onErr) => { - onErr(error); - }); - element.patchRange = {}; - return element.reload().then(() => { - assert.isTrue(onErrStub.calledOnce); - }); + test('error message if showLoadFailure', () => { + element.showLoadFailure = true; + element._handleGetDiffError({status: 500, statusText: 'Failure!'}); + assert.isFalse(serverErrorStub.called); + assert.isFalse(pageErrorStub.called); + assert.equal(element._errorMessage, + 'Encountered error when loading the diff: 500 Failure!'); + }); + }); + + suite('image diffs', () => { + let mockFile1; + let mockFile2; + setup(() => { + mockFile1 = { + body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + + 'wsAAAAAAAAAAAAAAAAA/w==', + type: 'image/bmp', + }; + mockFile2 = { + body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + + 'wsAAAAAAAAAAAAA/////w==', + type: 'image/bmp', + }; + sandbox.stub(element.$.restAPI, + 'getB64FileContents', + (changeId, patchNum, path, opt_parentIndex) => Promise.resolve( + opt_parentIndex === 1 ? mockFile1 : + mockFile2) + ); + + element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; + element.comments = { + left: [], + right: [], + meta: {patchRange: element.patchRange}, + }; }); - suite('_handleGetDiffError', () => { - let serverErrorStub; - let pageErrorStub; + test('renders image diffs with same file name', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index 2adc47d..f9c2f2c 100644', + '--- a/carrot.jpg', + '+++ b/carrot.jpg', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + sandbox.stub(element.$.restAPI, 'getDiff') + .returns(Promise.resolve(mockDiff)); - setup(() => { - serverErrorStub = sinon.stub(); - element.addEventListener('server-error', serverErrorStub); - pageErrorStub = sinon.stub(); - element.addEventListener('page-error', pageErrorStub); - }); + const rendered = () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); - test('page error on HTTP-409', () => { - element._handleGetDiffError({status: 409}); - assert.isTrue(serverErrorStub.calledOnce); - assert.isFalse(pageErrorStub.called); - assert.isNotOk(element._errorMessage); - }); + // Left image rendered with the parent commit's version of the file. + const leftImage = + element.$.diff.$.diffTable.querySelector('td.left img'); + const leftLabel = + element.$.diff.$.diffTable.querySelector('td.left label'); + const leftLabelContent = leftLabel.querySelector('.label'); + const leftLabelName = leftLabel.querySelector('.name'); - test('server error on non-HTTP-409', () => { - element._handleGetDiffError({status: 500}); - assert.isFalse(serverErrorStub.called); - assert.isTrue(pageErrorStub.calledOnce); - assert.isNotOk(element._errorMessage); - }); + const rightImage = + element.$.diff.$.diffTable.querySelector('td.right img'); + const rightLabel = element.$.diff.$.diffTable.querySelector( + 'td.right label'); + const rightLabelContent = rightLabel.querySelector('.label'); + const rightLabelName = rightLabel.querySelector('.name'); - test('error message if showLoadFailure', () => { - element.showLoadFailure = true; - element._handleGetDiffError({status: 500, statusText: 'Failure!'}); - assert.isFalse(serverErrorStub.called); - assert.isFalse(pageErrorStub.called); - assert.equal(element._errorMessage, - 'Encountered error when loading the diff: 500 Failure!'); - }); - }); + assert.isNotOk(rightLabelName); + assert.isNotOk(leftLabelName); - suite('image diffs', () => { - let mockFile1; - let mockFile2; - setup(() => { - mockFile1 = { - body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + - 'wsAAAAAAAAAAAAAAAAA/w==', - type: 'image/bmp', - }; - mockFile2 = { - body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + - 'wsAAAAAAAAAAAAA/////w==', - type: 'image/bmp', - }; - sandbox.stub(element.$.restAPI, - 'getB64FileContents', - (changeId, patchNum, path, opt_parentIndex) => Promise.resolve( - opt_parentIndex === 1 ? mockFile1 : - mockFile2) - ); + let leftLoaded = false; + let rightLoaded = false; - element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; - element.comments = { - left: [], - right: [], - meta: {patchRange: element.patchRange}, - }; - }); - - test('renders image diffs with same file name', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'MODIFIED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index 2adc47d..f9c2f2c 100644', - '--- a/carrot.jpg', - '+++ b/carrot.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - sandbox.stub(element.$.restAPI, 'getDiff') - .returns(Promise.resolve(mockDiff)); - - const rendered = () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); - - // Left image rendered with the parent commit's version of the file. - const leftImage = - element.$.diff.$.diffTable.querySelector('td.left img'); - const leftLabel = - element.$.diff.$.diffTable.querySelector('td.left label'); - const leftLabelContent = leftLabel.querySelector('.label'); - const leftLabelName = leftLabel.querySelector('.name'); - - const rightImage = - element.$.diff.$.diffTable.querySelector('td.right img'); - const rightLabel = element.$.diff.$.diffTable.querySelector( - 'td.right label'); - const rightLabelContent = rightLabel.querySelector('.label'); - const rightLabelName = rightLabel.querySelector('.name'); - - assert.isNotOk(rightLabelName); - assert.isNotOk(leftLabelName); - - let leftLoaded = false; - let rightLoaded = false; - - leftImage.addEventListener('load', () => { - assert.isOk(leftImage); - assert.equal(leftImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile1.body); - assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); - leftLoaded = true; - if (rightLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - - rightImage.addEventListener('load', () => { - assert.isOk(rightImage); - assert.equal(rightImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile2.body); - assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); - - rightLoaded = true; - if (leftLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - }; - - element.addEventListener('render', rendered); - - element.$.restAPI.getDiffPreferences().then(prefs => { - element.prefs = prefs; - element.reload(); - }); - }); - - test('renders image diffs with a different file name', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'MODIFIED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot2.jpg', - 'index 2adc47d..f9c2f2c 100644', - '--- a/carrot.jpg', - '+++ b/carrot2.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - sandbox.stub(element.$.restAPI, 'getDiff') - .returns(Promise.resolve(mockDiff)); - - const rendered = () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); - - // Left image rendered with the parent commit's version of the file. - const leftImage = - element.$.diff.$.diffTable.querySelector('td.left img'); - const leftLabel = - element.$.diff.$.diffTable.querySelector('td.left label'); - const leftLabelContent = leftLabel.querySelector('.label'); - const leftLabelName = leftLabel.querySelector('.name'); - - const rightImage = - element.$.diff.$.diffTable.querySelector('td.right img'); - const rightLabel = element.$.diff.$.diffTable.querySelector( - 'td.right label'); - const rightLabelContent = rightLabel.querySelector('.label'); - const rightLabelName = rightLabel.querySelector('.name'); - - assert.isOk(rightLabelName); - assert.isOk(leftLabelName); - assert.equal(leftLabelName.textContent, mockDiff.meta_a.name); - assert.equal(rightLabelName.textContent, mockDiff.meta_b.name); - - let leftLoaded = false; - let rightLoaded = false; - - leftImage.addEventListener('load', () => { - assert.isOk(leftImage); - assert.equal(leftImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile1.body); - assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); - leftLoaded = true; - if (rightLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - - rightImage.addEventListener('load', () => { - assert.isOk(rightImage); - assert.equal(rightImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile2.body); - assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); - - rightLoaded = true; - if (leftLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - }; - - element.addEventListener('render', rendered); - - element.$.restAPI.getDiffPreferences().then(prefs => { - element.prefs = prefs; - element.reload(); - }); - }); - - test('renders added image', done => { - const mockDiff = { - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'ADDED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index 0000000..f9c2f2c 100644', - '--- /dev/null', - '+++ b/carrot.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - sandbox.stub(element.$.restAPI, 'getDiff') - .returns(Promise.resolve(mockDiff)); - - element.addEventListener('render', () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); - - const leftImage = - element.$.diff.$.diffTable.querySelector('td.left img'); - const rightImage = - element.$.diff.$.diffTable.querySelector('td.right img'); - - assert.isNotOk(leftImage); - assert.isOk(rightImage); - done(); - }); - - element.$.restAPI.getDiffPreferences().then(prefs => { - element.prefs = prefs; - element.reload(); - }); - }); - - test('renders removed image', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'DELETED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index f9c2f2c..0000000 100644', - '--- a/carrot.jpg', - '+++ /dev/null', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - sandbox.stub(element.$.restAPI, 'getDiff') - .returns(Promise.resolve(mockDiff)); - - element.addEventListener('render', () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); - - const leftImage = - element.$.diff.$.diffTable.querySelector('td.left img'); - const rightImage = - element.$.diff.$.diffTable.querySelector('td.right img'); - + leftImage.addEventListener('load', () => { assert.isOk(leftImage); - assert.isNotOk(rightImage); - done(); + assert.equal(leftImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile1.body); + assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); + leftLoaded = true; + if (rightLoaded) { + element.removeEventListener('render', rendered); + done(); + } }); - element.$.restAPI.getDiffPreferences().then(prefs => { - element.prefs = prefs; - element.reload(); + rightImage.addEventListener('load', () => { + assert.isOk(rightImage); + assert.equal(rightImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile2.body); + assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); + + rightLoaded = true; + if (leftLoaded) { + element.removeEventListener('render', rendered); + done(); + } }); - }); + }; - test('does not render disallowed image type', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil', - lines: 560}, - intraline_status: 'OK', - change_type: 'DELETED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index f9c2f2c..0000000 100644', - '--- a/carrot.jpg', - '+++ /dev/null', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - mockFile1.type = 'image/jpeg-evil'; + element.addEventListener('render', rendered); - sandbox.stub(element.$.restAPI, 'getDiff') - .returns(Promise.resolve(mockDiff)); - - element.addEventListener('render', () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); - const leftImage = - element.$.diff.$.diffTable.querySelector('td.left img'); - assert.isNotOk(leftImage); - done(); - }); - - element.$.restAPI.getDiffPreferences().then(prefs => { - element.prefs = prefs; - element.reload(); - }); - }); - }); - }); - - test('delegates cancel()', () => { - const stub = sandbox.stub(element.$.diff, 'cancel'); - element.patchRange = {}; - element.reload(); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 0); - }); - - test('delegates getCursorStops()', () => { - const returnValue = [document.createElement('b')]; - const stub = sandbox.stub(element.$.diff, 'getCursorStops') - .returns(returnValue); - assert.equal(element.getCursorStops(), returnValue); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 0); - }); - - test('delegates isRangeSelected()', () => { - const returnValue = true; - const stub = sandbox.stub(element.$.diff, 'isRangeSelected') - .returns(returnValue); - assert.equal(element.isRangeSelected(), returnValue); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 0); - }); - - test('delegates toggleLeftDiff()', () => { - const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff'); - element.toggleLeftDiff(); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 0); - }); - - suite('blame', () => { - setup(() => { - element = fixture('basic'); - }); - - test('clearBlame', () => { - element._blame = []; - const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame'); - element.clearBlame(); - assert.isNull(element._blame); - assert.isTrue(setBlameSpy.calledWithExactly(null)); - assert.equal(element.isBlameLoaded, false); - }); - - test('loadBlame', () => { - const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}]; - const showAlertStub = sinon.stub(); - element.addEventListener('show-alert', showAlertStub); - const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame') - .returns(Promise.resolve(mockBlame)); - element.changeNum = 42; - element.patchRange = {patchNum: 5, basePatchNum: 4}; - element.path = 'foo/bar.baz'; - return element.loadBlame().then(() => { - assert.isTrue(getBlameStub.calledWithExactly( - 42, 5, 'foo/bar.baz', true)); - assert.isFalse(showAlertStub.called); - assert.equal(element._blame, mockBlame); - assert.equal(element.isBlameLoaded, true); + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); }); }); - test('loadBlame empty', () => { - const mockBlame = []; - const showAlertStub = sinon.stub(); - element.addEventListener('show-alert', showAlertStub); - sandbox.stub(element.$.restAPI, 'getBlame') - .returns(Promise.resolve(mockBlame)); - element.changeNum = 42; - element.patchRange = {patchNum: 5, basePatchNum: 4}; - element.path = 'foo/bar.baz'; - return element.loadBlame() - .then(() => { - assert.isTrue(false, 'Promise should not resolve'); - }) - .catch(() => { - assert.isTrue(showAlertStub.calledOnce); - assert.isNull(element._blame); - assert.equal(element.isBlameLoaded, false); - }); - }); - }); - - test('getThreadEls() returns .comment-threads', () => { - const threadEl = document.createElement('div'); - threadEl.className = 'comment-thread'; - Polymer.dom(element.$.diff).appendChild(threadEl); - assert.deepEqual(element.getThreadEls(), [threadEl]); - }); - - test('delegates addDraftAtLine(el)', () => { - const param0 = document.createElement('b'); - const stub = sandbox.stub(element.$.diff, 'addDraftAtLine'); - element.addDraftAtLine(param0); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 1); - assert.equal(stub.lastCall.args[0], param0); - }); - - test('delegates clearDiffContent()', () => { - const stub = sandbox.stub(element.$.diff, 'clearDiffContent'); - element.clearDiffContent(); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 0); - }); - - test('delegates expandAllContext()', () => { - const stub = sandbox.stub(element.$.diff, 'expandAllContext'); - element.expandAllContext(); - assert.isTrue(stub.calledOnce); - assert.equal(stub.lastCall.args.length, 0); - }); - - test('passes in changeNum', () => { - const value = '12345'; - element.changeNum = value; - assert.equal(element.$.diff.changeNum, value); - }); - - test('passes in noAutoRender', () => { - const value = true; - element.noAutoRender = value; - assert.equal(element.$.diff.noAutoRender, value); - }); - - test('passes in patchRange', () => { - const value = {patchNum: 'foo', basePatchNum: 'bar'}; - element.patchRange = value; - assert.equal(element.$.diff.patchRange, value); - }); - - test('passes in path', () => { - const value = 'some/file/path'; - element.path = value; - assert.equal(element.$.diff.path, value); - }); - - test('passes in prefs', () => { - const value = {}; - element.prefs = value; - assert.equal(element.$.diff.prefs, value); - }); - - test('passes in changeNum', () => { - const value = '12345'; - element.changeNum = value; - assert.equal(element.$.diff.changeNum, value); - }); - - test('passes in projectName', () => { - const value = 'Gerrit'; - element.projectName = value; - assert.equal(element.$.diff.projectName, value); - }); - - test('passes in displayLine', () => { - const value = true; - element.displayLine = value; - assert.equal(element.$.diff.displayLine, value); - }); - - test('passes in commitRange', () => { - const value = {}; - element.commitRange = value; - assert.equal(element.$.diff.commitRange, value); - }); - - test('passes in hidden', () => { - const value = true; - element.hidden = value; - assert.equal(element.$.diff.hidden, value); - assert.isNotNull(element.getAttribute('hidden')); - }); - - test('passes in noRenderOnPrefsChange', () => { - const value = true; - element.noRenderOnPrefsChange = value; - assert.equal(element.$.diff.noRenderOnPrefsChange, value); - }); - - test('passes in lineWrapping', () => { - const value = true; - element.lineWrapping = value; - assert.equal(element.$.diff.lineWrapping, value); - }); - - test('passes in viewMode', () => { - const value = 'SIDE_BY_SIDE'; - element.viewMode = value; - assert.equal(element.$.diff.viewMode, value); - }); - - test('passes in lineOfInterest', () => { - const value = {number: 123, leftSide: true}; - element.lineOfInterest = value; - assert.equal(element.$.diff.lineOfInterest, value); - }); - - suite('_reportDiff', () => { - let reportStub; - - setup(() => { - element = fixture('basic'); - element.patchRange = {basePatchNum: 1}; - reportStub = sandbox.stub(element.$.reporting, 'reportInteraction'); - }); - - test('null and content-less', () => { - element._reportDiff(null); - assert.isFalse(reportStub.called); - - element._reportDiff({}); - assert.isFalse(reportStub.called); - }); - - test('diff w/ no delta', () => { - const diff = { - content: [ - {ab: ['foo', 'bar']}, - {ab: ['baz', 'foo']}, + test('renders image diffs with a different file name', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot2.jpg', + 'index 2adc47d..f9c2f2c 100644', + '--- a/carrot.jpg', + '+++ b/carrot2.jpg', + 'Binary files differ', ], + content: [{skip: 66}], + binary: true, }; - element._reportDiff(diff); - assert.isTrue(reportStub.calledOnce); - assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero'); - assert.isUndefined(reportStub.lastCall.args[1]); + sandbox.stub(element.$.restAPI, 'getDiff') + .returns(Promise.resolve(mockDiff)); + + const rendered = () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); + + // Left image rendered with the parent commit's version of the file. + const leftImage = + element.$.diff.$.diffTable.querySelector('td.left img'); + const leftLabel = + element.$.diff.$.diffTable.querySelector('td.left label'); + const leftLabelContent = leftLabel.querySelector('.label'); + const leftLabelName = leftLabel.querySelector('.name'); + + const rightImage = + element.$.diff.$.diffTable.querySelector('td.right img'); + const rightLabel = element.$.diff.$.diffTable.querySelector( + 'td.right label'); + const rightLabelContent = rightLabel.querySelector('.label'); + const rightLabelName = rightLabel.querySelector('.name'); + + assert.isOk(rightLabelName); + assert.isOk(leftLabelName); + assert.equal(leftLabelName.textContent, mockDiff.meta_a.name); + assert.equal(rightLabelName.textContent, mockDiff.meta_b.name); + + let leftLoaded = false; + let rightLoaded = false; + + leftImage.addEventListener('load', () => { + assert.isOk(leftImage); + assert.equal(leftImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile1.body); + assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); + leftLoaded = true; + if (rightLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + + rightImage.addEventListener('load', () => { + assert.isOk(rightImage); + assert.equal(rightImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile2.body); + assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); + + rightLoaded = true; + if (leftLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + }; + + element.addEventListener('render', rendered); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); }); - test('diff w/ no rebase delta', () => { - const diff = { - content: [ - {ab: ['foo', 'bar']}, - {a: ['baz', 'foo']}, - {ab: ['foo', 'bar']}, - {a: ['baz', 'foo'], b: ['bar', 'baz']}, - {ab: ['foo', 'bar']}, - {b: ['baz', 'foo']}, - {ab: ['foo', 'bar']}, + test('renders added image', done => { + const mockDiff = { + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'ADDED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index 0000000..f9c2f2c 100644', + '--- /dev/null', + '+++ b/carrot.jpg', + 'Binary files differ', ], + content: [{skip: 66}], + binary: true, }; - element._reportDiff(diff); - assert.isTrue(reportStub.calledOnce); - assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero'); - assert.isUndefined(reportStub.lastCall.args[1]); + sandbox.stub(element.$.restAPI, 'getDiff') + .returns(Promise.resolve(mockDiff)); + + element.addEventListener('render', () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); + + const leftImage = + element.$.diff.$.diffTable.querySelector('td.left img'); + const rightImage = + element.$.diff.$.diffTable.querySelector('td.right img'); + + assert.isNotOk(leftImage); + assert.isOk(rightImage); + done(); + }); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); }); - test('diff w/ some rebase delta', () => { - const diff = { - content: [ - {ab: ['foo', 'bar']}, - {a: ['baz', 'foo'], due_to_rebase: true}, - {ab: ['foo', 'bar']}, - {a: ['baz', 'foo'], b: ['bar', 'baz']}, - {ab: ['foo', 'bar']}, - {b: ['baz', 'foo'], due_to_rebase: true}, - {ab: ['foo', 'bar']}, - {a: ['baz', 'foo']}, + test('renders removed image', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'DELETED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index f9c2f2c..0000000 100644', + '--- a/carrot.jpg', + '+++ /dev/null', + 'Binary files differ', ], + content: [{skip: 66}], + binary: true, }; - element._reportDiff(diff); - assert.isTrue(reportStub.calledOnce); - assert.isTrue(reportStub.calledWith( - 'rebase-percent-nonzero', - {percentRebaseDelta: 50} - )); + sandbox.stub(element.$.restAPI, 'getDiff') + .returns(Promise.resolve(mockDiff)); + + element.addEventListener('render', () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); + + const leftImage = + element.$.diff.$.diffTable.querySelector('td.left img'); + const rightImage = + element.$.diff.$.diffTable.querySelector('td.right img'); + + assert.isOk(leftImage); + assert.isNotOk(rightImage); + done(); + }); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); }); - test('diff w/ all rebase delta', () => { - const diff = {content: [{ - a: ['foo', 'bar'], - b: ['baz', 'foo'], - due_to_rebase: true, - }]}; - element._reportDiff(diff); - assert.isTrue(reportStub.calledOnce); - assert.isTrue(reportStub.calledWith( - 'rebase-percent-nonzero', - {percentRebaseDelta: 100} - )); - }); + test('does not render disallowed image type', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil', + lines: 560}, + intraline_status: 'OK', + change_type: 'DELETED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index f9c2f2c..0000000 100644', + '--- a/carrot.jpg', + '+++ /dev/null', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + mockFile1.type = 'image/jpeg-evil'; - test('diff against parent event', () => { - element.patchRange.basePatchNum = 'PARENT'; - const diff = {content: [{ - a: ['foo', 'bar'], - b: ['baz', 'foo'], - }]}; - element._reportDiff(diff); - assert.isTrue(reportStub.calledOnce); - assert.equal(reportStub.lastCall.args[0], 'diff-against-parent'); - assert.isUndefined(reportStub.lastCall.args[1]); + sandbox.stub(element.$.restAPI, 'getDiff') + .returns(Promise.resolve(mockDiff)); + + element.addEventListener('render', () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diff.$.diffBuilder._builder, GrDiffBuilderImage); + const leftImage = + element.$.diff.$.diffTable.querySelector('td.left img'); + assert.isNotOk(leftImage); + done(); + }); + + element.$.restAPI.getDiffPreferences().then(prefs => { + element.prefs = prefs; + element.reload(); + }); + }); + }); + }); + + test('delegates cancel()', () => { + const stub = sandbox.stub(element.$.diff, 'cancel'); + element.patchRange = {}; + element.reload(); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 0); + }); + + test('delegates getCursorStops()', () => { + const returnValue = [document.createElement('b')]; + const stub = sandbox.stub(element.$.diff, 'getCursorStops') + .returns(returnValue); + assert.equal(element.getCursorStops(), returnValue); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 0); + }); + + test('delegates isRangeSelected()', () => { + const returnValue = true; + const stub = sandbox.stub(element.$.diff, 'isRangeSelected') + .returns(returnValue); + assert.equal(element.isRangeSelected(), returnValue); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 0); + }); + + test('delegates toggleLeftDiff()', () => { + const stub = sandbox.stub(element.$.diff, 'toggleLeftDiff'); + element.toggleLeftDiff(); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 0); + }); + + suite('blame', () => { + setup(() => { + element = fixture('basic'); + }); + + test('clearBlame', () => { + element._blame = []; + const setBlameSpy = sandbox.spy(element.$.diff.$.diffBuilder, 'setBlame'); + element.clearBlame(); + assert.isNull(element._blame); + assert.isTrue(setBlameSpy.calledWithExactly(null)); + assert.equal(element.isBlameLoaded, false); + }); + + test('loadBlame', () => { + const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}]; + const showAlertStub = sinon.stub(); + element.addEventListener('show-alert', showAlertStub); + const getBlameStub = sandbox.stub(element.$.restAPI, 'getBlame') + .returns(Promise.resolve(mockBlame)); + element.changeNum = 42; + element.patchRange = {patchNum: 5, basePatchNum: 4}; + element.path = 'foo/bar.baz'; + return element.loadBlame().then(() => { + assert.isTrue(getBlameStub.calledWithExactly( + 42, 5, 'foo/bar.baz', true)); + assert.isFalse(showAlertStub.called); + assert.equal(element._blame, mockBlame); + assert.equal(element.isBlameLoaded, true); }); }); - test('comments sorting', () => { - const comments = [ - { - id: 'new_draft', - message: 'i do not like either of you', - __commentSide: 'left', - __draft: true, - updated: '2015-12-20 15:01:20.396000000', - }, - { - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-23 15:00:20.396000000', - line: 1, - __commentSide: 'left', - }, { - id: 'jacks_reply', - message: 'i like you, too', - updated: '2015-12-24 15:01:20.396000000', - __commentSide: 'left', - line: 1, - in_reply_to: 'sallys_confession', - }, - ]; - const sortedComments = element._sortComments(comments); - assert.equal(sortedComments[0], comments[1]); - assert.equal(sortedComments[1], comments[2]); - assert.equal(sortedComments[2], comments[0]); + test('loadBlame empty', () => { + const mockBlame = []; + const showAlertStub = sinon.stub(); + element.addEventListener('show-alert', showAlertStub); + sandbox.stub(element.$.restAPI, 'getBlame') + .returns(Promise.resolve(mockBlame)); + element.changeNum = 42; + element.patchRange = {patchNum: 5, basePatchNum: 4}; + element.path = 'foo/bar.baz'; + return element.loadBlame() + .then(() => { + assert.isTrue(false, 'Promise should not resolve'); + }) + .catch(() => { + assert.isTrue(showAlertStub.calledOnce); + assert.isNull(element._blame); + assert.equal(element.isBlameLoaded, false); + }); + }); + }); + + test('getThreadEls() returns .comment-threads', () => { + const threadEl = document.createElement('div'); + threadEl.className = 'comment-thread'; + dom(element.$.diff).appendChild(threadEl); + assert.deepEqual(element.getThreadEls(), [threadEl]); + }); + + test('delegates addDraftAtLine(el)', () => { + const param0 = document.createElement('b'); + const stub = sandbox.stub(element.$.diff, 'addDraftAtLine'); + element.addDraftAtLine(param0); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 1); + assert.equal(stub.lastCall.args[0], param0); + }); + + test('delegates clearDiffContent()', () => { + const stub = sandbox.stub(element.$.diff, 'clearDiffContent'); + element.clearDiffContent(); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 0); + }); + + test('delegates expandAllContext()', () => { + const stub = sandbox.stub(element.$.diff, 'expandAllContext'); + element.expandAllContext(); + assert.isTrue(stub.calledOnce); + assert.equal(stub.lastCall.args.length, 0); + }); + + test('passes in changeNum', () => { + const value = '12345'; + element.changeNum = value; + assert.equal(element.$.diff.changeNum, value); + }); + + test('passes in noAutoRender', () => { + const value = true; + element.noAutoRender = value; + assert.equal(element.$.diff.noAutoRender, value); + }); + + test('passes in patchRange', () => { + const value = {patchNum: 'foo', basePatchNum: 'bar'}; + element.patchRange = value; + assert.equal(element.$.diff.patchRange, value); + }); + + test('passes in path', () => { + const value = 'some/file/path'; + element.path = value; + assert.equal(element.$.diff.path, value); + }); + + test('passes in prefs', () => { + const value = {}; + element.prefs = value; + assert.equal(element.$.diff.prefs, value); + }); + + test('passes in changeNum', () => { + const value = '12345'; + element.changeNum = value; + assert.equal(element.$.diff.changeNum, value); + }); + + test('passes in projectName', () => { + const value = 'Gerrit'; + element.projectName = value; + assert.equal(element.$.diff.projectName, value); + }); + + test('passes in displayLine', () => { + const value = true; + element.displayLine = value; + assert.equal(element.$.diff.displayLine, value); + }); + + test('passes in commitRange', () => { + const value = {}; + element.commitRange = value; + assert.equal(element.$.diff.commitRange, value); + }); + + test('passes in hidden', () => { + const value = true; + element.hidden = value; + assert.equal(element.$.diff.hidden, value); + assert.isNotNull(element.getAttribute('hidden')); + }); + + test('passes in noRenderOnPrefsChange', () => { + const value = true; + element.noRenderOnPrefsChange = value; + assert.equal(element.$.diff.noRenderOnPrefsChange, value); + }); + + test('passes in lineWrapping', () => { + const value = true; + element.lineWrapping = value; + assert.equal(element.$.diff.lineWrapping, value); + }); + + test('passes in viewMode', () => { + const value = 'SIDE_BY_SIDE'; + element.viewMode = value; + assert.equal(element.$.diff.viewMode, value); + }); + + test('passes in lineOfInterest', () => { + const value = {number: 123, leftSide: true}; + element.lineOfInterest = value; + assert.equal(element.$.diff.lineOfInterest, value); + }); + + suite('_reportDiff', () => { + let reportStub; + + setup(() => { + element = fixture('basic'); + element.patchRange = {basePatchNum: 1}; + reportStub = sandbox.stub(element.$.reporting, 'reportInteraction'); }); - test('_createThreads', () => { - const comments = [ - { - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-23 15:00:20.396000000', - line: 1, - __commentSide: 'left', - }, { - id: 'jacks_reply', - message: 'i like you, too', - updated: '2015-12-24 15:01:20.396000000', - __commentSide: 'left', - line: 1, - in_reply_to: 'sallys_confession', - }, - { - id: 'new_draft', - message: 'i do not like either of you', - __commentSide: 'left', - __draft: true, - updated: '2015-12-20 15:01:20.396000000', - }, - ]; + test('null and content-less', () => { + element._reportDiff(null); + assert.isFalse(reportStub.called); - const actualThreads = element._createThreads(comments); - - assert.equal(actualThreads.length, 2); - - assert.equal( - actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000'); - assert.equal(actualThreads[0].commentSide, 'left'); - assert.equal(actualThreads[0].comments.length, 2); - assert.deepEqual(actualThreads[0].comments[0], comments[0]); - assert.deepEqual(actualThreads[0].comments[1], comments[1]); - assert.equal(actualThreads[0].patchNum, undefined); - assert.equal(actualThreads[0].rootId, 'sallys_confession'); - assert.equal(actualThreads[0].lineNum, 1); - - assert.equal( - actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000'); - assert.equal(actualThreads[1].commentSide, 'left'); - assert.equal(actualThreads[1].comments.length, 1); - assert.deepEqual(actualThreads[1].comments[0], comments[2]); - assert.equal(actualThreads[1].patchNum, undefined); - assert.equal(actualThreads[1].rootId, 'new_draft'); - assert.equal(actualThreads[1].lineNum, undefined); + element._reportDiff({}); + assert.isFalse(reportStub.called); }); - test('_createThreads inherits patchNum and range', () => { - const comments = [{ - id: 'betsys_confession', + test('diff w/ no delta', () => { + const diff = { + content: [ + {ab: ['foo', 'bar']}, + {ab: ['baz', 'foo']}, + ], + }; + element._reportDiff(diff); + assert.isTrue(reportStub.calledOnce); + assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero'); + assert.isUndefined(reportStub.lastCall.args[1]); + }); + + test('diff w/ no rebase delta', () => { + const diff = { + content: [ + {ab: ['foo', 'bar']}, + {a: ['baz', 'foo']}, + {ab: ['foo', 'bar']}, + {a: ['baz', 'foo'], b: ['bar', 'baz']}, + {ab: ['foo', 'bar']}, + {b: ['baz', 'foo']}, + {ab: ['foo', 'bar']}, + ], + }; + element._reportDiff(diff); + assert.isTrue(reportStub.calledOnce); + assert.equal(reportStub.lastCall.args[0], 'rebase-percent-zero'); + assert.isUndefined(reportStub.lastCall.args[1]); + }); + + test('diff w/ some rebase delta', () => { + const diff = { + content: [ + {ab: ['foo', 'bar']}, + {a: ['baz', 'foo'], due_to_rebase: true}, + {ab: ['foo', 'bar']}, + {a: ['baz', 'foo'], b: ['bar', 'baz']}, + {ab: ['foo', 'bar']}, + {b: ['baz', 'foo'], due_to_rebase: true}, + {ab: ['foo', 'bar']}, + {a: ['baz', 'foo']}, + ], + }; + element._reportDiff(diff); + assert.isTrue(reportStub.calledOnce); + assert.isTrue(reportStub.calledWith( + 'rebase-percent-nonzero', + {percentRebaseDelta: 50} + )); + }); + + test('diff w/ all rebase delta', () => { + const diff = {content: [{ + a: ['foo', 'bar'], + b: ['baz', 'foo'], + due_to_rebase: true, + }]}; + element._reportDiff(diff); + assert.isTrue(reportStub.calledOnce); + assert.isTrue(reportStub.calledWith( + 'rebase-percent-nonzero', + {percentRebaseDelta: 100} + )); + }); + + test('diff against parent event', () => { + element.patchRange.basePatchNum = 'PARENT'; + const diff = {content: [{ + a: ['foo', 'bar'], + b: ['baz', 'foo'], + }]}; + element._reportDiff(diff); + assert.isTrue(reportStub.calledOnce); + assert.equal(reportStub.lastCall.args[0], 'diff-against-parent'); + assert.isUndefined(reportStub.lastCall.args[1]); + }); + }); + + test('comments sorting', () => { + const comments = [ + { + id: 'new_draft', + message: 'i do not like either of you', + __commentSide: 'left', + __draft: true, + updated: '2015-12-20 15:01:20.396000000', + }, + { + id: 'sallys_confession', message: 'i like you, jack', - updated: '2015-12-24 15:00:10.396000000', - range: { - start_line: 1, - start_character: 1, - end_line: 1, - end_character: 2, - }, - patch_set: 5, + updated: '2015-12-23 15:00:20.396000000', + line: 1, + __commentSide: 'left', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:01:20.396000000', __commentSide: 'left', line: 1, - }]; + in_reply_to: 'sallys_confession', + }, + ]; + const sortedComments = element._sortComments(comments); + assert.equal(sortedComments[0], comments[1]); + assert.equal(sortedComments[1], comments[2]); + assert.equal(sortedComments[2], comments[0]); + }); - const expectedThreads = [ - { - start_datetime: '2015-12-24 15:00:10.396000000', - commentSide: 'left', - comments: [{ - id: 'betsys_confession', - message: 'i like you, jack', - updated: '2015-12-24 15:00:10.396000000', - range: { - start_line: 1, - start_character: 1, - end_line: 1, - end_character: 2, - }, - patch_set: 5, - __commentSide: 'left', - line: 1, - }], - patchNum: 5, - rootId: 'betsys_confession', + test('_createThreads', () => { + const comments = [ + { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + line: 1, + __commentSide: 'left', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:01:20.396000000', + __commentSide: 'left', + line: 1, + in_reply_to: 'sallys_confession', + }, + { + id: 'new_draft', + message: 'i do not like either of you', + __commentSide: 'left', + __draft: true, + updated: '2015-12-20 15:01:20.396000000', + }, + ]; + + const actualThreads = element._createThreads(comments); + + assert.equal(actualThreads.length, 2); + + assert.equal( + actualThreads[0].start_datetime, '2015-12-23 15:00:20.396000000'); + assert.equal(actualThreads[0].commentSide, 'left'); + assert.equal(actualThreads[0].comments.length, 2); + assert.deepEqual(actualThreads[0].comments[0], comments[0]); + assert.deepEqual(actualThreads[0].comments[1], comments[1]); + assert.equal(actualThreads[0].patchNum, undefined); + assert.equal(actualThreads[0].rootId, 'sallys_confession'); + assert.equal(actualThreads[0].lineNum, 1); + + assert.equal( + actualThreads[1].start_datetime, '2015-12-20 15:01:20.396000000'); + assert.equal(actualThreads[1].commentSide, 'left'); + assert.equal(actualThreads[1].comments.length, 1); + assert.deepEqual(actualThreads[1].comments[0], comments[2]); + assert.equal(actualThreads[1].patchNum, undefined); + assert.equal(actualThreads[1].rootId, 'new_draft'); + assert.equal(actualThreads[1].lineNum, undefined); + }); + + test('_createThreads inherits patchNum and range', () => { + const comments = [{ + id: 'betsys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:10.396000000', + range: { + start_line: 1, + start_character: 1, + end_line: 1, + end_character: 2, + }, + patch_set: 5, + __commentSide: 'left', + line: 1, + }]; + + const expectedThreads = [ + { + start_datetime: '2015-12-24 15:00:10.396000000', + commentSide: 'left', + comments: [{ + id: 'betsys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:10.396000000', range: { start_line: 1, start_character: 1, end_line: 1, end_character: 2, }, - lineNum: 1, - isOnParent: false, + patch_set: 5, + __commentSide: 'left', + line: 1, + }], + patchNum: 5, + rootId: 'betsys_confession', + range: { + start_line: 1, + start_character: 1, + end_line: 1, + end_character: 2, }, - ]; + lineNum: 1, + isOnParent: false, + }, + ]; - assert.deepEqual( - element._createThreads(comments), - expectedThreads); - }); + assert.deepEqual( + element._createThreads(comments), + expectedThreads); + }); - test('_createThreads does not thread unrelated comments at same location', - () => { - const comments = [ - { - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-23 15:00:20.396000000', - __commentSide: 'left', - }, { - id: 'jacks_reply', - message: 'i like you, too', - updated: '2015-12-24 15:01:20.396000000', - __commentSide: 'left', - }, - ]; - assert.equal(element._createThreads(comments).length, 2); - }); - - test('_createThreads derives isOnParent using side from first comment', - () => { - const comments = [ - { - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-23 15:00:20.396000000', - // line: 1, - // __commentSide: 'left', - }, { - id: 'jacks_reply', - message: 'i like you, too', - updated: '2015-12-24 15:01:20.396000000', - // __commentSide: 'left', - // line: 1, - in_reply_to: 'sallys_confession', - }, - ]; - - assert.equal(element._createThreads(comments)[0].isOnParent, false); - - comments[0].side = 'REVISION'; - assert.equal(element._createThreads(comments)[0].isOnParent, false); - - comments[0].side = 'PARENT'; - assert.equal(element._createThreads(comments)[0].isOnParent, true); - }); - - test('_getOrCreateThread', () => { - const commentSide = 'left'; - - assert.isOk(element._getOrCreateThread('2', 3, - commentSide, undefined, false)); - - let threads = Polymer.dom(element.$.diff) - .queryDistributedElements('gr-comment-thread'); - - assert.equal(threads.length, 1); - assert.equal(threads[0].commentSide, commentSide); - assert.equal(threads[0].range, undefined); - assert.equal(threads[0].isOnParent, false); - assert.equal(threads[0].patchNum, 2); - - // Try to fetch a thread with a different range. - const range = { - start_line: 1, - start_character: 1, - end_line: 1, - end_character: 3, - }; - - assert.isOk(element._getOrCreateThread( - '3', 1, commentSide, range, true)); - - threads = Polymer.dom(element.$.diff) - .queryDistributedElements('gr-comment-thread'); - - assert.equal(threads.length, 2); - assert.equal(threads[1].commentSide, commentSide); - assert.equal(threads[1].range, range); - assert.equal(threads[1].isOnParent, true); - assert.equal(threads[1].patchNum, 3); - }); - - test('_filterThreadElsForLocation with no threads', () => { - const line = {beforeNumber: 3, afterNumber: 5}; - - const threads = []; - assert.deepEqual(element._filterThreadElsForLocation(threads, line), []); - assert.deepEqual(element._filterThreadElsForLocation(threads, line, - Gerrit.DiffSide.LEFT), []); - assert.deepEqual(element._filterThreadElsForLocation(threads, line, - Gerrit.DiffSide.RIGHT), []); - }); - - test('_filterThreadElsForLocation for line comments', () => { - const line = {beforeNumber: 3, afterNumber: 5}; - - const l3 = document.createElement('div'); - l3.setAttribute('line-num', 3); - l3.setAttribute('comment-side', 'left'); - - const l5 = document.createElement('div'); - l5.setAttribute('line-num', 5); - l5.setAttribute('comment-side', 'left'); - - const r3 = document.createElement('div'); - r3.setAttribute('line-num', 3); - r3.setAttribute('comment-side', 'right'); - - const r5 = document.createElement('div'); - r5.setAttribute('line-num', 5); - r5.setAttribute('comment-side', 'right'); - - const threadEls = [l3, l5, r3, r5]; - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line), - [l3, r5]); - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, - Gerrit.DiffSide.LEFT), [l3]); - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, - Gerrit.DiffSide.RIGHT), [r5]); - }); - - test('_filterThreadElsForLocation for file comments', () => { - const line = {beforeNumber: 'FILE', afterNumber: 'FILE'}; - - const l = document.createElement('div'); - l.setAttribute('comment-side', 'left'); - l.setAttribute('line-num', 'FILE'); - - const r = document.createElement('div'); - r.setAttribute('comment-side', 'right'); - r.setAttribute('line-num', 'FILE'); - - const threadEls = [l, r]; - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line), - [l, r]); - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, - Gerrit.DiffSide.BOTH), [l, r]); - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, - Gerrit.DiffSide.LEFT), [l]); - assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, - Gerrit.DiffSide.RIGHT), [r]); - }); - - suite('syntax layer with syntax_highlighting on', () => { - setup(() => { - const prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - context: -1, - syntax_highlighting: true, - }; - element.patchRange = {}; - element.prefs = prefs; - }); - - test('gr-diff-host provides syntax highlighting layer to gr-diff', () => { - element.reload(); - assert.equal(element.$.diff.layers[0], element.$.syntaxLayer); - }); - - test('rendering normal-sized diff does not disable syntax', () => { - element.diff = { - content: [{ - a: ['foo'], - }], - }; - assert.isTrue(element.$.syntaxLayer.enabled); - }); - - test('rendering large diff disables syntax', () => { - // Before it renders, set the first diff line to 500 '*' characters. - element.diff = { - content: [{ - a: [new Array(501).join('*')], - }], - }; - assert.isFalse(element.$.syntaxLayer.enabled); - }); - - test('starts syntax layer processing on render event', done => { - sandbox.stub(element.$.syntaxLayer, 'process') - .returns(Promise.resolve()); - sandbox.stub(element.$.restAPI, 'getDiff').returns( - Promise.resolve({content: []})); - element.reload(); - setTimeout(() => { - element.dispatchEvent( - new CustomEvent('render', {bubbles: true, composed: true})); - assert.isTrue(element.$.syntaxLayer.process.called); - done(); - }); - }); - }); - - suite('syntax layer with syntax_highlgihting off', () => { - setup(() => { - const prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - context: -1, - }; - element.diff = { - content: [{ - a: ['foo'], - }], - }; - element.patchRange = {}; - element.prefs = prefs; - }); - - test('gr-diff-host provides syntax highlighting layer', () => { - element.reload(); - assert.equal(element.$.diff.layers[0], element.$.syntaxLayer); - }); - - test('syntax layer should be disabled', () => { - assert.isFalse(element.$.syntaxLayer.enabled); - }); - - test('still disabled for large diff', () => { - // Before it renders, set the first diff line to 500 '*' characters. - element.diff = { - content: [{ - a: [new Array(501).join('*')], - }], - }; - assert.isFalse(element.$.syntaxLayer.enabled); - }); - }); - - suite('coverage layer', () => { - let notifyStub; - setup(() => { - notifyStub = sinon.stub(); - stub('gr-js-api-interface', { - getCoverageAnnotationApi() { - return Promise.resolve({ - notify: notifyStub, - getCoverageProvider() { - return () => Promise.resolve([ - { - type: 'COVERED', - side: 'right', - code_range: { - start_line: 1, - end_line: 2, - }, - }, - { - type: 'NOT_COVERED', - side: 'right', - code_range: { - start_line: 3, - end_line: 4, - }, - }, - ]); - }, - }); + test('_createThreads does not thread unrelated comments at same location', + () => { + const comments = [ + { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + __commentSide: 'left', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:01:20.396000000', + __commentSide: 'left', }, - }); - element = fixture('basic'); - const prefs = { - line_length: 10, - show_tabs: true, - tab_size: 4, - context: -1, - }; - element.diff = { - content: [{ - a: ['foo'], - }], - }; - element.patchRange = {}; - element.prefs = prefs; + ]; + assert.equal(element._createThreads(comments).length, 2); }); - test('getCoverageAnnotationApi should be called', done => { - element.reload(); - flush(() => { - assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce); - done(); - }); + test('_createThreads derives isOnParent using side from first comment', + () => { + const comments = [ + { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-23 15:00:20.396000000', + // line: 1, + // __commentSide: 'left', + }, { + id: 'jacks_reply', + message: 'i like you, too', + updated: '2015-12-24 15:01:20.396000000', + // __commentSide: 'left', + // line: 1, + in_reply_to: 'sallys_confession', + }, + ]; + + assert.equal(element._createThreads(comments)[0].isOnParent, false); + + comments[0].side = 'REVISION'; + assert.equal(element._createThreads(comments)[0].isOnParent, false); + + comments[0].side = 'PARENT'; + assert.equal(element._createThreads(comments)[0].isOnParent, true); }); - test('coverageRangeChanged should be called', done => { - element.reload(); - flush(() => { - assert.equal(notifyStub.callCount, 2); - done(); - }); - }); + test('_getOrCreateThread', () => { + const commentSide = 'left'; + + assert.isOk(element._getOrCreateThread('2', 3, + commentSide, undefined, false)); + + let threads = dom(element.$.diff) + .queryDistributedElements('gr-comment-thread'); + + assert.equal(threads.length, 1); + assert.equal(threads[0].commentSide, commentSide); + assert.equal(threads[0].range, undefined); + assert.equal(threads[0].isOnParent, false); + assert.equal(threads[0].patchNum, 2); + + // Try to fetch a thread with a different range. + const range = { + start_line: 1, + start_character: 1, + end_line: 1, + end_character: 3, + }; + + assert.isOk(element._getOrCreateThread( + '3', 1, commentSide, range, true)); + + threads = dom(element.$.diff) + .queryDistributedElements('gr-comment-thread'); + + assert.equal(threads.length, 2); + assert.equal(threads[1].commentSide, commentSide); + assert.equal(threads[1].range, range); + assert.equal(threads[1].isOnParent, true); + assert.equal(threads[1].patchNum, 3); + }); + + test('_filterThreadElsForLocation with no threads', () => { + const line = {beforeNumber: 3, afterNumber: 5}; + + const threads = []; + assert.deepEqual(element._filterThreadElsForLocation(threads, line), []); + assert.deepEqual(element._filterThreadElsForLocation(threads, line, + Gerrit.DiffSide.LEFT), []); + assert.deepEqual(element._filterThreadElsForLocation(threads, line, + Gerrit.DiffSide.RIGHT), []); + }); + + test('_filterThreadElsForLocation for line comments', () => { + const line = {beforeNumber: 3, afterNumber: 5}; + + const l3 = document.createElement('div'); + l3.setAttribute('line-num', 3); + l3.setAttribute('comment-side', 'left'); + + const l5 = document.createElement('div'); + l5.setAttribute('line-num', 5); + l5.setAttribute('comment-side', 'left'); + + const r3 = document.createElement('div'); + r3.setAttribute('line-num', 3); + r3.setAttribute('comment-side', 'right'); + + const r5 = document.createElement('div'); + r5.setAttribute('line-num', 5); + r5.setAttribute('comment-side', 'right'); + + const threadEls = [l3, l5, r3, r5]; + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line), + [l3, r5]); + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, + Gerrit.DiffSide.LEFT), [l3]); + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, + Gerrit.DiffSide.RIGHT), [r5]); + }); + + test('_filterThreadElsForLocation for file comments', () => { + const line = {beforeNumber: 'FILE', afterNumber: 'FILE'}; + + const l = document.createElement('div'); + l.setAttribute('comment-side', 'left'); + l.setAttribute('line-num', 'FILE'); + + const r = document.createElement('div'); + r.setAttribute('comment-side', 'right'); + r.setAttribute('line-num', 'FILE'); + + const threadEls = [l, r]; + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line), + [l, r]); + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, + Gerrit.DiffSide.BOTH), [l, r]); + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, + Gerrit.DiffSide.LEFT), [l]); + assert.deepEqual(element._filterThreadElsForLocation(threadEls, line, + Gerrit.DiffSide.RIGHT), [r]); + }); + + suite('syntax layer with syntax_highlighting on', () => { + setup(() => { + const prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + syntax_highlighting: true, + }; + element.patchRange = {}; + element.prefs = prefs; }); - suite('trailing newlines', () => { - setup(() => { - }); + test('gr-diff-host provides syntax highlighting layer to gr-diff', () => { + element.reload(); + assert.equal(element.$.diff.layers[0], element.$.syntaxLayer); + }); - suite('_lastChunkForSide', () => { - test('deltas', () => { - const diff = {content: [ - {a: ['foo', 'bar'], b: ['baz']}, - {ab: ['foo', 'bar', 'baz']}, - {b: ['foo']}, - ]}; - assert.equal(element._lastChunkForSide(diff, false), diff.content[2]); - assert.equal(element._lastChunkForSide(diff, true), diff.content[1]); + test('rendering normal-sized diff does not disable syntax', () => { + element.diff = { + content: [{ + a: ['foo'], + }], + }; + assert.isTrue(element.$.syntaxLayer.enabled); + }); - diff.content.push({a: ['foo'], b: ['bar']}); - assert.equal(element._lastChunkForSide(diff, false), diff.content[3]); - assert.equal(element._lastChunkForSide(diff, true), diff.content[3]); - }); + test('rendering large diff disables syntax', () => { + // Before it renders, set the first diff line to 500 '*' characters. + element.diff = { + content: [{ + a: [new Array(501).join('*')], + }], + }; + assert.isFalse(element.$.syntaxLayer.enabled); + }); - test('addition with a undefined', () => { - const diff = {content: [ - {b: ['foo', 'bar', 'baz']}, - ]}; - assert.equal(element._lastChunkForSide(diff, false), diff.content[0]); - assert.isNull(element._lastChunkForSide(diff, true)); - }); - - test('addition with a empty', () => { - const diff = {content: [ - {a: [], b: ['foo', 'bar', 'baz']}, - ]}; - assert.equal(element._lastChunkForSide(diff, false), diff.content[0]); - assert.isNull(element._lastChunkForSide(diff, true)); - }); - - test('deletion with b undefined', () => { - const diff = {content: [ - {a: ['foo', 'bar', 'baz']}, - ]}; - assert.isNull(element._lastChunkForSide(diff, false)); - assert.equal(element._lastChunkForSide(diff, true), diff.content[0]); - }); - - test('deletion with b empty', () => { - const diff = {content: [ - {a: ['foo', 'bar', 'baz'], b: []}, - ]}; - assert.isNull(element._lastChunkForSide(diff, false)); - assert.equal(element._lastChunkForSide(diff, true), diff.content[0]); - }); - - test('empty', () => { - const diff = {content: []}; - assert.isNull(element._lastChunkForSide(diff, false)); - assert.isNull(element._lastChunkForSide(diff, true)); - }); - }); - - suite('_hasTrailingNewlines', () => { - test('shared no trailing', () => { - const diff = undefined; - sandbox.stub(element, '_lastChunkForSide') - .returns({ab: ['foo', 'bar']}); - assert.isFalse(element._hasTrailingNewlines(diff, false)); - assert.isFalse(element._hasTrailingNewlines(diff, true)); - }); - - test('delta trailing in right', () => { - const diff = undefined; - sandbox.stub(element, '_lastChunkForSide') - .returns({a: ['foo', 'bar'], b: ['baz', '']}); - assert.isTrue(element._hasTrailingNewlines(diff, false)); - assert.isFalse(element._hasTrailingNewlines(diff, true)); - }); - - test('addition', () => { - const diff = undefined; - sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => { - if (leftSide) { return null; } - return {b: ['foo', '']}; - }); - assert.isTrue(element._hasTrailingNewlines(diff, false)); - assert.isNull(element._hasTrailingNewlines(diff, true)); - }); - - test('deletion', () => { - const diff = undefined; - sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => { - if (!leftSide) { return null; } - return {a: ['foo']}; - }); - assert.isNull(element._hasTrailingNewlines(diff, false)); - assert.isFalse(element._hasTrailingNewlines(diff, true)); - }); + test('starts syntax layer processing on render event', done => { + sandbox.stub(element.$.syntaxLayer, 'process') + .returns(Promise.resolve()); + sandbox.stub(element.$.restAPI, 'getDiff').returns( + Promise.resolve({content: []})); + element.reload(); + setTimeout(() => { + element.dispatchEvent( + new CustomEvent('render', {bubbles: true, composed: true})); + assert.isTrue(element.$.syntaxLayer.process.called); + done(); }); }); }); + + suite('syntax layer with syntax_highlgihting off', () => { + setup(() => { + const prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + }; + element.diff = { + content: [{ + a: ['foo'], + }], + }; + element.patchRange = {}; + element.prefs = prefs; + }); + + test('gr-diff-host provides syntax highlighting layer', () => { + element.reload(); + assert.equal(element.$.diff.layers[0], element.$.syntaxLayer); + }); + + test('syntax layer should be disabled', () => { + assert.isFalse(element.$.syntaxLayer.enabled); + }); + + test('still disabled for large diff', () => { + // Before it renders, set the first diff line to 500 '*' characters. + element.diff = { + content: [{ + a: [new Array(501).join('*')], + }], + }; + assert.isFalse(element.$.syntaxLayer.enabled); + }); + }); + + suite('coverage layer', () => { + let notifyStub; + setup(() => { + notifyStub = sinon.stub(); + stub('gr-js-api-interface', { + getCoverageAnnotationApi() { + return Promise.resolve({ + notify: notifyStub, + getCoverageProvider() { + return () => Promise.resolve([ + { + type: 'COVERED', + side: 'right', + code_range: { + start_line: 1, + end_line: 2, + }, + }, + { + type: 'NOT_COVERED', + side: 'right', + code_range: { + start_line: 3, + end_line: 4, + }, + }, + ]); + }, + }); + }, + }); + element = fixture('basic'); + const prefs = { + line_length: 10, + show_tabs: true, + tab_size: 4, + context: -1, + }; + element.diff = { + content: [{ + a: ['foo'], + }], + }; + element.patchRange = {}; + element.prefs = prefs; + }); + + test('getCoverageAnnotationApi should be called', done => { + element.reload(); + flush(() => { + assert.isTrue(element.$.jsAPI.getCoverageAnnotationApi.calledOnce); + done(); + }); + }); + + test('coverageRangeChanged should be called', done => { + element.reload(); + flush(() => { + assert.equal(notifyStub.callCount, 2); + done(); + }); + }); + }); + + suite('trailing newlines', () => { + setup(() => { + }); + + suite('_lastChunkForSide', () => { + test('deltas', () => { + const diff = {content: [ + {a: ['foo', 'bar'], b: ['baz']}, + {ab: ['foo', 'bar', 'baz']}, + {b: ['foo']}, + ]}; + assert.equal(element._lastChunkForSide(diff, false), diff.content[2]); + assert.equal(element._lastChunkForSide(diff, true), diff.content[1]); + + diff.content.push({a: ['foo'], b: ['bar']}); + assert.equal(element._lastChunkForSide(diff, false), diff.content[3]); + assert.equal(element._lastChunkForSide(diff, true), diff.content[3]); + }); + + test('addition with a undefined', () => { + const diff = {content: [ + {b: ['foo', 'bar', 'baz']}, + ]}; + assert.equal(element._lastChunkForSide(diff, false), diff.content[0]); + assert.isNull(element._lastChunkForSide(diff, true)); + }); + + test('addition with a empty', () => { + const diff = {content: [ + {a: [], b: ['foo', 'bar', 'baz']}, + ]}; + assert.equal(element._lastChunkForSide(diff, false), diff.content[0]); + assert.isNull(element._lastChunkForSide(diff, true)); + }); + + test('deletion with b undefined', () => { + const diff = {content: [ + {a: ['foo', 'bar', 'baz']}, + ]}; + assert.isNull(element._lastChunkForSide(diff, false)); + assert.equal(element._lastChunkForSide(diff, true), diff.content[0]); + }); + + test('deletion with b empty', () => { + const diff = {content: [ + {a: ['foo', 'bar', 'baz'], b: []}, + ]}; + assert.isNull(element._lastChunkForSide(diff, false)); + assert.equal(element._lastChunkForSide(diff, true), diff.content[0]); + }); + + test('empty', () => { + const diff = {content: []}; + assert.isNull(element._lastChunkForSide(diff, false)); + assert.isNull(element._lastChunkForSide(diff, true)); + }); + }); + + suite('_hasTrailingNewlines', () => { + test('shared no trailing', () => { + const diff = undefined; + sandbox.stub(element, '_lastChunkForSide') + .returns({ab: ['foo', 'bar']}); + assert.isFalse(element._hasTrailingNewlines(diff, false)); + assert.isFalse(element._hasTrailingNewlines(diff, true)); + }); + + test('delta trailing in right', () => { + const diff = undefined; + sandbox.stub(element, '_lastChunkForSide') + .returns({a: ['foo', 'bar'], b: ['baz', '']}); + assert.isTrue(element._hasTrailingNewlines(diff, false)); + assert.isFalse(element._hasTrailingNewlines(diff, true)); + }); + + test('addition', () => { + const diff = undefined; + sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => { + if (leftSide) { return null; } + return {b: ['foo', '']}; + }); + assert.isTrue(element._hasTrailingNewlines(diff, false)); + assert.isNull(element._hasTrailingNewlines(diff, true)); + }); + + test('deletion', () => { + const diff = undefined; + sandbox.stub(element, '_lastChunkForSide', (diff, leftSide) => { + if (!leftSide) { return null; } + return {a: ['foo']}; + }); + assert.isNull(element._hasTrailingNewlines(diff, false)); + assert.isFalse(element._hasTrailingNewlines(diff, true)); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js index 68bca23..acd9457 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.js
@@ -14,65 +14,74 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrDiffModeSelector extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-diff-mode-selector'; } +import '@polymer/iron-icon/iron-icon.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-mode-selector_html.js'; - static get properties() { - return { - mode: { - type: String, - notify: true, +/** @extends Polymer.Element */ +class GrDiffModeSelector extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-mode-selector'; } + + static get properties() { + return { + mode: { + type: String, + notify: true, + }, + + /** + * If set to true, the user's preference will be updated every time a + * button is tapped. Don't set to true if there is no user. + */ + saveOnChange: { + type: Boolean, + value: false, + }, + + /** @type {?} */ + _VIEW_MODES: { + type: Object, + readOnly: true, + value: { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', }, - - /** - * If set to true, the user's preference will be updated every time a - * button is tapped. Don't set to true if there is no user. - */ - saveOnChange: { - type: Boolean, - value: false, - }, - - /** @type {?} */ - _VIEW_MODES: { - type: Object, - readOnly: true, - value: { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }, - }, - }; - } - - /** - * Set the mode. If save on change is enabled also update the preference. - */ - setMode(newMode) { - if (this.saveOnChange && this.mode && this.mode !== newMode) { - this.$.restAPI.savePreferences({diff_view: newMode}); - } - this.mode = newMode; - } - - _computeSelectedClass(diffViewMode, buttonViewMode) { - return buttonViewMode === diffViewMode ? 'selected' : ''; - } - - _handleSideBySideTap() { - this.setMode(this._VIEW_MODES.SIDE_BY_SIDE); - } - - _handleUnifiedTap() { - this.setMode(this._VIEW_MODES.UNIFIED); - } + }, + }; } - customElements.define(GrDiffModeSelector.is, GrDiffModeSelector); -})(); + /** + * Set the mode. If save on change is enabled also update the preference. + */ + setMode(newMode) { + if (this.saveOnChange && this.mode && this.mode !== newMode) { + this.$.restAPI.savePreferences({diff_view: newMode}); + } + this.mode = newMode; + } + + _computeSelectedClass(diffViewMode, buttonViewMode) { + return buttonViewMode === diffViewMode ? 'selected' : ''; + } + + _handleSideBySideTap() { + this.setMode(this._VIEW_MODES.SIDE_BY_SIDE); + } + + _handleUnifiedTap() { + this.setMode(this._VIEW_MODES.UNIFIED); + } +} + +customElements.define(GrDiffModeSelector.is, GrDiffModeSelector);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js index 47cf771..5fe516c 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-diff-mode-selector"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { /* Used to remove horizontal whitespace between the icons. */ @@ -36,25 +30,11 @@ width: 1.3rem; } </style> - <gr-button - id="sideBySideBtn" - link - has-tooltip - class$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]" - title="Side-by-side diff" - on-click="_handleSideBySideTap"> + <gr-button id="sideBySideBtn" link="" has-tooltip="" class\$="[[_computeSelectedClass(mode, _VIEW_MODES.SIDE_BY_SIDE)]]" title="Side-by-side diff" on-click="_handleSideBySideTap"> <iron-icon icon="gr-icons:side-by-side"></iron-icon> </gr-button> - <gr-button - id="unifiedBtn" - link - has-tooltip - title="Unified diff" - class$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]" - on-click="_handleUnifiedTap"> + <gr-button id="unifiedBtn" link="" has-tooltip="" title="Unified diff" class\$="[[_computeSelectedClass(mode, _VIEW_MODES.UNIFIED)]]" on-click="_handleUnifiedTap"> <iron-icon icon="gr-icons:unified"></iron-icon> </gr-button> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-diff-mode-selector.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html index 2f3d262..921bc74 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.html
@@ -19,18 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-mode-selector</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-diff-mode-selector.html"> +<script type="module" src="./gr-diff-mode-selector.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-diff-mode-selector.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -38,52 +44,55 @@ </template> </test-fixture> -<script> - suite('gr-diff-mode-selector tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-diff-mode-selector.js'; +suite('gr-diff-mode-selector tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('_computeSelectedClass', () => { - assert.equal( - element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'), - 'selected'); - assert.equal( - element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), ''); - }); - - test('setMode', () => { - const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences'); - - // Setting the mode initially does not save prefs. - element.saveOnChange = true; - element.setMode('SIDE_BY_SIDE'); - assert.isFalse(saveStub.called); - - // Setting the mode to itself does not save prefs. - element.setMode('SIDE_BY_SIDE'); - assert.isFalse(saveStub.called); - - // Setting the mode to something else does not save prefs if saveOnChange - // is false. - element.saveOnChange = false; - element.setMode('UNIFIED_DIFF'); - assert.isFalse(saveStub.called); - - // Setting the mode to something else does not save prefs if saveOnChange - // is false. - element.saveOnChange = true; - element.setMode('SIDE_BY_SIDE'); - assert.isTrue(saveStub.calledOnce); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeSelectedClass', () => { + assert.equal( + element._computeSelectedClass('SIDE_BY_SIDE', 'SIDE_BY_SIDE'), + 'selected'); + assert.equal( + element._computeSelectedClass('SIDE_BY_SIDE', 'UNIFIED_DIFF'), ''); + }); + + test('setMode', () => { + const saveStub = sandbox.stub(element.$.restAPI, 'savePreferences'); + + // Setting the mode initially does not save prefs. + element.saveOnChange = true; + element.setMode('SIDE_BY_SIDE'); + assert.isFalse(saveStub.called); + + // Setting the mode to itself does not save prefs. + element.setMode('SIDE_BY_SIDE'); + assert.isFalse(saveStub.called); + + // Setting the mode to something else does not save prefs if saveOnChange + // is false. + element.saveOnChange = false; + element.setMode('UNIFIED_DIFF'); + assert.isFalse(saveStub.called); + + // Setting the mode to something else does not save prefs if saveOnChange + // is false. + element.saveOnChange = true; + element.setMode('SIDE_BY_SIDE'); + assert.isTrue(saveStub.calledOnce); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js index 6aad66c..fa79e49 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog.js
@@ -14,65 +14,76 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrDiffPreferencesDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff-preferences-dialog'; } +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-diff-preferences/gr-diff-preferences.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-preferences-dialog_html.js'; - static get properties() { - return { - /** @type {?} */ - diffPrefs: Object, +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrDiffPreferencesDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _diffPrefsChanged: Boolean, - }; - } + static get is() { return 'gr-diff-preferences-dialog'; } - getFocusStops() { - return { - start: this.$.diffPreferences.$.contextSelect, - end: this.$.saveButton, - }; - } + static get properties() { + return { + /** @type {?} */ + diffPrefs: Object, - resetFocus() { - this.$.diffPreferences.$.contextSelect.focus(); - } - - _computeHeaderClass(changed) { - return changed ? 'edited' : ''; - } - - _handleCancelDiff(e) { - e.stopPropagation(); - this.$.diffPrefsOverlay.close(); - } - - open() { - this.$.diffPrefsOverlay.open().then(() => { - const focusStops = this.getFocusStops(); - this.$.diffPrefsOverlay.setFocusStops(focusStops); - this.resetFocus(); - }); - } - - _handleSaveDiffPreferences() { - this.$.diffPreferences.save().then(() => { - this.fire('reload-diff-preference', null, {bubbles: false}); - - this.$.diffPrefsOverlay.close(); - }); - } + _diffPrefsChanged: Boolean, + }; } - customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog); -})(); + getFocusStops() { + return { + start: this.$.diffPreferences.$.contextSelect, + end: this.$.saveButton, + }; + } + + resetFocus() { + this.$.diffPreferences.$.contextSelect.focus(); + } + + _computeHeaderClass(changed) { + return changed ? 'edited' : ''; + } + + _handleCancelDiff(e) { + e.stopPropagation(); + this.$.diffPrefsOverlay.close(); + } + + open() { + this.$.diffPrefsOverlay.open().then(() => { + const focusStops = this.getFocusStops(); + this.$.diffPrefsOverlay.setFocusStops(focusStops); + this.resetFocus(); + }); + } + + _handleSaveDiffPreferences() { + this.$.diffPreferences.save().then(() => { + this.fire('reload-diff-preference', null, {bubbles: false}); + + this.$.diffPrefsOverlay.close(); + }); + } +} + +customElements.define(GrDiffPreferencesDialog.is, GrDiffPreferencesDialog);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js index 21f6282..cd26a0b 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-preferences-dialog/gr-diff-preferences-dialog_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2019 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> - -<dom-module id="gr-diff-preferences-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .diffHeader, .diffActions { @@ -54,28 +47,16 @@ padding: var(--spacing-s) var(--spacing-xl); } </style> - <gr-overlay id="diffPrefsOverlay" with-backdrop> - <div class$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div> - <gr-diff-preferences - id="diffPreferences" - diff-prefs="{{diffPrefs}}" - has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences> + <gr-overlay id="diffPrefsOverlay" with-backdrop=""> + <div class\$="diffHeader [[_computeHeaderClass(_diffPrefsChanged)]]">Diff Preferences</div> + <gr-diff-preferences id="diffPreferences" diff-prefs="{{diffPrefs}}" has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences> <div class="diffActions"> - <gr-button - id="cancelButton" - link - on-click="_handleCancelDiff"> + <gr-button id="cancelButton" link="" on-click="_handleCancelDiff"> Cancel </gr-button> - <gr-button - id="saveButton" - link primary - on-click="_handleSaveDiffPreferences" - disabled$="[[!_diffPrefsChanged]]"> + <gr-button id="saveButton" link="" primary="" on-click="_handleSaveDiffPreferences" disabled\$="[[!_diffPrefsChanged]]"> Save </gr-button> </div> </gr-overlay> - </template> - <script src="gr-diff-preferences-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html deleted file mode 100644 index 7a0bce1..0000000 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.html +++ /dev/null
@@ -1,25 +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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<script src="../gr-diff/gr-diff-line.js"></script> -<script src="../gr-diff/gr-diff-group.js"></script> -<script src="../../../scripts/util.js"></script> - -<dom-module id="gr-diff-processor"> - <script src="gr-diff-processor.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js index dcda64d..adcb375 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor.js
@@ -14,653 +14,658 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const WHOLE_FILE = -1; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff/gr-diff-group.js'; +import '../../../scripts/util.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; - const DiffSide = { - LEFT: 'left', - RIGHT: 'right', - }; +const WHOLE_FILE = -1; - const DiffHighlights = { - ADDED: 'edit_b', - REMOVED: 'edit_a', - }; +const DiffSide = { + LEFT: 'left', + RIGHT: 'right', +}; + +const DiffHighlights = { + ADDED: 'edit_b', + REMOVED: 'edit_a', +}; + +/** + * The maximum size for an addition or removal chunk before it is broken down + * into a series of chunks that are this size at most. + * + * Note: The value of 120 is chosen so that it is larger than the default + * _asyncThreshold of 64, but feel free to tune this constant to your + * performance needs. + */ +const MAX_GROUP_SIZE = 120; + +/** + * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering. + * + * Glossary: + * - "chunk": A single `DiffContent` as returned by the API. + * - "group": A single `GrDiffGroup` as used for rendering. + * - "common" chunk/group: A chunk/group that should be considered unchanged + * for diffing purposes. This can mean its either actually unchanged, or it + * has only whitespace changes. + * - "key location": A line number and side of the diff that should not be + * collapsed e.g. because a comment is attached to it, or because it was + * provided in the URL and thus should be visible + * - "uncollapsible" chunk/group: A chunk/group that is either not "common", + * or cannot be collapsed because it contains a key location + * + * Here a a number of tasks this processor performs: + * - splitting large chunks to allow more granular async rendering + * - adding a group for the "File" pseudo line that file-level comments can + * be attached to + * - replacing common parts of the diff that are outside the user's + * context setting and do not have comments with a group representing the + * "expand context" widget. This may require splitting a chunk/group so + * that the part that is within the context or has comments is shown, while + * the rest is not. + * + * @extends Polymer.Element + */ +class GrDiffProcessor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get is() { return 'gr-diff-processor'; } + + static get properties() { + return { + + /** + * The amount of context around collapsed groups. + */ + context: Number, + + /** + * The array of groups output by the processor. + */ + groups: { + type: Array, + notify: true, + }, + + /** + * Locations that should not be collapsed, including the locations of + * comments. + */ + keyLocations: { + type: Object, + value() { return {left: {}, right: {}}; }, + }, + + /** + * The maximum number of lines to process synchronously. + */ + _asyncThreshold: { + type: Number, + value: 64, + }, + + /** @type {?number} */ + _nextStepHandle: Number, + /** + * The promise last returned from `process()` while the asynchronous + * processing is running - `null` otherwise. Provides a `cancel()` + * method that rejects it with `{isCancelled: true}`. + * + * @type {?Object} + */ + _processPromise: { + type: Object, + value: null, + }, + _isScrolling: Boolean, + }; + } + + /** @override */ + attached() { + super.attached(); + this.listen(window, 'scroll', '_handleWindowScroll'); + } + + /** @override */ + detached() { + super.detached(); + this.cancel(); + this.unlisten(window, 'scroll', '_handleWindowScroll'); + } + + _handleWindowScroll() { + this._isScrolling = true; + this.debounce('resetIsScrolling', () => { + this._isScrolling = false; + }, 50); + } /** - * The maximum size for an addition or removal chunk before it is broken down - * into a series of chunks that are this size at most. + * Asynchronously process the diff chunks into groups. As it processes, it + * will splice groups into the `groups` property of the component. * - * Note: The value of 120 is chosen so that it is larger than the default - * _asyncThreshold of 64, but feel free to tune this constant to your - * performance needs. + * @param {!Array<!Gerrit.DiffChunk>} chunks + * @param {boolean} isBinary + * + * @return {!Promise<!Array<!Object>>} A promise that resolves with an + * array of GrDiffGroups when the diff is completely processed. */ - const MAX_GROUP_SIZE = 120; + process(chunks, isBinary) { + // Cancel any still running process() calls, because they append to the + // same groups field. + this.cancel(); - /** - * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering. - * - * Glossary: - * - "chunk": A single `DiffContent` as returned by the API. - * - "group": A single `GrDiffGroup` as used for rendering. - * - "common" chunk/group: A chunk/group that should be considered unchanged - * for diffing purposes. This can mean its either actually unchanged, or it - * has only whitespace changes. - * - "key location": A line number and side of the diff that should not be - * collapsed e.g. because a comment is attached to it, or because it was - * provided in the URL and thus should be visible - * - "uncollapsible" chunk/group: A chunk/group that is either not "common", - * or cannot be collapsed because it contains a key location - * - * Here a a number of tasks this processor performs: - * - splitting large chunks to allow more granular async rendering - * - adding a group for the "File" pseudo line that file-level comments can - * be attached to - * - replacing common parts of the diff that are outside the user's - * context setting and do not have comments with a group representing the - * "expand context" widget. This may require splitting a chunk/group so - * that the part that is within the context or has comments is shown, while - * the rest is not. - * - * @extends Polymer.Element - */ - class GrDiffProcessor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-diff-processor'; } + this.groups = []; + this.push('groups', this._makeFileComments()); - static get properties() { - return { + // If it's a binary diff, we won't be rendering hunks of text differences + // so finish processing. + if (isBinary) { return Promise.resolve(); } - /** - * The amount of context around collapsed groups. - */ - context: Number, + this._processPromise = util.makeCancelable( + new Promise(resolve => { + const state = { + lineNums: {left: 0, right: 0}, + chunkIndex: 0, + }; - /** - * The array of groups output by the processor. - */ - groups: { - type: Array, - notify: true, - }, + chunks = this._splitLargeChunks(chunks); + chunks = this._splitCommonChunksWithKeyLocations(chunks); - /** - * Locations that should not be collapsed, including the locations of - * comments. - */ - keyLocations: { - type: Object, - value() { return {left: {}, right: {}}; }, - }, - - /** - * The maximum number of lines to process synchronously. - */ - _asyncThreshold: { - type: Number, - value: 64, - }, - - /** @type {?number} */ - _nextStepHandle: Number, - /** - * The promise last returned from `process()` while the asynchronous - * processing is running - `null` otherwise. Provides a `cancel()` - * method that rejects it with `{isCancelled: true}`. - * - * @type {?Object} - */ - _processPromise: { - type: Object, - value: null, - }, - _isScrolling: Boolean, - }; - } - - /** @override */ - attached() { - super.attached(); - this.listen(window, 'scroll', '_handleWindowScroll'); - } - - /** @override */ - detached() { - super.detached(); - this.cancel(); - this.unlisten(window, 'scroll', '_handleWindowScroll'); - } - - _handleWindowScroll() { - this._isScrolling = true; - this.debounce('resetIsScrolling', () => { - this._isScrolling = false; - }, 50); - } - - /** - * Asynchronously process the diff chunks into groups. As it processes, it - * will splice groups into the `groups` property of the component. - * - * @param {!Array<!Gerrit.DiffChunk>} chunks - * @param {boolean} isBinary - * - * @return {!Promise<!Array<!Object>>} A promise that resolves with an - * array of GrDiffGroups when the diff is completely processed. - */ - process(chunks, isBinary) { - // Cancel any still running process() calls, because they append to the - // same groups field. - this.cancel(); - - this.groups = []; - this.push('groups', this._makeFileComments()); - - // If it's a binary diff, we won't be rendering hunks of text differences - // so finish processing. - if (isBinary) { return Promise.resolve(); } - - this._processPromise = util.makeCancelable( - new Promise(resolve => { - const state = { - lineNums: {left: 0, right: 0}, - chunkIndex: 0, - }; - - chunks = this._splitLargeChunks(chunks); - chunks = this._splitCommonChunksWithKeyLocations(chunks); - - let currentBatch = 0; - const nextStep = () => { - if (this._isScrolling) { - this._nextStepHandle = this.async(nextStep, 100); - return; - } - // If we are done, resolve the promise. - if (state.chunkIndex >= chunks.length) { - resolve(); - this._nextStepHandle = null; - return; - } - - // Process the next chunk and incorporate the result. - const stateUpdate = this._processNext(state, chunks); - for (const group of stateUpdate.groups) { - this.push('groups', group); - currentBatch += group.lines.length; - } - state.lineNums.left += stateUpdate.lineDelta.left; - state.lineNums.right += stateUpdate.lineDelta.right; - - // Increment the index and recurse. - state.chunkIndex = stateUpdate.newChunkIndex; - if (currentBatch >= this._asyncThreshold) { - currentBatch = 0; - this._nextStepHandle = this.async(nextStep, 1); - } else { - nextStep.call(this); - } - }; - - nextStep.call(this); - })); - return this._processPromise - .finally(() => { this._processPromise = null; }); - } - - /** - * Cancel any jobs that are running. - */ - cancel() { - if (this._nextStepHandle != null) { - this.cancelAsync(this._nextStepHandle); - this._nextStepHandle = null; - } - if (this._processPromise) { - this._processPromise.cancel(); - } - } - - /** - * Process the next uncollapsible chunk, or the next collapsible chunks. - * - * @param {!Object} state - * @param {!Array<!Object>} chunks - * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}} - */ - _processNext(state, chunks) { - const firstUncollapsibleChunkIndex = - this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex); - if (firstUncollapsibleChunkIndex === state.chunkIndex) { - const chunk = chunks[state.chunkIndex]; - return { - lineDelta: { - left: this._linesLeft(chunk).length, - right: this._linesRight(chunk).length, - }, - groups: [this._chunkToGroup( - chunk, state.lineNums.left + 1, state.lineNums.right + 1)], - newChunkIndex: state.chunkIndex + 1, - }; - } - - return this._processCollapsibleChunks( - state, chunks, firstUncollapsibleChunkIndex); - } - - _linesLeft(chunk) { - return chunk.ab || chunk.a || []; - } - - _linesRight(chunk) { - return chunk.ab || chunk.b || []; - } - - _firstUncollapsibleChunkIndex(chunks, offset) { - let chunkIndex = offset; - while (chunkIndex < chunks.length && - this._isCollapsibleChunk(chunks[chunkIndex])) { - chunkIndex++; - } - return chunkIndex; - } - - _isCollapsibleChunk(chunk) { - return (chunk.ab || chunk.common) && !chunk.keyLocation; - } - - /** - * Process a stretch of collapsible chunks. - * - * Outputs up to three groups: - * 1) Visible context before the hidden common code, unless it's the - * very beginning of the file. - * 2) Context hidden behind a context bar, unless empty. - * 3) Visible context after the hidden common code, unless it's the very - * end of the file. - * - * @param {!Object} state - * @param {!Array<Object>} chunks - * @param {number} firstUncollapsibleChunkIndex - * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}} - */ - _processCollapsibleChunks( - state, chunks, firstUncollapsibleChunkIndex) { - const collapsibleChunks = chunks.slice( - state.chunkIndex, firstUncollapsibleChunkIndex); - const lineCount = collapsibleChunks.reduce( - (sum, chunk) => sum + this._commonChunkLength(chunk), 0); - - let groups = this._chunksToGroups( - collapsibleChunks, - state.lineNums.left + 1, - state.lineNums.right + 1); - - if (this.context !== WHOLE_FILE) { - const hiddenStart = state.chunkIndex === 0 ? 0 : this.context; - const hiddenEnd = lineCount - ( - firstUncollapsibleChunkIndex === chunks.length ? - 0 : this.context); - groups = GrDiffGroup.hideInContextControl( - groups, hiddenStart, hiddenEnd); - } - - return { - lineDelta: { - left: lineCount, - right: lineCount, - }, - groups, - newChunkIndex: firstUncollapsibleChunkIndex, - }; - } - - _commonChunkLength(chunk) { - console.assert(chunk.ab || chunk.common); - console.assert( - !chunk.a || (chunk.b && chunk.a.length === chunk.b.length), - `common chunk needs same number of a and b lines: `, chunk); - return this._linesLeft(chunk).length; - } - - /** - * @param {!Array<!Object>} chunks - * @param {number} offsetLeft - * @param {number} offsetRight - * @return {!Array<!Object>} (GrDiffGroup) - */ - _chunksToGroups(chunks, offsetLeft, offsetRight) { - return chunks.map(chunk => { - const group = this._chunkToGroup(chunk, offsetLeft, offsetRight); - const chunkLength = this._commonChunkLength(chunk); - offsetLeft += chunkLength; - offsetRight += chunkLength; - return group; - }); - } - - /** - * @param {!Object} chunk - * @param {number} offsetLeft - * @param {number} offsetRight - * @return {!Object} (GrDiffGroup) - */ - _chunkToGroup(chunk, offsetLeft, offsetRight) { - const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA; - const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight); - const group = new GrDiffGroup(type, lines); - group.keyLocation = chunk.keyLocation; - group.dueToRebase = chunk.due_to_rebase; - group.ignoredWhitespaceOnly = chunk.common; - return group; - } - - _linesFromChunk(chunk, offsetLeft, offsetRight) { - if (chunk.ab) { - return chunk.ab.map((row, i) => this._lineFromRow( - GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i)); - } - let lines = []; - if (chunk.a) { - // Avoiding a.push(...b) because that causes callstack overflows for - // large b, which can occur when large files are added removed. - lines = lines.concat(this._linesFromRows( - GrDiffLine.Type.REMOVE, chunk.a, offsetLeft, - chunk[DiffHighlights.REMOVED])); - } - if (chunk.b) { - // Avoiding a.push(...b) because that causes callstack overflows for - // large b, which can occur when large files are added removed. - lines = lines.concat(this._linesFromRows( - GrDiffLine.Type.ADD, chunk.b, offsetRight, - chunk[DiffHighlights.ADDED])); - } - return lines; - } - - /** - * @param {string} lineType (GrDiffLine.Type) - * @param {!Array<string>} rows - * @param {number} offset - * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos - * @return {!Array<!Object>} (GrDiffLine) - */ - _linesFromRows(lineType, rows, offset, opt_intralineInfos) { - const grDiffHighlights = opt_intralineInfos ? - this._convertIntralineInfos(rows, opt_intralineInfos) : undefined; - return rows.map((row, i) => this._lineFromRow( - lineType, offset, offset, row, i, grDiffHighlights)); - } - - /** - * @param {string} type (GrDiffLine.Type) - * @param {number} offsetLeft - * @param {number} offsetRight - * @param {string} row - * @param {number} i - * @param {!Array<!Object>=} opt_highlights - * @return {!Object} (GrDiffLine) - */ - _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) { - const line = new GrDiffLine(type); - line.text = row; - if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i; - if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i; - if (opt_highlights) { - line.hasIntralineInfo = true; - line.highlights = opt_highlights.filter(hl => hl.contentIndex === i); - } else { - line.hasIntralineInfo = false; - } - return line; - } - - _makeFileComments() { - const line = new GrDiffLine(GrDiffLine.Type.BOTH); - line.beforeNumber = GrDiffLine.FILE; - line.afterNumber = GrDiffLine.FILE; - return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]); - } - - /** - * Split chunks into smaller chunks of the same kind. - * - * This is done to prevent doing too much work on the main thread in one - * uninterrupted rendering step, which would make the browser unresponsive. - * - * Note that in the case of unmodified chunks, we only split chunks if the - * context is set to file (because otherwise they are split up further down - * the processing into the visible and hidden context), and only split it - * into 2 chunks, one max sized one and the rest (for reasons that are - * unclear to me). - * - * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server - * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks. - */ - _splitLargeChunks(chunks) { - const newChunks = []; - - for (const chunk of chunks) { - if (!chunk.ab) { - for (const subChunk of this._breakdownChunk(chunk)) { - newChunks.push(subChunk); - } - continue; - } - - // If the context is set to "whole file", then break down the shared - // chunks so they can be rendered incrementally. Note: this is not - // enabled for any other context preference because manipulating the - // chunks in this way violates assumptions by the context grouper logic. - if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) { - // Split large shared chunks in two, where the first is the maximum - // group size. - newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)}); - newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)}); - } else { - newChunks.push(chunk); - } - } - return newChunks; - } - - /** - * In order to show key locations, such as comments, out of the bounds of - * the selected context, treat them as separate chunks within the model so - * that the content (and context surrounding it) renders correctly. - * - * @param {!Array<!Object>} chunks DiffContents as returned from server. - * @return {!Array<!Object>} Finer grained DiffContents. - */ - _splitCommonChunksWithKeyLocations(chunks) { - const result = []; - let leftLineNum = 1; - let rightLineNum = 1; - - for (const chunk of chunks) { - // If it isn't a common chunk, append it as-is and update line numbers. - if (!chunk.ab && !chunk.common) { - if (chunk.a) { - leftLineNum += chunk.a.length; - } - if (chunk.b) { - rightLineNum += chunk.b.length; - } - result.push(chunk); - continue; - } - - if (chunk.common && chunk.a.length != chunk.b.length) { - throw new Error( - 'DiffContent with common=true must always have equal length'); - } - const numLines = this._commonChunkLength(chunk); - const chunkEnds = this._findChunkEndsAtKeyLocations( - numLines, leftLineNum, rightLineNum); - leftLineNum += numLines; - rightLineNum += numLines; - - if (chunk.ab) { - result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds) - .map(({lines, keyLocation}) => - Object.assign({}, chunk, {ab: lines, keyLocation}))); - } else if (chunk.common) { - const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds); - const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds); - result.push(...aChunks.map(({lines, keyLocation}, i) => - Object.assign( - {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation}))); - } - } - - return result; - } - - /** - * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the - * new chunk ends, including whether it's a key location. - */ - _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) { - const result = []; - let lastChunkEnd = 0; - for (let i=0; i<numLines; i++) { - // If this line should not be collapsed. - if (this.keyLocations[DiffSide.LEFT][leftOffset + i] || - this.keyLocations[DiffSide.RIGHT][rightOffset + i]) { - // If any lines have been accumulated into the chunk leading up to - // this non-collapse line, then add them as a chunk and start a new - // one. - if (i > lastChunkEnd) { - result.push({offset: i, keyLocation: false}); - lastChunkEnd = i; - } - - // Add the non-collapse line as its own chunk. - result.push({offset: i + 1, keyLocation: true}); - } - } - - if (numLines > lastChunkEnd) { - result.push({offset: numLines, keyLocation: false}); - } - - return result; - } - - _splitAtChunkEnds(lines, chunkEnds) { - const result = []; - let lastChunkEndOffset = 0; - for (const {offset, keyLocation} of chunkEnds) { - result.push( - {lines: lines.slice(lastChunkEndOffset, offset), keyLocation}); - lastChunkEndOffset = offset; - } - return result; - } - - /** - * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used - * for rendering. - * - * @param {!Array<string>} rows - * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos - * @return {!Array<!Object>} (GrDiffLine.Highlight) - */ - _convertIntralineInfos(rows, intralineInfos) { - let rowIndex = 0; - let idx = 0; - const normalized = []; - for (const [skipLength, markLength] of intralineInfos) { - let line = rows[rowIndex] + '\n'; - let j = 0; - while (j < skipLength) { - if (idx === line.length) { - idx = 0; - line = rows[++rowIndex] + '\n'; - continue; - } - idx++; - j++; - } - let lineHighlight = { - contentIndex: rowIndex, - startIndex: idx, - }; - - j = 0; - while (line && j < markLength) { - if (idx === line.length) { - idx = 0; - line = rows[++rowIndex] + '\n'; - normalized.push(lineHighlight); - lineHighlight = { - contentIndex: rowIndex, - startIndex: idx, - }; - continue; - } - idx++; - j++; - } - lineHighlight.endIndex = idx; - normalized.push(lineHighlight); - } - return normalized; - } - - /** - * If a group is an addition or a removal, break it down into smaller groups - * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk - * or a delta it is returned as the single element of the result array. - * - * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response. - * @return {!Array<!Array<!Object>>} - */ - _breakdownChunk(chunk) { - let key = null; - if (chunk.a && !chunk.b) { - key = 'a'; - } else if (chunk.b && !chunk.a) { - key = 'b'; - } else if (chunk.ab) { - key = 'ab'; - } - - if (!key) { return [chunk]; } - - return this._breakdown(chunk[key], MAX_GROUP_SIZE) - .map(subChunkLines => { - const subChunk = {}; - subChunk[key] = subChunkLines; - if (chunk.due_to_rebase) { - subChunk.due_to_rebase = true; + let currentBatch = 0; + const nextStep = () => { + if (this._isScrolling) { + this._nextStepHandle = this.async(nextStep, 100); + return; } - return subChunk; - }); + // If we are done, resolve the promise. + if (state.chunkIndex >= chunks.length) { + resolve(); + this._nextStepHandle = null; + return; + } + + // Process the next chunk and incorporate the result. + const stateUpdate = this._processNext(state, chunks); + for (const group of stateUpdate.groups) { + this.push('groups', group); + currentBatch += group.lines.length; + } + state.lineNums.left += stateUpdate.lineDelta.left; + state.lineNums.right += stateUpdate.lineDelta.right; + + // Increment the index and recurse. + state.chunkIndex = stateUpdate.newChunkIndex; + if (currentBatch >= this._asyncThreshold) { + currentBatch = 0; + this._nextStepHandle = this.async(nextStep, 1); + } else { + nextStep.call(this); + } + }; + + nextStep.call(this); + })); + return this._processPromise + .finally(() => { this._processPromise = null; }); + } + + /** + * Cancel any jobs that are running. + */ + cancel() { + if (this._nextStepHandle != null) { + this.cancelAsync(this._nextStepHandle); + this._nextStepHandle = null; } - - /** - * Given an array and a size, return an array of arrays where no inner array - * is larger than that size, preserving the original order. - * - * @param {!Array<T>} array - * @param {number} size - * @return {!Array<!Array<T>>} - * @template T - */ - _breakdown(array, size) { - if (!array.length) { return []; } - if (array.length < size) { return [array]; } - - const head = array.slice(0, array.length - size); - const tail = array.slice(array.length - size); - - return this._breakdown(head, size).concat([tail]); + if (this._processPromise) { + this._processPromise.cancel(); } } - customElements.define(GrDiffProcessor.is, GrDiffProcessor); -})(); + /** + * Process the next uncollapsible chunk, or the next collapsible chunks. + * + * @param {!Object} state + * @param {!Array<!Object>} chunks + * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}} + */ + _processNext(state, chunks) { + const firstUncollapsibleChunkIndex = + this._firstUncollapsibleChunkIndex(chunks, state.chunkIndex); + if (firstUncollapsibleChunkIndex === state.chunkIndex) { + const chunk = chunks[state.chunkIndex]; + return { + lineDelta: { + left: this._linesLeft(chunk).length, + right: this._linesRight(chunk).length, + }, + groups: [this._chunkToGroup( + chunk, state.lineNums.left + 1, state.lineNums.right + 1)], + newChunkIndex: state.chunkIndex + 1, + }; + } + + return this._processCollapsibleChunks( + state, chunks, firstUncollapsibleChunkIndex); + } + + _linesLeft(chunk) { + return chunk.ab || chunk.a || []; + } + + _linesRight(chunk) { + return chunk.ab || chunk.b || []; + } + + _firstUncollapsibleChunkIndex(chunks, offset) { + let chunkIndex = offset; + while (chunkIndex < chunks.length && + this._isCollapsibleChunk(chunks[chunkIndex])) { + chunkIndex++; + } + return chunkIndex; + } + + _isCollapsibleChunk(chunk) { + return (chunk.ab || chunk.common) && !chunk.keyLocation; + } + + /** + * Process a stretch of collapsible chunks. + * + * Outputs up to three groups: + * 1) Visible context before the hidden common code, unless it's the + * very beginning of the file. + * 2) Context hidden behind a context bar, unless empty. + * 3) Visible context after the hidden common code, unless it's the very + * end of the file. + * + * @param {!Object} state + * @param {!Array<Object>} chunks + * @param {number} firstUncollapsibleChunkIndex + * @return {{lineDelta: {left: number, right: number}, groups: !Array<!Object>, newChunkIndex: number}} + */ + _processCollapsibleChunks( + state, chunks, firstUncollapsibleChunkIndex) { + const collapsibleChunks = chunks.slice( + state.chunkIndex, firstUncollapsibleChunkIndex); + const lineCount = collapsibleChunks.reduce( + (sum, chunk) => sum + this._commonChunkLength(chunk), 0); + + let groups = this._chunksToGroups( + collapsibleChunks, + state.lineNums.left + 1, + state.lineNums.right + 1); + + if (this.context !== WHOLE_FILE) { + const hiddenStart = state.chunkIndex === 0 ? 0 : this.context; + const hiddenEnd = lineCount - ( + firstUncollapsibleChunkIndex === chunks.length ? + 0 : this.context); + groups = GrDiffGroup.hideInContextControl( + groups, hiddenStart, hiddenEnd); + } + + return { + lineDelta: { + left: lineCount, + right: lineCount, + }, + groups, + newChunkIndex: firstUncollapsibleChunkIndex, + }; + } + + _commonChunkLength(chunk) { + console.assert(chunk.ab || chunk.common); + console.assert( + !chunk.a || (chunk.b && chunk.a.length === chunk.b.length), + `common chunk needs same number of a and b lines: `, chunk); + return this._linesLeft(chunk).length; + } + + /** + * @param {!Array<!Object>} chunks + * @param {number} offsetLeft + * @param {number} offsetRight + * @return {!Array<!Object>} (GrDiffGroup) + */ + _chunksToGroups(chunks, offsetLeft, offsetRight) { + return chunks.map(chunk => { + const group = this._chunkToGroup(chunk, offsetLeft, offsetRight); + const chunkLength = this._commonChunkLength(chunk); + offsetLeft += chunkLength; + offsetRight += chunkLength; + return group; + }); + } + + /** + * @param {!Object} chunk + * @param {number} offsetLeft + * @param {number} offsetRight + * @return {!Object} (GrDiffGroup) + */ + _chunkToGroup(chunk, offsetLeft, offsetRight) { + const type = chunk.ab ? GrDiffGroup.Type.BOTH : GrDiffGroup.Type.DELTA; + const lines = this._linesFromChunk(chunk, offsetLeft, offsetRight); + const group = new GrDiffGroup(type, lines); + group.keyLocation = chunk.keyLocation; + group.dueToRebase = chunk.due_to_rebase; + group.ignoredWhitespaceOnly = chunk.common; + return group; + } + + _linesFromChunk(chunk, offsetLeft, offsetRight) { + if (chunk.ab) { + return chunk.ab.map((row, i) => this._lineFromRow( + GrDiffLine.Type.BOTH, offsetLeft, offsetRight, row, i)); + } + let lines = []; + if (chunk.a) { + // Avoiding a.push(...b) because that causes callstack overflows for + // large b, which can occur when large files are added removed. + lines = lines.concat(this._linesFromRows( + GrDiffLine.Type.REMOVE, chunk.a, offsetLeft, + chunk[DiffHighlights.REMOVED])); + } + if (chunk.b) { + // Avoiding a.push(...b) because that causes callstack overflows for + // large b, which can occur when large files are added removed. + lines = lines.concat(this._linesFromRows( + GrDiffLine.Type.ADD, chunk.b, offsetRight, + chunk[DiffHighlights.ADDED])); + } + return lines; + } + + /** + * @param {string} lineType (GrDiffLine.Type) + * @param {!Array<string>} rows + * @param {number} offset + * @param {?Array<!Gerrit.IntralineInfo>=} opt_intralineInfos + * @return {!Array<!Object>} (GrDiffLine) + */ + _linesFromRows(lineType, rows, offset, opt_intralineInfos) { + const grDiffHighlights = opt_intralineInfos ? + this._convertIntralineInfos(rows, opt_intralineInfos) : undefined; + return rows.map((row, i) => this._lineFromRow( + lineType, offset, offset, row, i, grDiffHighlights)); + } + + /** + * @param {string} type (GrDiffLine.Type) + * @param {number} offsetLeft + * @param {number} offsetRight + * @param {string} row + * @param {number} i + * @param {!Array<!Object>=} opt_highlights + * @return {!Object} (GrDiffLine) + */ + _lineFromRow(type, offsetLeft, offsetRight, row, i, opt_highlights) { + const line = new GrDiffLine(type); + line.text = row; + if (type !== GrDiffLine.Type.ADD) line.beforeNumber = offsetLeft + i; + if (type !== GrDiffLine.Type.REMOVE) line.afterNumber = offsetRight + i; + if (opt_highlights) { + line.hasIntralineInfo = true; + line.highlights = opt_highlights.filter(hl => hl.contentIndex === i); + } else { + line.hasIntralineInfo = false; + } + return line; + } + + _makeFileComments() { + const line = new GrDiffLine(GrDiffLine.Type.BOTH); + line.beforeNumber = GrDiffLine.FILE; + line.afterNumber = GrDiffLine.FILE; + return new GrDiffGroup(GrDiffGroup.Type.BOTH, [line]); + } + + /** + * Split chunks into smaller chunks of the same kind. + * + * This is done to prevent doing too much work on the main thread in one + * uninterrupted rendering step, which would make the browser unresponsive. + * + * Note that in the case of unmodified chunks, we only split chunks if the + * context is set to file (because otherwise they are split up further down + * the processing into the visible and hidden context), and only split it + * into 2 chunks, one max sized one and the rest (for reasons that are + * unclear to me). + * + * @param {!Array<!Gerrit.DiffChunk>} chunks Chunks as returned from the server + * @return {!Array<!Gerrit.DiffChunk>} Finer grained chunks. + */ + _splitLargeChunks(chunks) { + const newChunks = []; + + for (const chunk of chunks) { + if (!chunk.ab) { + for (const subChunk of this._breakdownChunk(chunk)) { + newChunks.push(subChunk); + } + continue; + } + + // If the context is set to "whole file", then break down the shared + // chunks so they can be rendered incrementally. Note: this is not + // enabled for any other context preference because manipulating the + // chunks in this way violates assumptions by the context grouper logic. + if (this.context === -1 && chunk.ab.length > MAX_GROUP_SIZE * 2) { + // Split large shared chunks in two, where the first is the maximum + // group size. + newChunks.push({ab: chunk.ab.slice(0, MAX_GROUP_SIZE)}); + newChunks.push({ab: chunk.ab.slice(MAX_GROUP_SIZE)}); + } else { + newChunks.push(chunk); + } + } + return newChunks; + } + + /** + * In order to show key locations, such as comments, out of the bounds of + * the selected context, treat them as separate chunks within the model so + * that the content (and context surrounding it) renders correctly. + * + * @param {!Array<!Object>} chunks DiffContents as returned from server. + * @return {!Array<!Object>} Finer grained DiffContents. + */ + _splitCommonChunksWithKeyLocations(chunks) { + const result = []; + let leftLineNum = 1; + let rightLineNum = 1; + + for (const chunk of chunks) { + // If it isn't a common chunk, append it as-is and update line numbers. + if (!chunk.ab && !chunk.common) { + if (chunk.a) { + leftLineNum += chunk.a.length; + } + if (chunk.b) { + rightLineNum += chunk.b.length; + } + result.push(chunk); + continue; + } + + if (chunk.common && chunk.a.length != chunk.b.length) { + throw new Error( + 'DiffContent with common=true must always have equal length'); + } + const numLines = this._commonChunkLength(chunk); + const chunkEnds = this._findChunkEndsAtKeyLocations( + numLines, leftLineNum, rightLineNum); + leftLineNum += numLines; + rightLineNum += numLines; + + if (chunk.ab) { + result.push(...this._splitAtChunkEnds(chunk.ab, chunkEnds) + .map(({lines, keyLocation}) => + Object.assign({}, chunk, {ab: lines, keyLocation}))); + } else if (chunk.common) { + const aChunks = this._splitAtChunkEnds(chunk.a, chunkEnds); + const bChunks = this._splitAtChunkEnds(chunk.b, chunkEnds); + result.push(...aChunks.map(({lines, keyLocation}, i) => + Object.assign( + {}, chunk, {a: lines, b: bChunks[i].lines, keyLocation}))); + } + } + + return result; + } + + /** + * @return {!Array<{offset: number, keyLocation: boolean}>} Offsets of the + * new chunk ends, including whether it's a key location. + */ + _findChunkEndsAtKeyLocations(numLines, leftOffset, rightOffset) { + const result = []; + let lastChunkEnd = 0; + for (let i=0; i<numLines; i++) { + // If this line should not be collapsed. + if (this.keyLocations[DiffSide.LEFT][leftOffset + i] || + this.keyLocations[DiffSide.RIGHT][rightOffset + i]) { + // If any lines have been accumulated into the chunk leading up to + // this non-collapse line, then add them as a chunk and start a new + // one. + if (i > lastChunkEnd) { + result.push({offset: i, keyLocation: false}); + lastChunkEnd = i; + } + + // Add the non-collapse line as its own chunk. + result.push({offset: i + 1, keyLocation: true}); + } + } + + if (numLines > lastChunkEnd) { + result.push({offset: numLines, keyLocation: false}); + } + + return result; + } + + _splitAtChunkEnds(lines, chunkEnds) { + const result = []; + let lastChunkEndOffset = 0; + for (const {offset, keyLocation} of chunkEnds) { + result.push( + {lines: lines.slice(lastChunkEndOffset, offset), keyLocation}); + lastChunkEndOffset = offset; + } + return result; + } + + /** + * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used + * for rendering. + * + * @param {!Array<string>} rows + * @param {!Array<!Gerrit.IntralineInfo>} intralineInfos + * @return {!Array<!Object>} (GrDiffLine.Highlight) + */ + _convertIntralineInfos(rows, intralineInfos) { + let rowIndex = 0; + let idx = 0; + const normalized = []; + for (const [skipLength, markLength] of intralineInfos) { + let line = rows[rowIndex] + '\n'; + let j = 0; + while (j < skipLength) { + if (idx === line.length) { + idx = 0; + line = rows[++rowIndex] + '\n'; + continue; + } + idx++; + j++; + } + let lineHighlight = { + contentIndex: rowIndex, + startIndex: idx, + }; + + j = 0; + while (line && j < markLength) { + if (idx === line.length) { + idx = 0; + line = rows[++rowIndex] + '\n'; + normalized.push(lineHighlight); + lineHighlight = { + contentIndex: rowIndex, + startIndex: idx, + }; + continue; + } + idx++; + j++; + } + lineHighlight.endIndex = idx; + normalized.push(lineHighlight); + } + return normalized; + } + + /** + * If a group is an addition or a removal, break it down into smaller groups + * of that type using the MAX_GROUP_SIZE. If the group is a shared chunk + * or a delta it is returned as the single element of the result array. + * + * @param {!Gerrit.DiffChunk} chunk A raw chunk from a diff response. + * @return {!Array<!Array<!Object>>} + */ + _breakdownChunk(chunk) { + let key = null; + if (chunk.a && !chunk.b) { + key = 'a'; + } else if (chunk.b && !chunk.a) { + key = 'b'; + } else if (chunk.ab) { + key = 'ab'; + } + + if (!key) { return [chunk]; } + + return this._breakdown(chunk[key], MAX_GROUP_SIZE) + .map(subChunkLines => { + const subChunk = {}; + subChunk[key] = subChunkLines; + if (chunk.due_to_rebase) { + subChunk.due_to_rebase = true; + } + return subChunk; + }); + } + + /** + * Given an array and a size, return an array of arrays where no inner array + * is larger than that size, preserving the original order. + * + * @param {!Array<T>} array + * @param {number} size + * @return {!Array<!Array<T>>} + * @template T + */ + _breakdown(array, size) { + if (!array.length) { return []; } + if (array.length < size) { return [array]; } + + const head = array.slice(0, array.length - size); + const tail = array.slice(array.length - size); + + return this._breakdown(head, size).concat([tail]); + } +} + +customElements.define(GrDiffProcessor.is, GrDiffProcessor);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html index 9b3f3b2..e62abe5 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-processor/gr-diff-processor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-processor test</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-diff-processor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-diff-processor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-processor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,902 +40,904 @@ </template> </test-fixture> -<script> - suite('gr-diff-processor tests', async () => { - await readyToTest(); - const WHOLE_FILE = -1; - const loremIpsum = - 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' + - 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' + - 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' + - 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' + - 'fugit assum per.'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-processor.js'; +suite('gr-diff-processor tests', () => { + const WHOLE_FILE = -1; + const loremIpsum = + 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' + + 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' + + 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' + + 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' + + 'fugit assum per.'; - let element; - let sandbox; + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('not logged in', () => { setup(() => { - sandbox = sinon.sandbox.create(); + element = fixture('basic'); + + element.context = 4; }); - teardown(() => { - sandbox.restore(); - }); - - suite('not logged in', () => { - setup(() => { - element = fixture('basic'); - - element.context = 4; - }); - - test('process loaded content', () => { - const content = [ - { - ab: [ - '<!DOCTYPE html>', - '<meta charset="utf-8">', - ], - }, - { - a: [ - ' Welcome ', - ' to the wooorld of tomorrow!', - ], - b: [ - ' Hello, world!', - ], - }, - { - ab: [ - 'Leela: This is the only place the ship can’t hear us, so ', - 'everyone pretend to shower.', - 'Fry: Same as every day. Got it.', - ], - }, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - assert.equal(groups.length, 4); - - let group = groups[0]; - assert.equal(group.type, GrDiffGroup.Type.BOTH); - assert.equal(group.lines.length, 1); - assert.equal(group.lines[0].text, ''); - assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE); - assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE); - - group = groups[1]; - assert.equal(group.type, GrDiffGroup.Type.BOTH); - assert.equal(group.lines.length, 2); - assert.equal(group.lines.length, 2); - - function beforeNumberFn(l) { return l.beforeNumber; } - function afterNumberFn(l) { return l.afterNumber; } - function textFn(l) { return l.text; } - - assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]); - assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]); - assert.deepEqual(group.lines.map(textFn), [ + test('process loaded content', () => { + const content = [ + { + ab: [ '<!DOCTYPE html>', '<meta charset="utf-8">', - ]); - - group = groups[2]; - assert.equal(group.type, GrDiffGroup.Type.DELTA); - assert.equal(group.lines.length, 3); - assert.equal(group.adds.length, 1); - assert.equal(group.removes.length, 2); - assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]); - assert.deepEqual(group.adds.map(afterNumberFn), [3]); - assert.deepEqual(group.removes.map(textFn), [ + ], + }, + { + a: [ ' Welcome ', ' to the wooorld of tomorrow!', - ]); - assert.deepEqual(group.adds.map(textFn), [ + ], + b: [ ' Hello, world!', - ]); - - group = groups[3]; - assert.equal(group.type, GrDiffGroup.Type.BOTH); - assert.equal(group.lines.length, 3); - assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]); - assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]); - assert.deepEqual(group.lines.map(textFn), [ + ], + }, + { + ab: [ 'Leela: This is the only place the ship can’t hear us, so ', 'everyone pretend to shower.', 'Fry: Same as every day. Got it.', - ]); - }); - }); + ], + }, + ]; - test('first group is for file', () => { + return element.process(content).then(() => { + const groups = element.groups; + + assert.equal(groups.length, 4); + + let group = groups[0]; + assert.equal(group.type, GrDiffGroup.Type.BOTH); + assert.equal(group.lines.length, 1); + assert.equal(group.lines[0].text, ''); + assert.equal(group.lines[0].beforeNumber, GrDiffLine.FILE); + assert.equal(group.lines[0].afterNumber, GrDiffLine.FILE); + + group = groups[1]; + assert.equal(group.type, GrDiffGroup.Type.BOTH); + assert.equal(group.lines.length, 2); + assert.equal(group.lines.length, 2); + + function beforeNumberFn(l) { return l.beforeNumber; } + function afterNumberFn(l) { return l.afterNumber; } + function textFn(l) { return l.text; } + + assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]); + assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]); + assert.deepEqual(group.lines.map(textFn), [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + ]); + + group = groups[2]; + assert.equal(group.type, GrDiffGroup.Type.DELTA); + assert.equal(group.lines.length, 3); + assert.equal(group.adds.length, 1); + assert.equal(group.removes.length, 2); + assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]); + assert.deepEqual(group.adds.map(afterNumberFn), [3]); + assert.deepEqual(group.removes.map(textFn), [ + ' Welcome ', + ' to the wooorld of tomorrow!', + ]); + assert.deepEqual(group.adds.map(textFn), [ + ' Hello, world!', + ]); + + group = groups[3]; + assert.equal(group.type, GrDiffGroup.Type.BOTH); + assert.equal(group.lines.length, 3); + assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]); + assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]); + assert.deepEqual(group.lines.map(textFn), [ + 'Leela: This is the only place the ship can’t hear us, so ', + 'everyone pretend to shower.', + 'Fry: Same as every day. Got it.', + ]); + }); + }); + + test('first group is for file', () => { + const content = [ + {b: ['foo']}, + ]; + + return element.process(content).then(() => { + const groups = element.groups; + + assert.equal(groups[0].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[0].lines.length, 1); + assert.equal(groups[0].lines[0].text, ''); + assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE); + assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE); + }); + }); + + suite('context groups', () => { + test('at the beginning, larger than context', () => { + element.context = 10; const content = [ - {b: ['foo']}, + {ab: new Array(100) + .fill('all work and no play make jack a dull boy')}, + {a: ['all work and no play make andybons a dull boy']}, ]; return element.process(content).then(() => { const groups = element.groups; - assert.equal(groups[0].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[0].lines.length, 1); - assert.equal(groups[0].lines[0].text, ''); - assert.equal(groups[0].lines[0].beforeNumber, GrDiffLine.FILE); - assert.equal(groups[0].lines[0].afterNumber, GrDiffLine.FILE); + // group[0] is the file group + + assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL); + assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup); + assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90); + for (const l of groups[1].lines[0].contextGroups[0].lines) { + assert.equal(l.text, 'all work and no play make jack a dull boy'); + } + + assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[2].lines.length, 10); + for (const l of groups[2].lines) { + assert.equal(l.text, 'all work and no play make jack a dull boy'); + } }); }); - suite('context groups', () => { - test('at the beginning, larger than context', () => { - element.context = 10; - const content = [ - {ab: new Array(100) - .fill('all work and no play make jack a dull boy')}, - {a: ['all work and no play make andybons a dull boy']}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - - assert.equal(groups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL); - assert.instanceOf(groups[1].lines[0].contextGroups[0], GrDiffGroup); - assert.equal(groups[1].lines[0].contextGroups[0].lines.length, 90); - for (const l of groups[1].lines[0].contextGroups[0].lines) { - assert.equal(l.text, 'all work and no play make jack a dull boy'); - } - - assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[2].lines.length, 10); - for (const l of groups[2].lines) { - assert.equal(l.text, 'all work and no play make jack a dull boy'); - } - }); - }); - - test('at the beginning, smaller than context', () => { - element.context = 10; - const content = [ - {ab: new Array(5) - .fill('all work and no play make jack a dull boy')}, - {a: ['all work and no play make andybons a dull boy']}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - - assert.equal(groups[1].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[1].lines.length, 5); - for (const l of groups[1].lines) { - assert.equal(l.text, 'all work and no play make jack a dull boy'); - } - }); - }); - - test('at the end, larger than context', () => { - element.context = 10; - const content = [ - {a: ['all work and no play make andybons a dull boy']}, - {ab: new Array(100) - .fill('all work and no play make jill a dull girl')}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - // group[1] is the "a" group - - assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[2].lines.length, 10); - for (const l of groups[2].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - - assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL); - assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup); - assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90); - for (const l of groups[3].lines[0].contextGroups[0].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - }); - }); - - test('at the end, smaller than context', () => { - element.context = 10; - const content = [ - {a: ['all work and no play make andybons a dull boy']}, - {ab: new Array(5) - .fill('all work and no play make jill a dull girl')}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - // group[1] is the "a" group - - assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[2].lines.length, 5); - for (const l of groups[2].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - }); - }); - - test('for interleaved ab and common: true chunks', () => { - element.context = 10; - const content = [ - {a: ['all work and no play make andybons a dull boy']}, - {ab: new Array(3) - .fill('all work and no play make jill a dull girl')}, - { - a: new Array(3).fill( - 'all work and no play make jill a dull girl'), - b: new Array(3).fill( - ' all work and no play make jill a dull girl'), - common: true, - }, - {ab: new Array(3) - .fill('all work and no play make jill a dull girl')}, - { - a: new Array(3).fill( - 'all work and no play make jill a dull girl'), - b: new Array(3).fill( - ' all work and no play make jill a dull girl'), - common: true, - }, - {ab: new Array(3) - .fill('all work and no play make jill a dull girl')}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - // group[1] is the "a" group - - // The first three interleaved chunks are completely shown because - // they are part of the context (3 * 3 <= 10) - - assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[2].lines.length, 3); - for (const l of groups[2].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - - assert.equal(groups[3].type, GrDiffGroup.Type.DELTA); - assert.equal(groups[3].lines.length, 6); - assert.equal(groups[3].adds.length, 3); - assert.equal(groups[3].removes.length, 3); - for (const l of groups[3].removes) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - for (const l of groups[3].adds) { - assert.equal( - l.text, ' all work and no play make jill a dull girl'); - } - - assert.equal(groups[4].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[4].lines.length, 3); - for (const l of groups[4].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - - // The next chunk is partially shown, so it results in two groups - - assert.equal(groups[5].type, GrDiffGroup.Type.DELTA); - assert.equal(groups[5].lines.length, 2); - assert.equal(groups[5].adds.length, 1); - assert.equal(groups[5].removes.length, 1); - for (const l of groups[5].removes) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - for (const l of groups[5].adds) { - assert.equal( - l.text, ' all work and no play make jill a dull girl'); - } - - assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL); - assert.equal(groups[6].lines[0].contextGroups.length, 2); - - assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4); - assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2); - assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2); - for (const l of groups[6].lines[0].contextGroups[0].removes) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - for (const l of groups[6].lines[0].contextGroups[0].adds) { - assert.equal( - l.text, ' all work and no play make jill a dull girl'); - } - - // The final chunk is completely hidden - assert.equal( - groups[6].lines[0].contextGroups[1].type, - GrDiffGroup.Type.BOTH); - assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3); - for (const l of groups[6].lines[0].contextGroups[1].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - }); - }); - - test('in the middle, larger than context', () => { - element.context = 10; - const content = [ - {a: ['all work and no play make andybons a dull boy']}, - {ab: new Array(100) - .fill('all work and no play make jill a dull girl')}, - {a: ['all work and no play make andybons a dull boy']}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - // group[1] is the "a" group - - assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[2].lines.length, 10); - for (const l of groups[2].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - - assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL); - assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup); - assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80); - for (const l of groups[3].lines[0].contextGroups[0].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - - assert.equal(groups[4].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[4].lines.length, 10); - for (const l of groups[4].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - }); - }); - - test('in the middle, smaller than context', () => { - element.context = 10; - const content = [ - {a: ['all work and no play make andybons a dull boy']}, - {ab: new Array(5) - .fill('all work and no play make jill a dull girl')}, - {a: ['all work and no play make andybons a dull boy']}, - ]; - - return element.process(content).then(() => { - const groups = element.groups; - - // group[0] is the file group - // group[1] is the "a" group - - assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); - assert.equal(groups[2].lines.length, 5); - for (const l of groups[2].lines) { - assert.equal( - l.text, 'all work and no play make jill a dull girl'); - } - }); - }); - }); - - test('break up common diff chunks', () => { - element.keyLocations = { - left: {1: true}, - right: {10: true}, - }; - + test('at the beginning, smaller than context', () => { + element.context = 10; const content = [ - { - ab: [ - 'Copyright (C) 2015 The Android Open Source Project', - '', - 'Licensed under the Apache License, Version 2.0 (the "License");', - 'you may not use this file except in compliance with the ' + - 'License.', - 'You may obtain a copy of the License at', - '', - 'http://www.apache.org/licenses/LICENSE-2.0', - '', - 'Unless required by applicable law or agreed to in writing, ', - 'software distributed under the License is distributed on an ', - '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ', - 'either express or implied. See the License for the specific ', - 'language governing permissions and limitations under the ' + - 'License.', - ], - }, + {ab: new Array(5) + .fill('all work and no play make jack a dull boy')}, + {a: ['all work and no play make andybons a dull boy']}, ]; - const result = - element._splitCommonChunksWithKeyLocations(content); - assert.deepEqual(result, [ - { - ab: ['Copyright (C) 2015 The Android Open Source Project'], - keyLocation: true, - }, - { - ab: [ - '', - '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, ', - ], - keyLocation: false, - }, - { - ab: [ - 'software distributed under the License is distributed on an '], - keyLocation: true, - }, - { - ab: [ - '"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.', - ], - keyLocation: false, - }, - ]); + + return element.process(content).then(() => { + const groups = element.groups; + + // group[0] is the file group + + assert.equal(groups[1].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[1].lines.length, 5); + for (const l of groups[1].lines) { + assert.equal(l.text, 'all work and no play make jack a dull boy'); + } + }); }); - test('breaks down shared chunks w/ whole-file', () => { - const size = 120 * 2 + 5; - const content = [{ - ab: _.times(size, () => `${Math.random()}`), - }]; - element.context = -1; - const result = element._splitLargeChunks(content); - assert.equal(result.length, 2); - assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120)); - assert.deepEqual(result[1].ab, content[0].ab.slice(120)); + test('at the end, larger than context', () => { + element.context = 10; + const content = [ + {a: ['all work and no play make andybons a dull boy']}, + {ab: new Array(100) + .fill('all work and no play make jill a dull girl')}, + ]; + + return element.process(content).then(() => { + const groups = element.groups; + + // group[0] is the file group + // group[1] is the "a" group + + assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[2].lines.length, 10); + for (const l of groups[2].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + + assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL); + assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup); + assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 90); + for (const l of groups[3].lines[0].contextGroups[0].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + }); }); - test('does not break-down common chunks w/ context', () => { - const content = [{ - ab: _.times(75, () => `${Math.random()}`), - }]; - element.context = 4; - const result = - element._splitCommonChunksWithKeyLocations(content); - assert.equal(result.length, 1); - assert.deepEqual(result[0].ab, content[0].ab); - assert.isFalse(result[0].keyLocation); + test('at the end, smaller than context', () => { + element.context = 10; + const content = [ + {a: ['all work and no play make andybons a dull boy']}, + {ab: new Array(5) + .fill('all work and no play make jill a dull girl')}, + ]; + + return element.process(content).then(() => { + const groups = element.groups; + + // group[0] is the file group + // group[1] is the "a" group + + assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[2].lines.length, 5); + for (const l of groups[2].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + }); }); - test('intraline normalization', () => { - // The content and highlights are in the format returned by the Gerrit - // REST API. - let content = [ - ' <section class="summary">', - ' <gr-linked-text content="' + - '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>', - ' </section>', - ]; - let highlights = [ - [31, 34], [42, 26], + test('for interleaved ab and common: true chunks', () => { + element.context = 10; + const content = [ + {a: ['all work and no play make andybons a dull boy']}, + {ab: new Array(3) + .fill('all work and no play make jill a dull girl')}, + { + a: new Array(3).fill( + 'all work and no play make jill a dull girl'), + b: new Array(3).fill( + ' all work and no play make jill a dull girl'), + common: true, + }, + {ab: new Array(3) + .fill('all work and no play make jill a dull girl')}, + { + a: new Array(3).fill( + 'all work and no play make jill a dull girl'), + b: new Array(3).fill( + ' all work and no play make jill a dull girl'), + common: true, + }, + {ab: new Array(3) + .fill('all work and no play make jill a dull girl')}, ]; - let results = element._convertIntralineInfos(content, - highlights); - assert.deepEqual(results, [ - { - contentIndex: 0, - startIndex: 31, - }, - { - contentIndex: 1, - startIndex: 0, - endIndex: 33, - }, - { - contentIndex: 1, - startIndex: 75, - }, - { - contentIndex: 2, - startIndex: 0, - endIndex: 6, - }, - ]); + return element.process(content).then(() => { + const groups = element.groups; - content = [ - ' this._path = value.path;', - '', - ' // When navigating away from the page, there is a ' + - 'possibility that the', - ' // patch number is no longer a part of the URL ' + - '(say when navigating to', - ' // the top-level change info view) and therefore ' + - 'undefined in `params`.', - ' if (!this._patchRange.patchNum) {', - ]; - highlights = [ - [14, 17], - [11, 70], - [12, 67], - [12, 67], - [14, 29], - ]; - results = element._convertIntralineInfos(content, highlights); - assert.deepEqual(results, [ - { - contentIndex: 0, - startIndex: 14, - endIndex: 31, - }, - { - contentIndex: 2, - startIndex: 8, - endIndex: 78, - }, - { - contentIndex: 3, - startIndex: 11, - endIndex: 78, - }, - { - contentIndex: 4, - startIndex: 11, - endIndex: 78, - }, - { - contentIndex: 5, - startIndex: 12, - endIndex: 41, - }, - ]); + // group[0] is the file group + // group[1] is the "a" group + + // The first three interleaved chunks are completely shown because + // they are part of the context (3 * 3 <= 10) + + assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[2].lines.length, 3); + for (const l of groups[2].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + + assert.equal(groups[3].type, GrDiffGroup.Type.DELTA); + assert.equal(groups[3].lines.length, 6); + assert.equal(groups[3].adds.length, 3); + assert.equal(groups[3].removes.length, 3); + for (const l of groups[3].removes) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + for (const l of groups[3].adds) { + assert.equal( + l.text, ' all work and no play make jill a dull girl'); + } + + assert.equal(groups[4].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[4].lines.length, 3); + for (const l of groups[4].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + + // The next chunk is partially shown, so it results in two groups + + assert.equal(groups[5].type, GrDiffGroup.Type.DELTA); + assert.equal(groups[5].lines.length, 2); + assert.equal(groups[5].adds.length, 1); + assert.equal(groups[5].removes.length, 1); + for (const l of groups[5].removes) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + for (const l of groups[5].adds) { + assert.equal( + l.text, ' all work and no play make jill a dull girl'); + } + + assert.equal(groups[6].type, GrDiffGroup.Type.CONTEXT_CONTROL); + assert.equal(groups[6].lines[0].contextGroups.length, 2); + + assert.equal(groups[6].lines[0].contextGroups[0].lines.length, 4); + assert.equal(groups[6].lines[0].contextGroups[0].removes.length, 2); + assert.equal(groups[6].lines[0].contextGroups[0].adds.length, 2); + for (const l of groups[6].lines[0].contextGroups[0].removes) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + for (const l of groups[6].lines[0].contextGroups[0].adds) { + assert.equal( + l.text, ' all work and no play make jill a dull girl'); + } + + // The final chunk is completely hidden + assert.equal( + groups[6].lines[0].contextGroups[1].type, + GrDiffGroup.Type.BOTH); + assert.equal(groups[6].lines[0].contextGroups[1].lines.length, 3); + for (const l of groups[6].lines[0].contextGroups[1].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + }); }); - test('scrolling pauses rendering', () => { - const contentRow = { + test('in the middle, larger than context', () => { + element.context = 10; + const content = [ + {a: ['all work and no play make andybons a dull boy']}, + {ab: new Array(100) + .fill('all work and no play make jill a dull girl')}, + {a: ['all work and no play make andybons a dull boy']}, + ]; + + return element.process(content).then(() => { + const groups = element.groups; + + // group[0] is the file group + // group[1] is the "a" group + + assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[2].lines.length, 10); + for (const l of groups[2].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + + assert.equal(groups[3].type, GrDiffGroup.Type.CONTEXT_CONTROL); + assert.instanceOf(groups[3].lines[0].contextGroups[0], GrDiffGroup); + assert.equal(groups[3].lines[0].contextGroups[0].lines.length, 80); + for (const l of groups[3].lines[0].contextGroups[0].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + + assert.equal(groups[4].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[4].lines.length, 10); + for (const l of groups[4].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + }); + }); + + test('in the middle, smaller than context', () => { + element.context = 10; + const content = [ + {a: ['all work and no play make andybons a dull boy']}, + {ab: new Array(5) + .fill('all work and no play make jill a dull girl')}, + {a: ['all work and no play make andybons a dull boy']}, + ]; + + return element.process(content).then(() => { + const groups = element.groups; + + // group[0] is the file group + // group[1] is the "a" group + + assert.equal(groups[2].type, GrDiffGroup.Type.BOTH); + assert.equal(groups[2].lines.length, 5); + for (const l of groups[2].lines) { + assert.equal( + l.text, 'all work and no play make jill a dull girl'); + } + }); + }); + }); + + test('break up common diff chunks', () => { + element.keyLocations = { + left: {1: true}, + right: {10: true}, + }; + + const content = [ + { ab: [ - '<!DOCTYPE html>', - '<meta charset="utf-8">', + 'Copyright (C) 2015 The Android Open Source Project', + '', + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the ' + + 'License.', + 'You may obtain a copy of the License at', + '', + 'http://www.apache.org/licenses/LICENSE-2.0', + '', + 'Unless required by applicable law or agreed to in writing, ', + 'software distributed under the License is distributed on an ', + '"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, ', + 'either express or implied. See the License for the specific ', + 'language governing permissions and limitations under the ' + + 'License.', ], - }; - const content = _.times(200, _.constant(contentRow)); - sandbox.stub(element, 'async'); - element._isScrolling = true; - element.process(content); - // Just the files group - no more processing during scrolling. - assert.equal(element.groups.length, 1); - - element._isScrolling = false; - element.process(content); - // More groups have been processed. How many does not matter here. - assert.isAtLeast(element.groups.length, 2); - }); - - test('image diffs', () => { - const contentRow = { + }, + ]; + const result = + element._splitCommonChunksWithKeyLocations(content); + assert.deepEqual(result, [ + { + ab: ['Copyright (C) 2015 The Android Open Source Project'], + keyLocation: true, + }, + { ab: [ - '<!DOCTYPE html>', - '<meta charset="utf-8">', + '', + '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, ', ], - }; - const content = _.times(200, _.constant(contentRow)); - sandbox.stub(element, 'async'); - element.process(content, true); - assert.equal(element.groups.length, 1); + keyLocation: false, + }, + { + ab: [ + 'software distributed under the License is distributed on an '], + keyLocation: true, + }, + { + ab: [ + '"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.', + ], + keyLocation: false, + }, + ]); + }); - // Image diffs don't process content, just the 'FILE' line. - assert.equal(element.groups[0].lines.length, 1); + test('breaks down shared chunks w/ whole-file', () => { + const size = 120 * 2 + 5; + const content = [{ + ab: _.times(size, () => `${Math.random()}`), + }]; + element.context = -1; + const result = element._splitLargeChunks(content); + assert.equal(result.length, 2); + assert.deepEqual(result[0].ab, content[0].ab.slice(0, 120)); + assert.deepEqual(result[1].ab, content[0].ab.slice(120)); + }); + + test('does not break-down common chunks w/ context', () => { + const content = [{ + ab: _.times(75, () => `${Math.random()}`), + }]; + element.context = 4; + const result = + element._splitCommonChunksWithKeyLocations(content); + assert.equal(result.length, 1); + assert.deepEqual(result[0].ab, content[0].ab); + assert.isFalse(result[0].keyLocation); + }); + + test('intraline normalization', () => { + // The content and highlights are in the format returned by the Gerrit + // REST API. + let content = [ + ' <section class="summary">', + ' <gr-linked-text content="' + + '[[_computeCurrentRevisionMessage(change)]]"></gr-linked-text>', + ' </section>', + ]; + let highlights = [ + [31, 34], [42, 26], + ]; + + let results = element._convertIntralineInfos(content, + highlights); + assert.deepEqual(results, [ + { + contentIndex: 0, + startIndex: 31, + }, + { + contentIndex: 1, + startIndex: 0, + endIndex: 33, + }, + { + contentIndex: 1, + startIndex: 75, + }, + { + contentIndex: 2, + startIndex: 0, + endIndex: 6, + }, + ]); + + content = [ + ' this._path = value.path;', + '', + ' // When navigating away from the page, there is a ' + + 'possibility that the', + ' // patch number is no longer a part of the URL ' + + '(say when navigating to', + ' // the top-level change info view) and therefore ' + + 'undefined in `params`.', + ' if (!this._patchRange.patchNum) {', + ]; + highlights = [ + [14, 17], + [11, 70], + [12, 67], + [12, 67], + [14, 29], + ]; + results = element._convertIntralineInfos(content, highlights); + assert.deepEqual(results, [ + { + contentIndex: 0, + startIndex: 14, + endIndex: 31, + }, + { + contentIndex: 2, + startIndex: 8, + endIndex: 78, + }, + { + contentIndex: 3, + startIndex: 11, + endIndex: 78, + }, + { + contentIndex: 4, + startIndex: 11, + endIndex: 78, + }, + { + contentIndex: 5, + startIndex: 12, + endIndex: 41, + }, + ]); + }); + + test('scrolling pauses rendering', () => { + const contentRow = { + ab: [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + ], + }; + const content = _.times(200, _.constant(contentRow)); + sandbox.stub(element, 'async'); + element._isScrolling = true; + element.process(content); + // Just the files group - no more processing during scrolling. + assert.equal(element.groups.length, 1); + + element._isScrolling = false; + element.process(content); + // More groups have been processed. How many does not matter here. + assert.isAtLeast(element.groups.length, 2); + }); + + test('image diffs', () => { + const contentRow = { + ab: [ + '<!DOCTYPE html>', + '<meta charset="utf-8">', + ], + }; + const content = _.times(200, _.constant(contentRow)); + sandbox.stub(element, 'async'); + element.process(content, true); + assert.equal(element.groups.length, 1); + + // Image diffs don't process content, just the 'FILE' line. + assert.equal(element.groups[0].lines.length, 1); + }); + + suite('_processNext', () => { + let rows; + + setup(() => { + rows = loremIpsum.split(' '); }); - suite('_processNext', () => { - let rows; + test('WHOLE_FILE', () => { + element.context = WHOLE_FILE; + const state = { + lineNums: {left: 10, right: 100}, + chunkIndex: 1, + }; + const chunks = [ + {a: ['foo']}, + {ab: rows}, + {a: ['bar']}, + ]; + const result = element._processNext(state, chunks); + + // Results in one, uncollapsed group with all rows. + assert.equal(result.groups.length, 1); + assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH); + assert.equal(result.groups[0].lines.length, rows.length); + + // Line numbers are set correctly. + assert.equal( + result.groups[0].lines[0].beforeNumber, + state.lineNums.left + 1); + assert.equal( + result.groups[0].lines[0].afterNumber, + state.lineNums.right + 1); + + assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber, + state.lineNums.left + rows.length); + assert.equal(result.groups[0].lines[rows.length - 1].afterNumber, + state.lineNums.right + rows.length); + }); + + test('with context', () => { + element.context = 10; + const state = { + lineNums: {left: 10, right: 100}, + chunkIndex: 1, + }; + const chunks = [ + {a: ['foo']}, + {ab: rows}, + {a: ['bar']}, + ]; + const result = element._processNext(state, chunks); + const expectedCollapseSize = rows.length - 2 * element.context; + + assert.equal(result.groups.length, 3, 'Results in three groups'); + + // The first and last are uncollapsed context, whereas the middle has + // a single context-control line. + assert.equal(result.groups[0].lines.length, element.context); + assert.equal(result.groups[1].lines.length, 1); + assert.equal(result.groups[2].lines.length, element.context); + + // The collapsed group has the hidden lines as its context group. + assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length, + expectedCollapseSize); + }); + + test('first', () => { + element.context = 10; + const state = { + lineNums: {left: 10, right: 100}, + chunkIndex: 0, + }; + const chunks = [ + {ab: rows}, + {a: ['foo']}, + {a: ['bar']}, + ]; + const result = element._processNext(state, chunks); + const expectedCollapseSize = rows.length - element.context; + + assert.equal(result.groups.length, 2, 'Results in two groups'); + + // Only the first group is collapsed. + assert.equal(result.groups[0].lines.length, 1); + assert.equal(result.groups[1].lines.length, element.context); + + // The collapsed group has the hidden lines as its context group. + assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length, + expectedCollapseSize); + }); + + test('few-rows', () => { + // Only ten rows. + rows = rows.slice(0, 10); + element.context = 10; + const state = { + lineNums: {left: 10, right: 100}, + chunkIndex: 0, + }; + const chunks = [ + {ab: rows}, + {a: ['foo']}, + {a: ['bar']}, + ]; + const result = element._processNext(state, chunks); + + // Results in one uncollapsed group with all rows. + assert.equal(result.groups.length, 1, 'Results in one group'); + assert.equal(result.groups[0].lines.length, rows.length); + }); + + test('no single line collapse', () => { + rows = rows.slice(0, 7); + element.context = 3; + const state = { + lineNums: {left: 10, right: 100}, + chunkIndex: 1, + }; + const chunks = [ + {a: ['foo']}, + {ab: rows}, + {a: ['bar']}, + ]; + const result = element._processNext(state, chunks); + + // Results in one uncollapsed group with all rows. + assert.equal(result.groups.length, 1, 'Results in one group'); + assert.equal(result.groups[0].lines.length, rows.length); + }); + + suite('with key location', () => { + let state; + let chunks; setup(() => { - rows = loremIpsum.split(' '); - }); - - test('WHOLE_FILE', () => { - element.context = WHOLE_FILE; - const state = { + state = { lineNums: {left: 10, right: 100}, - chunkIndex: 1, }; - const chunks = [ - {a: ['foo']}, - {ab: rows}, - {a: ['bar']}, - ]; - const result = element._processNext(state, chunks); - - // Results in one, uncollapsed group with all rows. - assert.equal(result.groups.length, 1); - assert.equal(result.groups[0].type, GrDiffGroup.Type.BOTH); - assert.equal(result.groups[0].lines.length, rows.length); - - // Line numbers are set correctly. - assert.equal( - result.groups[0].lines[0].beforeNumber, - state.lineNums.left + 1); - assert.equal( - result.groups[0].lines[0].afterNumber, - state.lineNums.right + 1); - - assert.equal(result.groups[0].lines[rows.length - 1].beforeNumber, - state.lineNums.left + rows.length); - assert.equal(result.groups[0].lines[rows.length - 1].afterNumber, - state.lineNums.right + rows.length); - }); - - test('with context', () => { element.context = 10; - const state = { - lineNums: {left: 10, right: 100}, - chunkIndex: 1, - }; - const chunks = [ - {a: ['foo']}, + chunks = [ {ab: rows}, - {a: ['bar']}, + {ab: ['foo'], keyLocation: true}, + {ab: rows}, ]; - const result = element._processNext(state, chunks); - const expectedCollapseSize = rows.length - 2 * element.context; - - assert.equal(result.groups.length, 3, 'Results in three groups'); - - // The first and last are uncollapsed context, whereas the middle has - // a single context-control line. - assert.equal(result.groups[0].lines.length, element.context); - assert.equal(result.groups[1].lines.length, 1); - assert.equal(result.groups[2].lines.length, element.context); - - // The collapsed group has the hidden lines as its context group. - assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length, - expectedCollapseSize); }); - test('first', () => { - element.context = 10; - const state = { - lineNums: {left: 10, right: 100}, - chunkIndex: 0, - }; - const chunks = [ - {ab: rows}, - {a: ['foo']}, - {a: ['bar']}, - ]; + test('context before', () => { + state.chunkIndex = 0; const result = element._processNext(state, chunks); - const expectedCollapseSize = rows.length - element.context; - assert.equal(result.groups.length, 2, 'Results in two groups'); - - // Only the first group is collapsed. + // The first chunk is split into two groups: + // 1) A context-control, hiding everything but the context before + // the key location. + // 2) The context before the key location. + // The key location is not processed in this call to _processNext + assert.equal(result.groups.length, 2); assert.equal(result.groups[0].lines.length, 1); - assert.equal(result.groups[1].lines.length, element.context); - // The collapsed group has the hidden lines as its context group. assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length, - expectedCollapseSize); + rows.length - element.context); + assert.equal(result.groups[1].lines.length, element.context); }); - test('few-rows', () => { - // Only ten rows. - rows = rows.slice(0, 10); - element.context = 10; - const state = { - lineNums: {left: 10, right: 100}, - chunkIndex: 0, - }; - const chunks = [ - {ab: rows}, - {a: ['foo']}, - {a: ['bar']}, - ]; + test('key location itself', () => { + state.chunkIndex = 1; const result = element._processNext(state, chunks); - // Results in one uncollapsed group with all rows. - assert.equal(result.groups.length, 1, 'Results in one group'); - assert.equal(result.groups[0].lines.length, rows.length); + // The second chunk results in a single group, that is just the + // line with the key location + assert.equal(result.groups.length, 1); + assert.equal(result.groups[0].lines.length, 1); + assert.equal(result.lineDelta.left, 1); + assert.equal(result.lineDelta.right, 1); }); - test('no single line collapse', () => { - rows = rows.slice(0, 7); - element.context = 3; - const state = { - lineNums: {left: 10, right: 100}, - chunkIndex: 1, - }; - const chunks = [ - {a: ['foo']}, - {ab: rows}, - {a: ['bar']}, - ]; + test('context after', () => { + state.chunkIndex = 2; const result = element._processNext(state, chunks); - // Results in one uncollapsed group with all rows. - assert.equal(result.groups.length, 1, 'Results in one group'); - assert.equal(result.groups[0].lines.length, rows.length); - }); - - suite('with key location', () => { - let state; - let chunks; - - setup(() => { - state = { - lineNums: {left: 10, right: 100}, - }; - element.context = 10; - chunks = [ - {ab: rows}, - {ab: ['foo'], keyLocation: true}, - {ab: rows}, - ]; - }); - - test('context before', () => { - state.chunkIndex = 0; - const result = element._processNext(state, chunks); - - // The first chunk is split into two groups: - // 1) A context-control, hiding everything but the context before - // the key location. - // 2) The context before the key location. - // The key location is not processed in this call to _processNext - assert.equal(result.groups.length, 2); - assert.equal(result.groups[0].lines.length, 1); - // The collapsed group has the hidden lines as its context group. - assert.equal(result.groups[0].lines[0].contextGroups[0].lines.length, - rows.length - element.context); - assert.equal(result.groups[1].lines.length, element.context); - }); - - test('key location itself', () => { - state.chunkIndex = 1; - const result = element._processNext(state, chunks); - - // The second chunk results in a single group, that is just the - // line with the key location - assert.equal(result.groups.length, 1); - assert.equal(result.groups[0].lines.length, 1); - assert.equal(result.lineDelta.left, 1); - assert.equal(result.lineDelta.right, 1); - }); - - test('context after', () => { - state.chunkIndex = 2; - const result = element._processNext(state, chunks); - - // The last chunk is split into two groups: - // 1) The context after the key location. - // 1) A context-control, hiding everything but the context after the - // key location. - assert.equal(result.groups.length, 2); - assert.equal(result.groups[0].lines.length, element.context); - assert.equal(result.groups[1].lines.length, 1); - // The collapsed group has the hidden lines as its context group. - assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length, - rows.length - element.context); - }); - }); - }); - - suite('gr-diff-processor helpers', () => { - let rows; - - setup(() => { - rows = loremIpsum.split(' '); - }); - - test('_linesFromRows', () => { - const startLineNum = 10; - let result = element._linesFromRows(GrDiffLine.Type.ADD, rows, - startLineNum + 1); - - assert.equal(result.length, rows.length); - assert.equal(result[0].type, GrDiffLine.Type.ADD); - assert.equal(result[0].afterNumber, startLineNum + 1); - assert.notOk(result[0].beforeNumber); - assert.equal(result[result.length - 1].afterNumber, - startLineNum + rows.length); - assert.notOk(result[result.length - 1].beforeNumber); - - result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows, - startLineNum + 1); - - assert.equal(result.length, rows.length); - assert.equal(result[0].type, GrDiffLine.Type.REMOVE); - assert.equal(result[0].beforeNumber, startLineNum + 1); - assert.notOk(result[0].afterNumber); - assert.equal(result[result.length - 1].beforeNumber, - startLineNum + rows.length); - assert.notOk(result[result.length - 1].afterNumber); - }); - }); - - suite('_breakdown*', () => { - test('_breakdownChunk breaks down additions', () => { - sandbox.spy(element, '_breakdown'); - const chunk = {b: ['blah', 'blah', 'blah']}; - const result = element._breakdownChunk(chunk); - assert.deepEqual(result, [chunk]); - assert.isTrue(element._breakdown.called); - }); - - test('_breakdownChunk keeps due_to_rebase for broken down additions', - () => { - sandbox.spy(element, '_breakdown'); - const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true}; - const result = element._breakdownChunk(chunk); - for (const subResult of result) { - assert.isTrue(subResult.due_to_rebase); - } - }); - - test('_breakdown common case', () => { - const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' - .split(' '); - const size = 3; - - const result = element._breakdown(array, size); - - for (const subResult of result) { - assert.isAtMost(subResult.length, size); - } - const flattened = result - .reduce((a, b) => a.concat(b), []); - assert.deepEqual(flattened, array); - }); - - test('_breakdown smaller than size', () => { - const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' - .split(' '); - const size = 10; - const expected = [array]; - - const result = element._breakdown(array, size); - - assert.deepEqual(result, expected); - }); - - test('_breakdown empty', () => { - const array = []; - const size = 10; - const expected = []; - - const result = element._breakdown(array, size); - - assert.deepEqual(result, expected); + // The last chunk is split into two groups: + // 1) The context after the key location. + // 1) A context-control, hiding everything but the context after the + // key location. + assert.equal(result.groups.length, 2); + assert.equal(result.groups[0].lines.length, element.context); + assert.equal(result.groups[1].lines.length, 1); + // The collapsed group has the hidden lines as its context group. + assert.equal(result.groups[1].lines[0].contextGroups[0].lines.length, + rows.length - element.context); }); }); }); - test('detaching cancels', () => { - element = fixture('basic'); - sandbox.stub(element, 'cancel'); - element.detached(); - assert(element.cancel.called); + suite('gr-diff-processor helpers', () => { + let rows; + + setup(() => { + rows = loremIpsum.split(' '); + }); + + test('_linesFromRows', () => { + const startLineNum = 10; + let result = element._linesFromRows(GrDiffLine.Type.ADD, rows, + startLineNum + 1); + + assert.equal(result.length, rows.length); + assert.equal(result[0].type, GrDiffLine.Type.ADD); + assert.equal(result[0].afterNumber, startLineNum + 1); + assert.notOk(result[0].beforeNumber); + assert.equal(result[result.length - 1].afterNumber, + startLineNum + rows.length); + assert.notOk(result[result.length - 1].beforeNumber); + + result = element._linesFromRows(GrDiffLine.Type.REMOVE, rows, + startLineNum + 1); + + assert.equal(result.length, rows.length); + assert.equal(result[0].type, GrDiffLine.Type.REMOVE); + assert.equal(result[0].beforeNumber, startLineNum + 1); + assert.notOk(result[0].afterNumber); + assert.equal(result[result.length - 1].beforeNumber, + startLineNum + rows.length); + assert.notOk(result[result.length - 1].afterNumber); + }); + }); + + suite('_breakdown*', () => { + test('_breakdownChunk breaks down additions', () => { + sandbox.spy(element, '_breakdown'); + const chunk = {b: ['blah', 'blah', 'blah']}; + const result = element._breakdownChunk(chunk); + assert.deepEqual(result, [chunk]); + assert.isTrue(element._breakdown.called); + }); + + test('_breakdownChunk keeps due_to_rebase for broken down additions', + () => { + sandbox.spy(element, '_breakdown'); + const chunk = {b: ['blah', 'blah', 'blah'], due_to_rebase: true}; + const result = element._breakdownChunk(chunk); + for (const subResult of result) { + assert.isTrue(subResult.due_to_rebase); + } + }); + + test('_breakdown common case', () => { + const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' + .split(' '); + const size = 3; + + const result = element._breakdown(array, size); + + for (const subResult of result) { + assert.isAtMost(subResult.length, size); + } + const flattened = result + .reduce((a, b) => a.concat(b), []); + assert.deepEqual(flattened, array); + }); + + test('_breakdown smaller than size', () => { + const array = 'Lorem ipsum dolor sit amet, suspendisse inceptos' + .split(' '); + const size = 10; + const expected = [array]; + + const result = element._breakdown(array, size); + + assert.deepEqual(result, expected); + }); + + test('_breakdown empty', () => { + const array = []; + const size = 10; + const expected = []; + + const result = element._breakdown(array, size); + + assert.deepEqual(result, expected); + }); }); }); + + test('detaching cancels', () => { + element = fixture('basic'); + sandbox.stub(element, 'cancel'); + element.detached(); + assert(element.cancel.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js index e46f959..e59b0c2 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -14,352 +14,364 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * Possible CSS classes indicating the state of selection. Dynamically added/ - * removed based on where the user clicks within the diff. - */ - const SelectionClass = { - COMMENT: 'selected-comment', - LEFT: 'selected-left', - RIGHT: 'selected-right', - BLAME: 'selected-blame', - }; +import '../../../behaviors/dom-util-behavior/dom-util-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../../scripts/util.js'; +import '../gr-diff-highlight/gr-range-normalizer.js'; +import {addListener} from '@polymer/polymer/lib/utils/gestures.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-selection_html.js'; - const getNewCache = () => { return {left: null, right: null}; }; +/** + * Possible CSS classes indicating the state of selection. Dynamically added/ + * removed based on where the user clicks within the diff. + */ +const SelectionClass = { + COMMENT: 'selected-comment', + LEFT: 'selected-left', + RIGHT: 'selected-right', + BLAME: 'selected-blame', +}; - /** - * @appliesMixin Gerrit.DomUtilMixin - * @extends Polymer.Element - */ - class GrDiffSelection extends Polymer.mixinBehaviors( [ - Gerrit.DomUtilBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff-selection'; } +const getNewCache = () => { return {left: null, right: null}; }; - static get properties() { - return { - diff: Object, - /** @type {?Object} */ - _cachedDiffBuilder: Object, - _linesCache: { - type: Object, - value: getNewCache(), - }, - }; +/** + * @appliesMixin Gerrit.DomUtilMixin + * @extends Polymer.Element + */ +class GrDiffSelection extends mixinBehaviors( [ + Gerrit.DomUtilBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-selection'; } + + static get properties() { + return { + diff: Object, + /** @type {?Object} */ + _cachedDiffBuilder: Object, + _linesCache: { + type: Object, + value: getNewCache(), + }, + }; + } + + static get observers() { + return [ + '_diffChanged(diff)', + ]; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('copy', + e => this._handleCopy(e)); + addListener(this, 'down', + e => this._handleDown(e)); + } + + /** @override */ + attached() { + super.attached(); + this.classList.add(SelectionClass.RIGHT); + } + + get diffBuilder() { + if (!this._cachedDiffBuilder) { + this._cachedDiffBuilder = + dom(this).querySelector('gr-diff-builder'); } + return this._cachedDiffBuilder; + } - static get observers() { - return [ - '_diffChanged(diff)', - ]; - } + _diffChanged() { + this._linesCache = getNewCache(); + } - /** @override */ - created() { - super.created(); - this.addEventListener('copy', - e => this._handleCopy(e)); - Polymer.Gestures.addListener(this, 'down', - e => this._handleDown(e)); - } - - /** @override */ - attached() { - super.attached(); - this.classList.add(SelectionClass.RIGHT); - } - - get diffBuilder() { - if (!this._cachedDiffBuilder) { - this._cachedDiffBuilder = - Polymer.dom(this).querySelector('gr-diff-builder'); - } - return this._cachedDiffBuilder; - } - - _diffChanged() { - this._linesCache = getNewCache(); - } - - _handleDownOnRangeComment(node) { - if (node && - node.nodeName && - node.nodeName.toLowerCase() === 'gr-comment-thread') { - this._setClasses([ - SelectionClass.COMMENT, - node.commentSide === 'left' ? - SelectionClass.LEFT : - SelectionClass.RIGHT, - ]); - return true; - } - return false; - } - - _handleDown(e) { - // Handle the down event on comment thread in Polymer 2 - const handled = this._handleDownOnRangeComment(e.target); - if (handled) return; - - const lineEl = this.diffBuilder.getLineElByChild(e.target); - const blameSelected = this._elementDescendedFromClass(e.target, 'blame'); - if (!lineEl && !blameSelected) { return; } - - const targetClasses = []; - - if (blameSelected) { - targetClasses.push(SelectionClass.BLAME); - } else { - const commentSelected = - this._elementDescendedFromClass(e.target, 'gr-comment'); - const side = this.diffBuilder.getSideByLineEl(lineEl); - - targetClasses.push(side === 'left' ? + _handleDownOnRangeComment(node) { + if (node && + node.nodeName && + node.nodeName.toLowerCase() === 'gr-comment-thread') { + this._setClasses([ + SelectionClass.COMMENT, + node.commentSide === 'left' ? SelectionClass.LEFT : - SelectionClass.RIGHT); - - if (commentSelected) { - targetClasses.push(SelectionClass.COMMENT); - } - } - - this._setClasses(targetClasses); + SelectionClass.RIGHT, + ]); + return true; } + return false; + } - /** - * Set the provided list of classes on the element, to the exclusion of all - * other SelectionClass values. - * - * @param {!Array<!string>} targetClasses - */ - _setClasses(targetClasses) { - // Remove any selection classes that do not belong. - for (const key in SelectionClass) { - if (SelectionClass.hasOwnProperty(key)) { - const className = SelectionClass[key]; - if (!targetClasses.includes(className)) { - this.classList.remove(SelectionClass[key]); - } - } - } - // Add new selection classes iff they are not already present. - for (const _class of targetClasses) { - if (!this.classList.contains(_class)) { - this.classList.add(_class); - } - } - } + _handleDown(e) { + // Handle the down event on comment thread in Polymer 2 + const handled = this._handleDownOnRangeComment(e.target); + if (handled) return; - _getCopyEventTarget(e) { - return Polymer.dom(e).rootTarget; - } + const lineEl = this.diffBuilder.getLineElByChild(e.target); + const blameSelected = this._elementDescendedFromClass(e.target, 'blame'); + if (!lineEl && !blameSelected) { return; } - /** - * Utility function to determine whether an element is a descendant of - * another element with the particular className. - * - * @param {!Element} element - * @param {!string} className - * @return {boolean} - */ - _elementDescendedFromClass(element, className) { - return this.descendedFromClass(element, className, - this.diffBuilder.diffElement); - } + const targetClasses = []; - _handleCopy(e) { - let commentSelected = false; - const target = this._getCopyEventTarget(e); - if (target.type === 'textarea') { return; } - if (!this._elementDescendedFromClass(target, 'diff-row')) { return; } - if (this.classList.contains(SelectionClass.COMMENT)) { - commentSelected = true; - } - const lineEl = this.diffBuilder.getLineElByChild(target); - if (!lineEl) { - return; - } + if (blameSelected) { + targetClasses.push(SelectionClass.BLAME); + } else { + const commentSelected = + this._elementDescendedFromClass(e.target, 'gr-comment'); const side = this.diffBuilder.getSideByLineEl(lineEl); - const text = this._getSelectedText(side, commentSelected); - if (text) { - e.clipboardData.setData('Text', text); - e.preventDefault(); - } - } - _getSelection() { - const diffHosts = util.querySelectorAll(document.body, 'gr-diff'); - if (!diffHosts.length) return window.getSelection(); + targetClasses.push(side === 'left' ? + SelectionClass.LEFT : + SelectionClass.RIGHT); - const curDiffHost = diffHosts.find(diffHost => { - if (!diffHost || !diffHost.shadowRoot) return false; - const selection = diffHost.shadowRoot.getSelection(); - // Pick the one with valid selection: - // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type - return selection && selection.type !== 'None'; - }); - - return curDiffHost ? - curDiffHost.shadowRoot.getSelection(): window.getSelection(); - } - - /** - * Get the text of the current selection. If commentSelected is - * true, it returns only the text of comments within the selection. - * Otherwise it returns the text of the selected diff region. - * - * @param {!string} side The side that is selected. - * @param {boolean} commentSelected Whether or not a comment is selected. - * @return {string} The selected text. - */ - _getSelectedText(side, commentSelected) { - const sel = this._getSelection(); - if (sel.rangeCount != 1) { - return ''; // No multi-select support yet. - } if (commentSelected) { - return this._getCommentLines(sel, side); + targetClasses.push(SelectionClass.COMMENT); } - const range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); - const startLineEl = - this.diffBuilder.getLineElByChild(range.startContainer); - const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer); - // Happens when triple click in side-by-side mode with other side empty. - const endsAtOtherEmptySide = !endLineEl && - range.endOffset === 0 && - range.endContainer.nodeName === 'TD' && - (range.endContainer.classList.contains('left') || - range.endContainer.classList.contains('right')); - const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10); - let endLineNum; - if (endsAtOtherEmptySide) { - endLineNum = startLineNum + 1; - } else if (endLineEl) { - endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10); - } - - return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum, - range.endOffset, side); } - /** - * Query the diff object for the selected lines. - * - * @param {number} startLineNum - * @param {number} startOffset - * @param {number|undefined} endLineNum Use undefined to get the range - * extending to the end of the file. - * @param {number} endOffset - * @param {!string} side The side that is currently selected. - * @return {string} The selected diff text. - */ - _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) { - 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); + this._setClasses(targetClasses); + } + + /** + * Set the provided list of classes on the element, to the exclusion of all + * other SelectionClass values. + * + * @param {!Array<!string>} targetClasses + */ + _setClasses(targetClasses) { + // Remove any selection classes that do not belong. + for (const key in SelectionClass) { + if (SelectionClass.hasOwnProperty(key)) { + const className = SelectionClass[key]; + if (!targetClasses.includes(className)) { + this.classList.remove(SelectionClass[key]); + } } - return lines.join('\n'); } - - /** - * Query the diff object for the lines from a particular side. - * - * @param {!string} side The side that is currently selected. - * @return {!Array<string>} An array of strings indexed by line number. - */ - _getDiffLines(side) { - if (this._linesCache[side]) { - return this._linesCache[side]; + // Add new selection classes iff they are not already present. + for (const _class of targetClasses) { + if (!this.classList.contains(_class)) { + this.classList.add(_class); } - let lines = []; - const key = side === 'left' ? 'a' : 'b'; - for (const chunk of this.diff.content) { - if (chunk.ab) { - lines = lines.concat(chunk.ab); - } else if (chunk[key]) { - lines = lines.concat(chunk[key]); - } - } - this._linesCache[side] = lines; - return lines; - } - - /** - * Query the diffElement for comments and check whether they lie inside the - * selection range. - * - * @param {!Selection} sel The selection of the window. - * @param {!string} side The side that is currently selected. - * @return {string} The selected comment text. - */ - _getCommentLines(sel, side) { - const range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); - const content = []; - // Query the diffElement for comments. - const messages = this.diffBuilder.diffElement.querySelectorAll( - `.side-by-side [data-side="${side - }"] .message *, .unified .message *`); - - for (let i = 0; i < messages.length; i++) { - const el = messages[i]; - // Check if the comment element exists inside the selection. - if (sel.containsNode(el, true)) { - // Padded elements require newlines for accurate spacing. - if (el.parentElement.id === 'container' || - el.parentElement.nodeName === 'BLOCKQUOTE') { - if (content.length && content[content.length - 1] !== '') { - content.push(''); - } - } - - if (el.id === 'output' && - !this._elementDescendedFromClass(el, 'collapsed')) { - content.push(this._getTextContentForRange(el, sel, range)); - } - } - } - - return content.join('\n'); - } - - /** - * Given a DOM node, a selection, and a selection range, recursively get all - * of the text content within that selection. - * Using a domNode that isn't in the selection returns an empty string. - * - * @param {!Node} domNode The root DOM node. - * @param {!Selection} sel The selection. - * @param {!Range} range The normalized selection range. - * @return {string} The text within the selection. - */ - _getTextContentForRange(domNode, sel, range) { - if (!sel.containsNode(domNode, true)) { return ''; } - - let text = ''; - if (domNode instanceof Text) { - text = domNode.textContent; - if (domNode === range.endContainer) { - text = text.substring(0, range.endOffset); - } - if (domNode === range.startContainer) { - text = text.substring(range.startOffset); - } - } else { - for (const childNode of domNode.childNodes) { - text += this._getTextContentForRange(childNode, sel, range); - } - } - return text; } } - customElements.define(GrDiffSelection.is, GrDiffSelection); -})(); + _getCopyEventTarget(e) { + return dom(e).rootTarget; + } + + /** + * Utility function to determine whether an element is a descendant of + * another element with the particular className. + * + * @param {!Element} element + * @param {!string} className + * @return {boolean} + */ + _elementDescendedFromClass(element, className) { + return this.descendedFromClass(element, className, + this.diffBuilder.diffElement); + } + + _handleCopy(e) { + let commentSelected = false; + const target = this._getCopyEventTarget(e); + if (target.type === 'textarea') { return; } + if (!this._elementDescendedFromClass(target, 'diff-row')) { return; } + if (this.classList.contains(SelectionClass.COMMENT)) { + commentSelected = true; + } + const lineEl = this.diffBuilder.getLineElByChild(target); + if (!lineEl) { + return; + } + const side = this.diffBuilder.getSideByLineEl(lineEl); + const text = this._getSelectedText(side, commentSelected); + if (text) { + e.clipboardData.setData('Text', text); + e.preventDefault(); + } + } + + _getSelection() { + const diffHosts = util.querySelectorAll(document.body, 'gr-diff'); + if (!diffHosts.length) return window.getSelection(); + + const curDiffHost = diffHosts.find(diffHost => { + if (!diffHost || !diffHost.shadowRoot) return false; + const selection = diffHost.shadowRoot.getSelection(); + // Pick the one with valid selection: + // https://developer.mozilla.org/en-US/docs/Web/API/Selection/type + return selection && selection.type !== 'None'; + }); + + return curDiffHost ? + curDiffHost.shadowRoot.getSelection(): window.getSelection(); + } + + /** + * Get the text of the current selection. If commentSelected is + * true, it returns only the text of comments within the selection. + * Otherwise it returns the text of the selected diff region. + * + * @param {!string} side The side that is selected. + * @param {boolean} commentSelected Whether or not a comment is selected. + * @return {string} The selected text. + */ + _getSelectedText(side, commentSelected) { + const sel = this._getSelection(); + if (sel.rangeCount != 1) { + return ''; // No multi-select support yet. + } + if (commentSelected) { + return this._getCommentLines(sel, side); + } + const range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); + const startLineEl = + this.diffBuilder.getLineElByChild(range.startContainer); + const endLineEl = this.diffBuilder.getLineElByChild(range.endContainer); + // Happens when triple click in side-by-side mode with other side empty. + const endsAtOtherEmptySide = !endLineEl && + range.endOffset === 0 && + range.endContainer.nodeName === 'TD' && + (range.endContainer.classList.contains('left') || + range.endContainer.classList.contains('right')); + const startLineNum = parseInt(startLineEl.getAttribute('data-value'), 10); + let endLineNum; + if (endsAtOtherEmptySide) { + endLineNum = startLineNum + 1; + } else if (endLineEl) { + endLineNum = parseInt(endLineEl.getAttribute('data-value'), 10); + } + + return this._getRangeFromDiff(startLineNum, range.startOffset, endLineNum, + range.endOffset, side); + } + + /** + * Query the diff object for the selected lines. + * + * @param {number} startLineNum + * @param {number} startOffset + * @param {number|undefined} endLineNum Use undefined to get the range + * extending to the end of the file. + * @param {number} endOffset + * @param {!string} side The side that is currently selected. + * @return {string} The selected diff text. + */ + _getRangeFromDiff(startLineNum, startOffset, endLineNum, endOffset, side) { + 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); + } + return lines.join('\n'); + } + + /** + * Query the diff object for the lines from a particular side. + * + * @param {!string} side The side that is currently selected. + * @return {!Array<string>} An array of strings indexed by line number. + */ + _getDiffLines(side) { + if (this._linesCache[side]) { + return this._linesCache[side]; + } + let lines = []; + const key = side === 'left' ? 'a' : 'b'; + for (const chunk of this.diff.content) { + if (chunk.ab) { + lines = lines.concat(chunk.ab); + } else if (chunk[key]) { + lines = lines.concat(chunk[key]); + } + } + this._linesCache[side] = lines; + return lines; + } + + /** + * Query the diffElement for comments and check whether they lie inside the + * selection range. + * + * @param {!Selection} sel The selection of the window. + * @param {!string} side The side that is currently selected. + * @return {string} The selected comment text. + */ + _getCommentLines(sel, side) { + const range = GrRangeNormalizer.normalize(sel.getRangeAt(0)); + const content = []; + // Query the diffElement for comments. + const messages = this.diffBuilder.diffElement.querySelectorAll( + `.side-by-side [data-side="${side + }"] .message *, .unified .message *`); + + for (let i = 0; i < messages.length; i++) { + const el = messages[i]; + // Check if the comment element exists inside the selection. + if (sel.containsNode(el, true)) { + // Padded elements require newlines for accurate spacing. + if (el.parentElement.id === 'container' || + el.parentElement.nodeName === 'BLOCKQUOTE') { + if (content.length && content[content.length - 1] !== '') { + content.push(''); + } + } + + if (el.id === 'output' && + !this._elementDescendedFromClass(el, 'collapsed')) { + content.push(this._getTextContentForRange(el, sel, range)); + } + } + } + + return content.join('\n'); + } + + /** + * Given a DOM node, a selection, and a selection range, recursively get all + * of the text content within that selection. + * Using a domNode that isn't in the selection returns an empty string. + * + * @param {!Node} domNode The root DOM node. + * @param {!Selection} sel The selection. + * @param {!Range} range The normalized selection range. + * @return {string} The text within the selection. + */ + _getTextContentForRange(domNode, sel, range) { + if (!sel.containsNode(domNode, true)) { return ''; } + + let text = ''; + if (domNode instanceof Text) { + text = domNode.textContent; + if (domNode === range.endContainer) { + text = text.substring(0, range.endOffset); + } + if (domNode === range.startContainer) { + text = text.substring(range.startOffset); + } + } else { + for (const childNode of domNode.childNodes) { + text += this._getTextContentForRange(childNode, sel, range); + } + } + return text; + } +} + +customElements.define(GrDiffSelection.is, GrDiffSelection);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js index 016305c..ce6008e 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_html.js
@@ -1,30 +1,23 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/dom-util-behavior/dom-util-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<script src="../../../scripts/util.js"></script> -<script src="../gr-diff-highlight/gr-range-normalizer.js"></script> - -<dom-module id="gr-diff-selection"> - <template> +export const htmlTemplate = html` <div class="contentWrapper"> <slot></slot> </div> - </template> - <script src="gr-diff-selection.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html index a8e85e2..ac3a87f 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-selection</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-diff-selection.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-diff-selection.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-selection.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -107,298 +112,300 @@ </template> </test-fixture> -<script> - suite('gr-diff-selection', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-selection.js'; +suite('gr-diff-selection', () => { + let element; + let sandbox; - const emulateCopyOn = function(target) { - const fakeEvent = { - target, - preventDefault: sandbox.stub(), - clipboardData: { - setData: sandbox.stub(), + const emulateCopyOn = function(target) { + const fakeEvent = { + target, + preventDefault: sandbox.stub(), + clipboardData: { + setData: sandbox.stub(), + }, + }; + element._getCopyEventTarget.returns(target); + element._handleCopy(fakeEvent); + return fakeEvent; + }; + + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + sandbox.stub(element, '_getCopyEventTarget'); + element._cachedDiffBuilder = { + getLineElByChild: sandbox.stub().returns({}), + getSideByLineEl: sandbox.stub(), + diffElement: element.querySelector('#diffTable'), + }; + element.diff = { + content: [ + { + a: ['ba ba'], + b: ['some other text'], }, - }; - element._getCopyEventTarget.returns(target); - element._handleCopy(fakeEvent); - return fakeEvent; + { + a: ['zin'], + b: ['more more more'], + }, + { + a: ['ga ga'], + b: ['some other text'], + }, + ], + }; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('applies selected-left on left side click', () => { + element.classList.add('selected-right'); + element._cachedDiffBuilder.getSideByLineEl.returns('left'); + MockInteractions.down(element); + 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'); + element._cachedDiffBuilder.getSideByLineEl.returns('right'); + MockInteractions.down(element); + 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); + sandbox.stub(element, '_elementDescendedFromClass', + (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', () => { + sandbox.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'); + sandbox.stub(element, '_getSelectedText'); + emulateCopyOn(element.querySelector('div.contentText')); + assert.deepEqual(['left', false], element._getSelectedText.lastCall.args); + }); + + test('reacts to copy for content Elements', () => { + sandbox.stub(element, '_getSelectedText'); + emulateCopyOn(element.querySelector('div.contentText')); + assert.isTrue(element._getSelectedText.called); + }); + + test('copy event is prevented for content Elements', () => { + sandbox.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', () => { + sandbox.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 = sandbox.stub(element.classList, 'add'); + const removeStub = sandbox.stub(element.classList, 'remove', () => { + 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 = window.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 = window.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 = window.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 = sandbox.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 = window.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 = window.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 = fixture('basic'); - sandbox = sinon.sandbox.create(); - sandbox.stub(element, '_getCopyEventTarget'); - element._cachedDiffBuilder = { - getLineElByChild: sandbox.stub().returns({}), - getSideByLineEl: sandbox.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'], - }, - ], - }; - }); - - teardown(() => { - sandbox.restore(); - }); - - test('applies selected-left on left side click', () => { - element.classList.add('selected-right'); - element._cachedDiffBuilder.getSideByLineEl.returns('left'); - MockInteractions.down(element); - 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'); - element._cachedDiffBuilder.getSideByLineEl.returns('right'); - MockInteractions.down(element); - 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); - sandbox.stub(element, '_elementDescendedFromClass', - (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', () => { - sandbox.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'); - sandbox.stub(element, '_getSelectedText'); - emulateCopyOn(element.querySelector('div.contentText')); - assert.deepEqual(['left', false], element._getSelectedText.lastCall.args); - }); - - test('reacts to copy for content Elements', () => { - sandbox.stub(element, '_getSelectedText'); - emulateCopyOn(element.querySelector('div.contentText')); - assert.isTrue(element._getSelectedText.called); - }); - - test('copy event is prevented for content Elements', () => { - sandbox.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', () => { - sandbox.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 = sandbox.stub(element.classList, 'add'); - const removeStub = sandbox.stub(element.classList, 'remove', () => { - 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 = window.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 = window.getSelection(); + selection = window.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); + 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('s is a comment\nThis is a differ', - element._getSelectedText('left', true)); + assert.equal(element._getTextContentForRange(element, selection, range), + 'his is a differ'); }); - test('respects astral chars in comments', () => { - element.classList.add('selected-left'); - element.classList.add('selected-comment'); - element.classList.remove('selected-right'); - const selection = window.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); + 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('mment 💩 u', - element._getSelectedText('left', true)); + assert.equal(element._getTextContentForRange(element, selection, range), + 'a differ'); }); - test('defers to default behavior for textarea', () => { - element.classList.add('selected-left'); - element.classList.remove('selected-right'); - const selectedTextSpy = sandbox.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 = window.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); + test('startContainer === endContainer', () => { + range.setStart(nodes[0].firstChild, 2); + range.setEnd(nodes[0].firstChild, 12); 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 = window.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 = window.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 = {}; - flushAsynchronousOperations(); - assert.deepEqual(element._linesCache, {left: null, right: null}); + 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 = {}; + flushAsynchronousOperations(); + assert.deepEqual(element._linesCache, {left: null, right: null}); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js index eb5ea01..cf29a03 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,1225 +14,1258 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const ERR_REVIEW_STATUS = 'Couldn’t change file review status.'; - const MSG_LOADING_BLAME = 'Loading blame...'; - const MSG_LOADED_BLAME = 'Blame loaded'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '@polymer/iron-dropdown/iron-dropdown.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-dropdown-list/gr-dropdown-list.js'; +import '../../shared/gr-fixed-panel/gr-fixed-panel.js'; +import '../../shared/gr-icons/gr-icons.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import '../../shared/revision-info/revision-info.js'; +import '../gr-comment-api/gr-comment-api.js'; +import '../gr-diff-cursor/gr-diff-cursor.js'; +import '../gr-apply-fix-dialog/gr-apply-fix-dialog.js'; +import '../gr-diff-host/gr-diff-host.js'; +import '../gr-diff-mode-selector/gr-diff-mode-selector.js'; +import '../gr-diff-preferences-dialog/gr-diff-preferences-dialog.js'; +import '../gr-patch-range-select/gr-patch-range-select.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-view_html.js'; - const PARENT = 'PARENT'; +const ERR_REVIEW_STATUS = 'Couldn’t change file review status.'; +const MSG_LOADING_BLAME = 'Loading blame...'; +const MSG_LOADED_BLAME = 'Blame loaded'; - const DiffSides = { - LEFT: 'left', - RIGHT: 'right', - }; +const PARENT = 'PARENT'; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +const DiffSides = { + LEFT: 'left', + RIGHT: 'right', +}; + +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrDiffView extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.PathListBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff-view'; } + /** + * Fired when the title of the page should change. + * + * @event title-change + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.PathListMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when user tries to navigate away while comments are pending save. + * + * @event show-alert */ - class GrDiffView extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.PathListBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff-view'; } - /** - * Fired when the title of the page should change. - * - * @event title-change - */ + static get properties() { + return { /** - * Fired when user tries to navigate away while comments are pending save. - * - * @event show-alert + * URL params passed from the router. */ - - static get properties() { - return { + params: { + type: Object, + observer: '_paramsChanged', + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, /** - * URL params passed from the router. + * @type {{ diffMode: (string|undefined) }} */ - params: { - type: Object, - observer: '_paramsChanged', - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - /** - * @type {{ diffMode: (string|undefined) }} - */ - changeViewState: { - type: Object, - notify: true, - value() { return {}; }, - observer: '_changeViewStateChanged', - }, - disableDiffPrefs: { - type: Boolean, - value: false, - }, - _diffPrefsDisabled: { - type: Boolean, - computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', - }, - /** @type {?} */ - _patchRange: Object, - /** @type {?} */ - _commitRange: Object, - /** - * @type {{ - * subject: string, - * project: string, - * revisions: string, - * }} - */ - _change: Object, - /** @type {?} */ - _changeComments: Object, - _changeNum: String, - /** - * This is a DiffInfo object. - * This is retrieved and owned by a child component. - */ - _diff: Object, - // An array specifically formatted to be used in a gr-dropdown-list - // element for selected a file to view. - _formattedFiles: { - type: Array, - computed: '_formatFilesForDropdown(_files, ' + - '_patchRange.patchNum, _changeComments)', - }, - // An sorted array of files, as returned by the rest API. - _fileList: { - type: Array, - computed: '_getSortedFileList(_files)', - }, - /** - * Contains information about files as returned by the rest API. - * - * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }} - */ - _files: { - type: Object, - value() { return {sortedFileList: [], changeFilesByPath: {}}; }, - }, + changeViewState: { + type: Object, + notify: true, + value() { return {}; }, + observer: '_changeViewStateChanged', + }, + disableDiffPrefs: { + type: Boolean, + value: false, + }, + _diffPrefsDisabled: { + type: Boolean, + computed: '_computeDiffPrefsDisabled(disableDiffPrefs, _loggedIn)', + }, + /** @type {?} */ + _patchRange: Object, + /** @type {?} */ + _commitRange: Object, + /** + * @type {{ + * subject: string, + * project: string, + * revisions: string, + * }} + */ + _change: Object, + /** @type {?} */ + _changeComments: Object, + _changeNum: String, + /** + * This is a DiffInfo object. + * This is retrieved and owned by a child component. + */ + _diff: Object, + // An array specifically formatted to be used in a gr-dropdown-list + // element for selected a file to view. + _formattedFiles: { + type: Array, + computed: '_formatFilesForDropdown(_files, ' + + '_patchRange.patchNum, _changeComments)', + }, + // An sorted array of files, as returned by the rest API. + _fileList: { + type: Array, + computed: '_getSortedFileList(_files)', + }, + /** + * Contains information about files as returned by the rest API. + * + * @type {{ sortedFileList: Array<string>, changeFilesByPath: Object }} + */ + _files: { + type: Object, + value() { return {sortedFileList: [], changeFilesByPath: {}}; }, + }, - _path: { - type: String, - observer: '_pathChanged', - }, - _fileNum: { - type: Number, - computed: '_computeFileNum(_path, _formattedFiles)', - }, - _loggedIn: { - type: Boolean, - value: false, - }, - _loading: { - type: Boolean, - value: true, - }, - _prefs: Object, - _localPrefs: Object, - _projectConfig: Object, - _userPrefs: Object, - _diffMode: { - type: String, - computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)', - }, - _isImageDiff: Boolean, - _filesWeblinks: Object, + _path: { + type: String, + observer: '_pathChanged', + }, + _fileNum: { + type: Number, + computed: '_computeFileNum(_path, _formattedFiles)', + }, + _loggedIn: { + type: Boolean, + value: false, + }, + _loading: { + type: Boolean, + value: true, + }, + _prefs: Object, + _localPrefs: Object, + _projectConfig: Object, + _userPrefs: Object, + _diffMode: { + type: String, + computed: '_getDiffViewMode(changeViewState.diffMode, _userPrefs)', + }, + _isImageDiff: Boolean, + _filesWeblinks: Object, - /** - * Map of paths in the current change and patch range that have comments - * or drafts or robot comments. - */ - _commentMap: Object, + /** + * Map of paths in the current change and patch range that have comments + * or drafts or robot comments. + */ + _commentMap: Object, - _commentsForDiff: Object, + _commentsForDiff: Object, - /** - * Object to contain the path of the next and previous file in the current - * change and patch range that has comments. - */ - _commentSkips: { - type: Object, - computed: '_computeCommentSkips(_commentMap, _fileList, _path)', - }, - _panelFloatingDisabled: { - type: Boolean, - value: () => window.PANEL_FLOATING_DISABLED, - }, - _editMode: { - type: Boolean, - computed: '_computeEditMode(_patchRange.*)', - }, - _isBlameLoaded: Boolean, - _isBlameLoading: { - type: Boolean, - value: false, - }, - _allPatchSets: { - type: Array, - computed: 'computeAllPatchSets(_change, _change.revisions.*)', - }, - _revisionInfo: { - type: Object, - computed: '_getRevisionInfo(_change)', - }, - _reviewedFiles: { - type: Object, - value: () => new Set(), - }, + /** + * Object to contain the path of the next and previous file in the current + * change and patch range that has comments. + */ + _commentSkips: { + type: Object, + computed: '_computeCommentSkips(_commentMap, _fileList, _path)', + }, + _panelFloatingDisabled: { + type: Boolean, + value: () => window.PANEL_FLOATING_DISABLED, + }, + _editMode: { + type: Boolean, + computed: '_computeEditMode(_patchRange.*)', + }, + _isBlameLoaded: Boolean, + _isBlameLoading: { + type: Boolean, + value: false, + }, + _allPatchSets: { + type: Array, + computed: 'computeAllPatchSets(_change, _change.revisions.*)', + }, + _revisionInfo: { + type: Object, + computed: '_getRevisionInfo(_change)', + }, + _reviewedFiles: { + type: Object, + value: () => new Set(), + }, - /** - * gr-diff-view has gr-fixed-panel on top. The panel can - * intersect a main element and partially hides a content of - * the main element. To correctly calculates visibility of an - * element, the cursor must know how much height occuped by a fixed - * panel. - * The scrollTopMargin defines margin occuped by fixed panel. - */ - _scrollTopMargin: { - type: Number, - value: 0, - }, - }; - } + /** + * gr-diff-view has gr-fixed-panel on top. The panel can + * intersect a main element and partially hides a content of + * the main element. To correctly calculates visibility of an + * element, the cursor must know how much height occuped by a fixed + * panel. + * The scrollTopMargin defines margin occuped by fixed panel. + */ + _scrollTopMargin: { + type: Number, + value: 0, + }, + }; + } - static get observers() { - return [ - '_getProjectConfig(_change.project)', - '_getFiles(_changeNum, _patchRange.*, _changeComments)', - '_setReviewedObserver(_loggedIn, params.*, _prefs)', - ]; - } + static get observers() { + return [ + '_getProjectConfig(_change.project)', + '_getFiles(_changeNum, _patchRange.*, _changeComments)', + '_setReviewedObserver(_loggedIn, params.*, _prefs)', + ]; + } - get keyBindings() { - return { - esc: '_handleEscKey', - }; - } + get keyBindings() { + return { + esc: '_handleEscKey', + }; + } - keyboardShortcuts() { - return { - [this.Shortcut.LEFT_PANE]: '_handleLeftPane', - [this.Shortcut.RIGHT_PANE]: '_handleRightPane', - [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments', - [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments', - [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine', - [this.Shortcut.NEXT_FILE_WITH_COMMENTS]: - '_handleNextLineOrFileWithComments', - [this.Shortcut.PREV_FILE_WITH_COMMENTS]: - '_handlePrevLineOrFileWithComments', - [this.Shortcut.NEW_COMMENT]: '_handleNewComment', - [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding - [this.Shortcut.NEXT_FILE]: '_handleNextFile', - [this.Shortcut.PREV_FILE]: '_handlePrevFile', - [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread', - [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread', - [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread', - [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread', - [this.Shortcut.OPEN_REPLY_DIALOG]: - '_handleOpenReplyDialogOrToggleLeftPane', - [this.Shortcut.TOGGLE_LEFT_PANE]: - '_handleOpenReplyDialogOrToggleLeftPane', - [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange', - [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey', - [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode', - [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed', - [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext', - [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile', - [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame', + keyboardShortcuts() { + return { + [this.Shortcut.LEFT_PANE]: '_handleLeftPane', + [this.Shortcut.RIGHT_PANE]: '_handleRightPane', + [this.Shortcut.NEXT_LINE]: '_handleNextLineOrFileWithComments', + [this.Shortcut.PREV_LINE]: '_handlePrevLineOrFileWithComments', + [this.Shortcut.VISIBLE_LINE]: '_handleVisibleLine', + [this.Shortcut.NEXT_FILE_WITH_COMMENTS]: + '_handleNextLineOrFileWithComments', + [this.Shortcut.PREV_FILE_WITH_COMMENTS]: + '_handlePrevLineOrFileWithComments', + [this.Shortcut.NEW_COMMENT]: '_handleNewComment', + [this.Shortcut.SAVE_COMMENT]: null, // DOC_ONLY binding + [this.Shortcut.NEXT_FILE]: '_handleNextFile', + [this.Shortcut.PREV_FILE]: '_handlePrevFile', + [this.Shortcut.NEXT_CHUNK]: '_handleNextChunkOrCommentThread', + [this.Shortcut.NEXT_COMMENT_THREAD]: '_handleNextChunkOrCommentThread', + [this.Shortcut.PREV_CHUNK]: '_handlePrevChunkOrCommentThread', + [this.Shortcut.PREV_COMMENT_THREAD]: '_handlePrevChunkOrCommentThread', + [this.Shortcut.OPEN_REPLY_DIALOG]: + '_handleOpenReplyDialogOrToggleLeftPane', + [this.Shortcut.TOGGLE_LEFT_PANE]: + '_handleOpenReplyDialogOrToggleLeftPane', + [this.Shortcut.UP_TO_CHANGE]: '_handleUpToChange', + [this.Shortcut.OPEN_DIFF_PREFS]: '_handleCommaKey', + [this.Shortcut.TOGGLE_DIFF_MODE]: '_handleToggleDiffMode', + [this.Shortcut.TOGGLE_FILE_REVIEWED]: '_handleToggleFileReviewed', + [this.Shortcut.EXPAND_ALL_DIFF_CONTEXT]: '_handleExpandAllDiffContext', + [this.Shortcut.NEXT_UNREVIEWED_FILE]: '_handleNextUnreviewedFile', + [this.Shortcut.TOGGLE_BLAME]: '_handleToggleBlame', - // Final two are actually handled by gr-comment-thread. - [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null, - [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null, - }; - } + // Final two are actually handled by gr-comment-thread. + [this.Shortcut.EXPAND_ALL_COMMENT_THREADS]: null, + [this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS]: null, + }; + } - /** @override */ - attached() { - super.attached(); - this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - }); + /** @override */ + attached() { + super.attached(); + this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + }); - this.addEventListener('open-fix-preview', - this._onOpenFixPreview.bind(this)); - this.$.cursor.push('diffs', this.$.diffHost); - } + this.addEventListener('open-fix-preview', + this._onOpenFixPreview.bind(this)); + this.$.cursor.push('diffs', this.$.diffHost); + } - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } - _getProjectConfig(project) { - return this.$.restAPI.getProjectConfig(project).then( - config => { - this._projectConfig = config; - }); - } - - _getChangeDetail(changeNum) { - return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { - this._change = change; - return change; - }); - } - - _getChangeEdit(changeNum) { - return this.$.restAPI.getChangeEdit(this._changeNum); - } - - _getSortedFileList(files) { - return files.sortedFileList; - } - - _getFiles(changeNum, patchRangeRecord, changeComments) { - // Polymer 2: check for undefined - if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments] - .some(arg => arg === undefined)) { - return Promise.resolve(); - } - - const patchRange = patchRangeRecord.base; - return this.$.restAPI.getChangeFiles( - changeNum, patchRange).then(changeFiles => { - if (!changeFiles) return; - const commentedPaths = changeComments.getPaths(patchRange); - const files = Object.assign({}, changeFiles); - Object.keys(commentedPaths).forEach(commentedPath => { - if (files.hasOwnProperty(commentedPath)) { return; } - files[commentedPath] = {status: 'U'}; + _getProjectConfig(project) { + return this.$.restAPI.getProjectConfig(project).then( + config => { + this._projectConfig = config; }); - this._files = { - sortedFileList: Object.keys(files).sort(this.specialFilePathCompare), - changeFilesByPath: files, - }; + } + + _getChangeDetail(changeNum) { + return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { + this._change = change; + return change; + }); + } + + _getChangeEdit(changeNum) { + return this.$.restAPI.getChangeEdit(this._changeNum); + } + + _getSortedFileList(files) { + return files.sortedFileList; + } + + _getFiles(changeNum, patchRangeRecord, changeComments) { + // Polymer 2: check for undefined + if ([changeNum, patchRangeRecord, patchRangeRecord.base, changeComments] + .some(arg => arg === undefined)) { + return Promise.resolve(); + } + + const patchRange = patchRangeRecord.base; + return this.$.restAPI.getChangeFiles( + changeNum, patchRange).then(changeFiles => { + if (!changeFiles) return; + const commentedPaths = changeComments.getPaths(patchRange); + const files = Object.assign({}, changeFiles); + Object.keys(commentedPaths).forEach(commentedPath => { + if (files.hasOwnProperty(commentedPath)) { return; } + files[commentedPath] = {status: 'U'}; }); + this._files = { + sortedFileList: Object.keys(files).sort(this.specialFilePathCompare), + changeFilesByPath: files, + }; + }); + } + + _getDiffPreferences() { + return this.$.restAPI.getDiffPreferences().then(prefs => { + this._prefs = prefs; + }); + } + + _getPreferences() { + return this.$.restAPI.getPreferences(); + } + + _getWindowWidth() { + return window.innerWidth; + } + + _handleReviewedChange(e) { + this._setReviewed(dom(e).rootTarget.checked); + } + + _setReviewed(reviewed) { + if (this._editMode) { return; } + this.$.reviewed.checked = reviewed; + this._saveReviewedState(reviewed).catch(err => { + this.fire('show-alert', {message: ERR_REVIEW_STATUS}); + throw err; + }); + } + + _saveReviewedState(reviewed) { + return this.$.restAPI.saveFileReviewed(this._changeNum, + this._patchRange.patchNum, this._path, reviewed); + } + + _handleToggleFileReviewed(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._setReviewed(!this.$.reviewed.checked); + } + + _handleEscKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this.$.diffHost.displayLine = false; + } + + _handleLeftPane(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this.$.cursor.moveLeft(); + } + + _handleRightPane(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this.$.cursor.moveRight(); + } + + _handlePrevLineOrFileWithComments(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (e.detail.keyboardEvent.shiftKey && + e.detail.keyboardEvent.keyCode === 75) { // 'K' + this._moveToPreviousFileWithComment(); + return; } + if (this.modifierPressed(e)) { return; } - _getDiffPreferences() { - return this.$.restAPI.getDiffPreferences().then(prefs => { - this._prefs = prefs; - }); + e.preventDefault(); + this.$.diffHost.displayLine = true; + this.$.cursor.moveUp(); + } + + _handleVisibleLine(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + this.$.cursor.moveToVisibleArea(); + } + + _onOpenFixPreview(e) { + this.$.applyFixDialog.open(e); + } + + _handleNextLineOrFileWithComments(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + if (e.detail.keyboardEvent.shiftKey && + e.detail.keyboardEvent.keyCode === 74) { // 'J' + this._moveToNextFileWithComment(); + return; } + if (this.modifierPressed(e)) { return; } - _getPreferences() { - return this.$.restAPI.getPreferences(); - } + e.preventDefault(); + this.$.diffHost.displayLine = true; + this.$.cursor.moveDown(); + } - _getWindowWidth() { - return window.innerWidth; - } + _moveToPreviousFileWithComment() { + if (!this._commentSkips) { return; } - _handleReviewedChange(e) { - this._setReviewed(Polymer.dom(e).rootTarget.checked); - } - - _setReviewed(reviewed) { - if (this._editMode) { return; } - this.$.reviewed.checked = reviewed; - this._saveReviewedState(reviewed).catch(err => { - this.fire('show-alert', {message: ERR_REVIEW_STATUS}); - throw err; - }); - } - - _saveReviewedState(reviewed) { - return this.$.restAPI.saveFileReviewed(this._changeNum, - this._patchRange.patchNum, this._path, reviewed); - } - - _handleToggleFileReviewed(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this._setReviewed(!this.$.reviewed.checked); - } - - _handleEscKey(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.diffHost.displayLine = false; - } - - _handleLeftPane(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - this.$.cursor.moveLeft(); - } - - _handleRightPane(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - this.$.cursor.moveRight(); - } - - _handlePrevLineOrFileWithComments(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - if (e.detail.keyboardEvent.shiftKey && - e.detail.keyboardEvent.keyCode === 75) { // 'K' - this._moveToPreviousFileWithComment(); - return; - } - if (this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.diffHost.displayLine = true; - this.$.cursor.moveUp(); - } - - _handleVisibleLine(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - this.$.cursor.moveToVisibleArea(); - } - - _onOpenFixPreview(e) { - this.$.applyFixDialog.open(e); - } - - _handleNextLineOrFileWithComments(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - if (e.detail.keyboardEvent.shiftKey && - e.detail.keyboardEvent.keyCode === 74) { // 'J' - this._moveToNextFileWithComment(); - return; - } - if (this.modifierPressed(e)) { return; } - - e.preventDefault(); - this.$.diffHost.displayLine = true; - this.$.cursor.moveDown(); - } - - _moveToPreviousFileWithComment() { - if (!this._commentSkips) { return; } - - // If there is no previous diff with comments, then return to the change - // view. - if (!this._commentSkips.previous) { - this._navToChangeView(); - return; - } - - Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous, - this._patchRange.patchNum, this._patchRange.basePatchNum); - } - - _moveToNextFileWithComment() { - if (!this._commentSkips) { return; } - - // If there is no next diff with comments, then return to the change view. - if (!this._commentSkips.next) { - this._navToChangeView(); - return; - } - - Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next, - this._patchRange.patchNum, this._patchRange.basePatchNum); - } - - _handleNewComment(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - e.preventDefault(); - this.$.cursor.createCommentInPlace(); - } - - _handlePrevFile(e) { - // Check for meta key to avoid overriding native chrome shortcut. - if (this.shouldSuppressKeyboardShortcut(e) || - this.getKeyboardEvent(e).metaKey) { return; } - - e.preventDefault(); - this._navToFile(this._path, this._fileList, -1); - } - - _handleNextFile(e) { - // Check for meta key to avoid overriding native chrome shortcut. - if (this.shouldSuppressKeyboardShortcut(e) || - this.getKeyboardEvent(e).metaKey) { return; } - - e.preventDefault(); - this._navToFile(this._path, this._fileList, 1); - } - - _handleNextChunkOrCommentThread(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - if (e.detail.keyboardEvent.shiftKey) { - this.$.cursor.moveToNextCommentThread(); - } else { - if (this.modifierPressed(e)) { return; } - this.$.cursor.moveToNextChunk(); - } - } - - _handlePrevChunkOrCommentThread(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - e.preventDefault(); - if (e.detail.keyboardEvent.shiftKey) { - this.$.cursor.moveToPreviousCommentThread(); - } else { - if (this.modifierPressed(e)) { return; } - this.$.cursor.moveToPreviousChunk(); - } - } - - _handleOpenReplyDialogOrToggleLeftPane(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - if (e.detail.keyboardEvent.shiftKey) { // Hide left diff. - e.preventDefault(); - this.$.diffHost.toggleLeftDiff(); - return; - } - - if (this.modifierPressed(e)) { return; } - - if (!this._loggedIn) { return; } - - this.set('changeViewState.showReplyDialog', true); - e.preventDefault(); + // If there is no previous diff with comments, then return to the change + // view. + if (!this._commentSkips.previous) { this._navToChangeView(); + return; } - _handleUpToChange(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } + Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.previous, + this._patchRange.patchNum, this._patchRange.basePatchNum); + } - e.preventDefault(); + _moveToNextFileWithComment() { + if (!this._commentSkips) { return; } + + // If there is no next diff with comments, then return to the change view. + if (!this._commentSkips.next) { this._navToChangeView(); + return; } - _handleCommaKey(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - if (this._diffPrefsDisabled) { return; } + Gerrit.Nav.navigateToDiff(this._change, this._commentSkips.next, + this._patchRange.patchNum, this._patchRange.basePatchNum); + } + _handleNewComment(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + e.preventDefault(); + this.$.cursor.createCommentInPlace(); + } + + _handlePrevFile(e) { + // Check for meta key to avoid overriding native chrome shortcut. + if (this.shouldSuppressKeyboardShortcut(e) || + this.getKeyboardEvent(e).metaKey) { return; } + + e.preventDefault(); + this._navToFile(this._path, this._fileList, -1); + } + + _handleNextFile(e) { + // Check for meta key to avoid overriding native chrome shortcut. + if (this.shouldSuppressKeyboardShortcut(e) || + this.getKeyboardEvent(e).metaKey) { return; } + + e.preventDefault(); + this._navToFile(this._path, this._fileList, 1); + } + + _handleNextChunkOrCommentThread(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + if (e.detail.keyboardEvent.shiftKey) { + this.$.cursor.moveToNextCommentThread(); + } else { + if (this.modifierPressed(e)) { return; } + this.$.cursor.moveToNextChunk(); + } + } + + _handlePrevChunkOrCommentThread(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + e.preventDefault(); + if (e.detail.keyboardEvent.shiftKey) { + this.$.cursor.moveToPreviousCommentThread(); + } else { + if (this.modifierPressed(e)) { return; } + this.$.cursor.moveToPreviousChunk(); + } + } + + _handleOpenReplyDialogOrToggleLeftPane(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + if (e.detail.keyboardEvent.shiftKey) { // Hide left diff. e.preventDefault(); - this.$.diffPreferencesDialog.open(); + this.$.diffHost.toggleLeftDiff(); + return; } - _handleToggleDiffMode(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } + if (this.modifierPressed(e)) { return; } - e.preventDefault(); - if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) { - this.$.modeSelect.setMode(DiffViewMode.UNIFIED); - } else { - this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE); - } + if (!this._loggedIn) { return; } + + this.set('changeViewState.showReplyDialog', true); + e.preventDefault(); + this._navToChangeView(); + } + + _handleUpToChange(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + this._navToChangeView(); + } + + _handleCommaKey(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + if (this._diffPrefsDisabled) { return; } + + e.preventDefault(); + this.$.diffPreferencesDialog.open(); + } + + _handleToggleDiffMode(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + + e.preventDefault(); + if (this._getDiffViewMode() === DiffViewMode.SIDE_BY_SIDE) { + this.$.modeSelect.setMode(DiffViewMode.UNIFIED); + } else { + this.$.modeSelect.setMode(DiffViewMode.SIDE_BY_SIDE); } + } - _navToChangeView() { - if (!this._changeNum || !this._patchRange.patchNum) { return; } + _navToChangeView() { + if (!this._changeNum || !this._patchRange.patchNum) { return; } + this._navigateToChange( + this._change, + this._patchRange, + this._change && this._change.revisions); + } + + _navToFile(path, fileList, direction) { + const newPath = this._getNavLinkPath(path, fileList, direction); + if (!newPath) { return; } + + if (newPath.up) { this._navigateToChange( this._change, this._patchRange, this._change && this._change.revisions); + return; } - _navToFile(path, fileList, direction) { - const newPath = this._getNavLinkPath(path, fileList, direction); - if (!newPath) { return; } + Gerrit.Nav.navigateToDiff(this._change, newPath.path, + this._patchRange.patchNum, this._patchRange.basePatchNum); + } - if (newPath.up) { - this._navigateToChange( - this._change, - this._patchRange, - this._change && this._change.revisions); - return; - } + /** + * @param {?string} path The path of the current file being shown. + * @param {!Array<string>} fileList The list of files in this change and + * patch range. + * @param {number} direction Either 1 (next file) or -1 (prev file). + * @param {(number|boolean)} opt_noUp Whether to return to the change view + * when advancing the file goes outside the bounds of fileList. + * + * @return {?string} The next URL when proceeding in the specified + * direction. + */ + _computeNavLinkURL(change, path, fileList, direction, opt_noUp) { + const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp); + if (!newPath) { return null; } - Gerrit.Nav.navigateToDiff(this._change, newPath.path, - this._patchRange.patchNum, this._patchRange.basePatchNum); + if (newPath.up) { + return this._getChangePath( + this._change, + this._patchRange, + this._change && this._change.revisions); + } + return this._getDiffUrl(this._change, this._patchRange, newPath.path); + } + + _goToEditFile() { + // TODO(taoalpha): add a shortcut for editing + const editUrl = Gerrit.Nav.getEditUrlForDiff( + this._change, this._path, this._patchRange.patchNum); + return Gerrit.Nav.navigateToRelativeUrl(editUrl); + } + + /** + * Gives an object representing the target of navigating either left or + * right through the change. The resulting object will have one of the + * following forms: + * * {path: "<target file path>"} - When another file path should be the + * result of the navigation. + * * {up: true} - When the result of navigating should go back to the + * change view. + * * null - When no navigation is possible for the given direction. + * + * @param {?string} path The path of the current file being shown. + * @param {!Array<string>} fileList The list of files in this change and + * patch range. + * @param {number} direction Either 1 (next file) or -1 (prev file). + * @param {?number|boolean=} opt_noUp Whether to return to the change view + * when advancing the file goes outside the bounds of fileList. + * @return {?Object} + */ + _getNavLinkPath(path, fileList, direction, opt_noUp) { + if (!path || !fileList || fileList.length === 0) { return null; } + + let idx = fileList.indexOf(path); + if (idx === -1) { + const file = direction > 0 ? + fileList[0] : + fileList[fileList.length - 1]; + return {path: file}; } - /** - * @param {?string} path The path of the current file being shown. - * @param {!Array<string>} fileList The list of files in this change and - * patch range. - * @param {number} direction Either 1 (next file) or -1 (prev file). - * @param {(number|boolean)} opt_noUp Whether to return to the change view - * when advancing the file goes outside the bounds of fileList. - * - * @return {?string} The next URL when proceeding in the specified - * direction. - */ - _computeNavLinkURL(change, path, fileList, direction, opt_noUp) { - const newPath = this._getNavLinkPath(path, fileList, direction, opt_noUp); - if (!newPath) { return null; } - - if (newPath.up) { - return this._getChangePath( - this._change, - this._patchRange, - this._change && this._change.revisions); - } - return this._getDiffUrl(this._change, this._patchRange, newPath.path); + idx += direction; + // Redirect to the change view if opt_noUp isn’t truthy and idx falls + // outside the bounds of [0, fileList.length). + if (idx < 0 || idx > fileList.length - 1) { + if (opt_noUp) { return null; } + return {up: true}; } - _goToEditFile() { - // TODO(taoalpha): add a shortcut for editing - const editUrl = Gerrit.Nav.getEditUrlForDiff( - this._change, this._path, this._patchRange.patchNum); - return Gerrit.Nav.navigateToRelativeUrl(editUrl); + return {path: fileList[idx]}; + } + + _getReviewedFiles(changeNum, patchNum) { + return this.$.restAPI.getReviewedFiles(changeNum, patchNum) + .then(files => { + this._reviewedFiles = new Set(files); + return this._reviewedFiles; + }); + } + + _getReviewedStatus(editMode, changeNum, patchNum, path) { + if (editMode) { return Promise.resolve(false); } + return this._getReviewedFiles(changeNum, patchNum) + .then(files => files.has(path)); + } + + _paramsChanged(value) { + if (value.view !== Gerrit.Nav.View.DIFF) { return; } + + if (value.changeNum && value.project) { + this.$.restAPI.setInProjectLookup(value.changeNum, value.project); } - /** - * Gives an object representing the target of navigating either left or - * right through the change. The resulting object will have one of the - * following forms: - * * {path: "<target file path>"} - When another file path should be the - * result of the navigation. - * * {up: true} - When the result of navigating should go back to the - * change view. - * * null - When no navigation is possible for the given direction. - * - * @param {?string} path The path of the current file being shown. - * @param {!Array<string>} fileList The list of files in this change and - * patch range. - * @param {number} direction Either 1 (next file) or -1 (prev file). - * @param {?number|boolean=} opt_noUp Whether to return to the change view - * when advancing the file goes outside the bounds of fileList. - * @return {?Object} - */ - _getNavLinkPath(path, fileList, direction, opt_noUp) { - if (!path || !fileList || fileList.length === 0) { return null; } + this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params); + this._initCursor(this.params); - let idx = fileList.indexOf(path); - if (idx === -1) { - const file = direction > 0 ? - fileList[0] : - fileList[fileList.length - 1]; - return {path: file}; - } + this._changeNum = value.changeNum; + this._path = value.path; + this._patchRange = { + patchNum: value.patchNum, + basePatchNum: value.basePatchNum || PARENT, + }; - idx += direction; - // Redirect to the change view if opt_noUp isn’t truthy and idx falls - // outside the bounds of [0, fileList.length). - if (idx < 0 || idx > fileList.length - 1) { - if (opt_noUp) { return null; } - return {up: true}; - } + // NOTE: This may be called before attachment (e.g. while parentElement is + // null). Fire title-change in an async so that, if attachment to the DOM + // has been queued, the event can bubble up to the handler in gr-app. + this.async(() => { + this.fire('title-change', + {title: this.computeTruncatedPath(this._path)}); + }); - return {path: fileList[idx]}; + // When navigating away from the page, there is a possibility that the + // patch number is no longer a part of the URL (say when navigating to + // the top-level change info view) and therefore undefined in `params`. + if (!this._patchRange.patchNum) { + return; } - _getReviewedFiles(changeNum, patchNum) { - return this.$.restAPI.getReviewedFiles(changeNum, patchNum) - .then(files => { - this._reviewedFiles = new Set(files); - return this._reviewedFiles; - }); - } + const promises = []; - _getReviewedStatus(editMode, changeNum, patchNum, path) { - if (editMode) { return Promise.resolve(false); } - return this._getReviewedFiles(changeNum, patchNum) - .then(files => files.has(path)); - } + promises.push(this._getDiffPreferences()); - _paramsChanged(value) { - if (value.view !== Gerrit.Nav.View.DIFF) { return; } + promises.push(this._getPreferences().then(prefs => { + this._userPrefs = prefs; + })); - if (value.changeNum && value.project) { - this.$.restAPI.setInProjectLookup(value.changeNum, value.project); - } - - this.$.diffHost.lineOfInterest = this._getLineOfInterest(this.params); - this._initCursor(this.params); - - this._changeNum = value.changeNum; - this._path = value.path; - this._patchRange = { - patchNum: value.patchNum, - basePatchNum: value.basePatchNum || PARENT, - }; - - // NOTE: This may be called before attachment (e.g. while parentElement is - // null). Fire title-change in an async so that, if attachment to the DOM - // has been queued, the event can bubble up to the handler in gr-app. - this.async(() => { - this.fire('title-change', - {title: this.computeTruncatedPath(this._path)}); - }); - - // When navigating away from the page, there is a possibility that the - // patch number is no longer a part of the URL (say when navigating to - // the top-level change info view) and therefore undefined in `params`. - if (!this._patchRange.patchNum) { - return; - } - - const promises = []; - - promises.push(this._getDiffPreferences()); - - promises.push(this._getPreferences().then(prefs => { - this._userPrefs = prefs; - })); - - promises.push(this._getChangeDetail(this._changeNum).then(change => { - let commit; - let baseCommit; - if (change) { - for (const commitSha in change.revisions) { - if (!change.revisions.hasOwnProperty(commitSha)) continue; - const revision = change.revisions[commitSha]; - const patchNum = revision._number.toString(); - if (patchNum === this._patchRange.patchNum) { - commit = commitSha; - const commitObj = revision.commit || {}; - const parents = commitObj.parents || []; - if (this._patchRange.basePatchNum === PARENT && parents.length) { - baseCommit = parents[parents.length - 1].commit; - } - } else if (patchNum === this._patchRange.basePatchNum) { - baseCommit = commitSha; + promises.push(this._getChangeDetail(this._changeNum).then(change => { + let commit; + let baseCommit; + if (change) { + for (const commitSha in change.revisions) { + if (!change.revisions.hasOwnProperty(commitSha)) continue; + const revision = change.revisions[commitSha]; + const patchNum = revision._number.toString(); + if (patchNum === this._patchRange.patchNum) { + commit = commitSha; + const commitObj = revision.commit || {}; + const parents = commitObj.parents || []; + if (this._patchRange.basePatchNum === PARENT && parents.length) { + baseCommit = parents[parents.length - 1].commit; } + } else if (patchNum === this._patchRange.basePatchNum) { + baseCommit = commitSha; } - this._commitRange = {commit, baseCommit}; } - })); + this._commitRange = {commit, baseCommit}; + } + })); - promises.push(this._loadComments()); + promises.push(this._loadComments()); - promises.push(this._getChangeEdit(this._changeNum)); + promises.push(this._getChangeEdit(this._changeNum)); - this._loading = true; - return Promise.all(promises) - .then(r => { - const edit = r[4]; - if (edit) { - this.set('_change.revisions.' + edit.commit.commit, { - _number: this.EDIT_NAME, - basePatchNum: edit.base_patch_set_number, - commit: edit.commit, - }); - } - this._loading = false; - this.$.diffHost.comments = this._commentsForDiff; - return this.$.diffHost.reload(true); - }) - .then(() => { - this.$.reporting.diffViewFullyLoaded(); - // If diff view displayed has not ended yet, it ends here. - this.$.reporting.diffViewDisplayed(); - }); - } - - _changeViewStateChanged(changeViewState) { - if (changeViewState.diffMode === null) { - // If screen size is small, always default to unified view. - this.$.restAPI.getPreferences().then(prefs => { - this.set('changeViewState.diffMode', prefs.default_diff_view); + this._loading = true; + return Promise.all(promises) + .then(r => { + const edit = r[4]; + if (edit) { + this.set('_change.revisions.' + edit.commit.commit, { + _number: this.EDIT_NAME, + basePatchNum: edit.base_patch_set_number, + commit: edit.commit, + }); + } + this._loading = false; + this.$.diffHost.comments = this._commentsForDiff; + return this.$.diffHost.reload(true); + }) + .then(() => { + this.$.reporting.diffViewFullyLoaded(); + // If diff view displayed has not ended yet, it ends here. + this.$.reporting.diffViewDisplayed(); }); - } - } + } - _setReviewedObserver(_loggedIn, paramsRecord, _prefs) { - // Polymer 2: check for undefined - if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) { - return; - } - - const params = paramsRecord.base || {}; - if (!_loggedIn) { return; } - - if (_prefs.manual_review) { - // Checkbox state needs to be set explicitly only when manual_review - // is specified. - this._getReviewedStatus(this.editMode, this._changeNum, - this._patchRange.patchNum, this._path).then(status => { - this.$.reviewed.checked = status; - }); - return; - } - - if (params.view === Gerrit.Nav.View.DIFF) { - this._setReviewed(true); - } - } - - /** - * If the params specify a diff address then configure the diff cursor. - */ - _initCursor(params) { - if (params.lineNum === undefined) { return; } - if (params.leftSide) { - this.$.cursor.side = DiffSides.LEFT; - } else { - this.$.cursor.side = DiffSides.RIGHT; - } - this.$.cursor.initialLineNumber = params.lineNum; - } - - _getLineOfInterest(params) { - // If there is a line number specified, pass it along to the diff so that - // it will not get collapsed. - if (!params.lineNum) { return null; } - return {number: params.lineNum, leftSide: params.leftSide}; - } - - _pathChanged(path) { - if (path) { - this.fire('title-change', - {title: this.computeTruncatedPath(path)}); - } - - if (this._fileList.length == 0) { return; } - - this.set('changeViewState.selectedFileIndex', - this._fileList.indexOf(path)); - } - - _getDiffUrl(change, patchRange, path) { - if ([change, patchRange, path].some(arg => arg === undefined)) { - return ''; - } - return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum, - patchRange.basePatchNum); - } - - _patchRangeStr(patchRange) { - let patchStr = patchRange.patchNum; - if (patchRange.basePatchNum != null && - patchRange.basePatchNum != PARENT) { - patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; - } - return patchStr; - } - - /** - * When the latest patch of the change is selected (and there is no base - * patch) then the patch range need not appear in the URL. Return a patch - * range object with undefined values when a range is not needed. - * - * @param {!Object} patchRange - * @param {!Object} revisions - * @return {!Object} - */ - _getChangeUrlRange(patchRange, revisions) { - let patchNum = undefined; - let basePatchNum = undefined; - let latestPatchNum = -1; - for (const rev of Object.values(revisions || {})) { - latestPatchNum = Math.max(latestPatchNum, rev._number); - } - if (patchRange.basePatchNum !== PARENT || - parseInt(patchRange.patchNum, 10) !== latestPatchNum) { - patchNum = patchRange.patchNum; - basePatchNum = patchRange.basePatchNum; - } - return {patchNum, basePatchNum}; - } - - _getChangePath(change, patchRange, revisions) { - if ([change, patchRange].some(arg => arg === undefined)) { - return ''; - } - const range = this._getChangeUrlRange(patchRange, revisions); - return Gerrit.Nav.getUrlForChange(change, range.patchNum, - range.basePatchNum); - } - - _navigateToChange(change, patchRange, revisions) { - const range = this._getChangeUrlRange(patchRange, revisions); - Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum); - } - - _computeChangePath(change, patchRangeRecord, revisions) { - return this._getChangePath(change, patchRangeRecord.base, revisions); - } - - _formatFilesForDropdown(files, patchNum, changeComments) { - // Polymer 2: check for undefined - if ([ - files, - patchNum, - changeComments, - ].some(arg => arg === undefined)) { - return; - } - - if (!files) { return; } - const dropdownContent = []; - for (const path of files.sortedFileList) { - dropdownContent.push({ - text: this.computeDisplayPath(path), - mobileText: this.computeTruncatedPath(path), - value: path, - bottomText: this._computeCommentString(changeComments, patchNum, - path, files.changeFilesByPath[path]), - }); - } - return dropdownContent; - } - - _computeCommentString(changeComments, patchNum, path, changeFileInfo) { - const unresolvedCount = changeComments.computeUnresolvedNum(patchNum, - path); - const commentCount = changeComments.computeCommentCount(patchNum, path); - const commentString = GrCountStringFormatter.computePluralString( - commentCount, 'comment'); - const unresolvedString = GrCountStringFormatter.computeString( - unresolvedCount, 'unresolved'); - - const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': ''; - - return [ - unmodifiedString, - commentString, - unresolvedString] - .filter(v => v && v.length > 0).join(', '); - } - - _computePrefsButtonHidden(prefs, prefsDisabled) { - return prefsDisabled || !prefs; - } - - _handleFileChange(e) { - // This is when it gets set initially. - const path = e.detail.value; - if (path === this._path) { - return; - } - - Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum, - this._patchRange.basePatchNum); - } - - _handleFileTap(e) { - // async is needed so that that the click event is fired before the - // dropdown closes (This was a bug for touch devices). - this.async(() => { - this.$.dropdown.close(); - }, 1); - } - - _handlePatchChange(e) { - const {basePatchNum, patchNum} = e.detail; - if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) && - this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; } - Gerrit.Nav.navigateToDiff( - this._change, this._path, patchNum, basePatchNum); - } - - _handlePrefsTap(e) { - e.preventDefault(); - this.$.diffPreferencesDialog.open(); - } - - /** - * _getDiffViewMode: Get the diff view (side-by-side or unified) based on - * the current state. - * - * The expected behavior is to use the mode specified in the user's - * preferences unless they have manually chosen the alternative view or they - * are on a mobile device. If the user navigates up to the change view, it - * should clear this choice and revert to the preference the next time a - * diff is viewed. - * - * Use side-by-side if the user is not logged in. - * - * @return {string} - */ - _getDiffViewMode() { - if (this.changeViewState.diffMode) { - return this.changeViewState.diffMode; - } else if (this._userPrefs) { - this.set('changeViewState.diffMode', this._userPrefs.default_diff_view); - return this._userPrefs.default_diff_view; - } else { - return 'SIDE_BY_SIDE'; - } - } - - _computeModeSelectHideClass(isImageDiff) { - return isImageDiff ? 'hide' : ''; - } - - _onLineSelected(e, detail) { - this.$.cursor.moveToLineNumber(detail.number, detail.side); - if (!this._change) { return; } - const cursorAddress = this.$.cursor.getAddress(); - const number = cursorAddress ? cursorAddress.number : undefined; - const leftSide = cursorAddress ? cursorAddress.leftSide : undefined; - const url = Gerrit.Nav.getUrlForDiffById(this._changeNum, - this._change.project, this._path, this._patchRange.patchNum, - this._patchRange.basePatchNum, number, leftSide); - history.replaceState(null, '', url); - } - - _computeDownloadDropdownLinks( - project, changeNum, patchRange, path, diff) { - if (!patchRange || !patchRange.patchNum) { return []; } - - const links = [ - { - url: this._computeDownloadPatchLink( - project, changeNum, patchRange, path), - name: 'Patch', - }, - ]; - - if (diff && diff.meta_a) { - let leftPath = path; - if (diff.change_type === 'RENAMED') { - leftPath = diff.meta_a.name; - } - links.push( - { - url: this._computeDownloadFileLink( - project, changeNum, patchRange, leftPath, true), - name: 'Left Content', - } - ); - } - - if (diff && diff.meta_b) { - links.push( - { - url: this._computeDownloadFileLink( - project, changeNum, patchRange, path, false), - name: 'Right Content', - } - ); - } - - return links; - } - - _computeDownloadFileLink( - project, changeNum, patchRange, path, isBase) { - let patchNum = patchRange.patchNum; - - const comparedAgainsParent = patchRange.basePatchNum === 'PARENT'; - - if (isBase && !comparedAgainsParent) { - patchNum = patchRange.basePatchNum; - } - - let url = this.changeBaseURL(project, changeNum, patchNum) + - `/files/${encodeURIComponent(path)}/download`; - - if (isBase && comparedAgainsParent) { - url += '?parent=1'; - } - - return url; - } - - _computeDownloadPatchLink(project, changeNum, patchRange, path) { - let url = this.changeBaseURL(project, changeNum, patchRange.patchNum); - url += '/patch?zip&path=' + encodeURIComponent(path); - return url; - } - - _loadComments() { - return this.$.commentAPI.loadAll(this._changeNum).then(comments => { - this._changeComments = comments; - this._commentMap = this._getPaths(this._patchRange); - - this._commentsForDiff = this._getCommentsForPath(this._path, - this._patchRange, this._projectConfig); + _changeViewStateChanged(changeViewState) { + if (changeViewState.diffMode === null) { + // If screen size is small, always default to unified view. + this.$.restAPI.getPreferences().then(prefs => { + this.set('changeViewState.diffMode', prefs.default_diff_view); }); } - - _getPaths(patchRange) { - return this._changeComments.getPaths(patchRange); - } - - _getCommentsForPath(path, patchRange, projectConfig) { - return this._changeComments.getCommentsBySideForPath(path, patchRange, - projectConfig); - } - - _getDiffDrafts() { - return this.$.restAPI.getDiffDrafts(this._changeNum); - } - - _computeCommentSkips(commentMap, fileList, path) { - // Polymer 2: check for undefined - if ([ - commentMap, - fileList, - path, - ].some(arg => arg === undefined)) { - return undefined; - } - - const skips = {previous: null, next: null}; - if (!fileList.length) { return skips; } - const pathIndex = fileList.indexOf(path); - - // Scan backward for the previous file. - for (let i = pathIndex - 1; i >= 0; i--) { - if (commentMap[fileList[i]]) { - skips.previous = fileList[i]; - break; - } - } - - // Scan forward for the next file. - for (let i = pathIndex + 1; i < fileList.length; i++) { - if (commentMap[fileList[i]]) { - skips.next = fileList[i]; - break; - } - } - - return skips; - } - - _computeDiffClass(panelFloatingDisabled) { - if (panelFloatingDisabled) { - return 'noOverflow'; - } - } - - /** - * @param {!Object} patchRangeRecord - */ - _computeEditMode(patchRangeRecord) { - const patchRange = patchRangeRecord.base || {}; - return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); - } - - /** - * @param {boolean} editMode - */ - _computeContainerClass(editMode) { - return editMode ? 'editMode' : ''; - } - - _computeBlameToggleLabel(loaded, loading) { - if (loaded) { return 'Hide blame'; } - return 'Show blame'; - } - - /** - * Load and display blame information if it has not already been loaded. - * Otherwise hide it. - */ - _toggleBlame() { - if (this._isBlameLoaded) { - this.$.diffHost.clearBlame(); - return; - } - - this._isBlameLoading = true; - this.fire('show-alert', {message: MSG_LOADING_BLAME}); - this.$.diffHost.loadBlame() - .then(() => { - this._isBlameLoading = false; - this.fire('show-alert', {message: MSG_LOADED_BLAME}); - }) - .catch(() => { - this._isBlameLoading = false; - }); - } - - _handleToggleBlame(e) { - if (this.shouldSuppressKeyboardShortcut(e) || - this.modifierPressed(e)) { return; } - this._toggleBlame(); - } - - _computeBlameLoaderClass(isImageDiff, path) { - return !this.isMagicPath(path) && !isImageDiff ? 'show' : ''; - } - - _getRevisionInfo(change) { - return new Gerrit.RevisionInfo(change); - } - - _computeFileNum(file, files) { - // Polymer 2: check for undefined - if ([file, files].some(arg => arg === undefined)) { - return undefined; - } - - return files.findIndex(({value}) => value === file) + 1; - } - - /** - * @param {number} fileNum - * @param {!Array<string>} files - * @return {string} - */ - _computeFileNumClass(fileNum, files) { - if (files && fileNum > 0) { - return 'show'; - } - return ''; - } - - _handleExpandAllDiffContext(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - this.$.diffHost.expandAllContext(); - } - - _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { - return disableDiffPrefs || !loggedIn; - } - - _handleNextUnreviewedFile(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - this._setReviewed(true); - // Ensure that the currently viewed file always appears in unreviewedFiles - // so we resolve the right "next" file. - const unreviewedFiles = this._fileList - .filter(file => - (file === this._path || !this._reviewedFiles.has(file))); - this._navToFile(this._path, unreviewedFiles, 1); - } - - _handleReloadingDiffPreference() { - this._getDiffPreferences(); - } - - _onChangeHeaderPanelHeightChanged(e) { - this._scrollTopMargin = e.detail.value; - } - - _computeIsLoggedIn(loggedIn) { - return loggedIn ? true : false; - } } - customElements.define(GrDiffView.is, GrDiffView); -})(); + _setReviewedObserver(_loggedIn, paramsRecord, _prefs) { + // Polymer 2: check for undefined + if ([_loggedIn, paramsRecord, _prefs].some(arg => arg === undefined)) { + return; + } + + const params = paramsRecord.base || {}; + if (!_loggedIn) { return; } + + if (_prefs.manual_review) { + // Checkbox state needs to be set explicitly only when manual_review + // is specified. + this._getReviewedStatus(this.editMode, this._changeNum, + this._patchRange.patchNum, this._path).then(status => { + this.$.reviewed.checked = status; + }); + return; + } + + if (params.view === Gerrit.Nav.View.DIFF) { + this._setReviewed(true); + } + } + + /** + * If the params specify a diff address then configure the diff cursor. + */ + _initCursor(params) { + if (params.lineNum === undefined) { return; } + if (params.leftSide) { + this.$.cursor.side = DiffSides.LEFT; + } else { + this.$.cursor.side = DiffSides.RIGHT; + } + this.$.cursor.initialLineNumber = params.lineNum; + } + + _getLineOfInterest(params) { + // If there is a line number specified, pass it along to the diff so that + // it will not get collapsed. + if (!params.lineNum) { return null; } + return {number: params.lineNum, leftSide: params.leftSide}; + } + + _pathChanged(path) { + if (path) { + this.fire('title-change', + {title: this.computeTruncatedPath(path)}); + } + + if (this._fileList.length == 0) { return; } + + this.set('changeViewState.selectedFileIndex', + this._fileList.indexOf(path)); + } + + _getDiffUrl(change, patchRange, path) { + if ([change, patchRange, path].some(arg => arg === undefined)) { + return ''; + } + return Gerrit.Nav.getUrlForDiff(change, path, patchRange.patchNum, + patchRange.basePatchNum); + } + + _patchRangeStr(patchRange) { + let patchStr = patchRange.patchNum; + if (patchRange.basePatchNum != null && + patchRange.basePatchNum != PARENT) { + patchStr = patchRange.basePatchNum + '..' + patchRange.patchNum; + } + return patchStr; + } + + /** + * When the latest patch of the change is selected (and there is no base + * patch) then the patch range need not appear in the URL. Return a patch + * range object with undefined values when a range is not needed. + * + * @param {!Object} patchRange + * @param {!Object} revisions + * @return {!Object} + */ + _getChangeUrlRange(patchRange, revisions) { + let patchNum = undefined; + let basePatchNum = undefined; + let latestPatchNum = -1; + for (const rev of Object.values(revisions || {})) { + latestPatchNum = Math.max(latestPatchNum, rev._number); + } + if (patchRange.basePatchNum !== PARENT || + parseInt(patchRange.patchNum, 10) !== latestPatchNum) { + patchNum = patchRange.patchNum; + basePatchNum = patchRange.basePatchNum; + } + return {patchNum, basePatchNum}; + } + + _getChangePath(change, patchRange, revisions) { + if ([change, patchRange].some(arg => arg === undefined)) { + return ''; + } + const range = this._getChangeUrlRange(patchRange, revisions); + return Gerrit.Nav.getUrlForChange(change, range.patchNum, + range.basePatchNum); + } + + _navigateToChange(change, patchRange, revisions) { + const range = this._getChangeUrlRange(patchRange, revisions); + Gerrit.Nav.navigateToChange(change, range.patchNum, range.basePatchNum); + } + + _computeChangePath(change, patchRangeRecord, revisions) { + return this._getChangePath(change, patchRangeRecord.base, revisions); + } + + _formatFilesForDropdown(files, patchNum, changeComments) { + // Polymer 2: check for undefined + if ([ + files, + patchNum, + changeComments, + ].some(arg => arg === undefined)) { + return; + } + + if (!files) { return; } + const dropdownContent = []; + for (const path of files.sortedFileList) { + dropdownContent.push({ + text: this.computeDisplayPath(path), + mobileText: this.computeTruncatedPath(path), + value: path, + bottomText: this._computeCommentString(changeComments, patchNum, + path, files.changeFilesByPath[path]), + }); + } + return dropdownContent; + } + + _computeCommentString(changeComments, patchNum, path, changeFileInfo) { + const unresolvedCount = changeComments.computeUnresolvedNum(patchNum, + path); + const commentCount = changeComments.computeCommentCount(patchNum, path); + const commentString = GrCountStringFormatter.computePluralString( + commentCount, 'comment'); + const unresolvedString = GrCountStringFormatter.computeString( + unresolvedCount, 'unresolved'); + + const unmodifiedString = changeFileInfo.status === 'U' ? 'no changes': ''; + + return [ + unmodifiedString, + commentString, + unresolvedString] + .filter(v => v && v.length > 0).join(', '); + } + + _computePrefsButtonHidden(prefs, prefsDisabled) { + return prefsDisabled || !prefs; + } + + _handleFileChange(e) { + // This is when it gets set initially. + const path = e.detail.value; + if (path === this._path) { + return; + } + + Gerrit.Nav.navigateToDiff(this._change, path, this._patchRange.patchNum, + this._patchRange.basePatchNum); + } + + _handleFileTap(e) { + // async is needed so that that the click event is fired before the + // dropdown closes (This was a bug for touch devices). + this.async(() => { + this.$.dropdown.close(); + }, 1); + } + + _handlePatchChange(e) { + const {basePatchNum, patchNum} = e.detail; + if (this.patchNumEquals(basePatchNum, this._patchRange.basePatchNum) && + this.patchNumEquals(patchNum, this._patchRange.patchNum)) { return; } + Gerrit.Nav.navigateToDiff( + this._change, this._path, patchNum, basePatchNum); + } + + _handlePrefsTap(e) { + e.preventDefault(); + this.$.diffPreferencesDialog.open(); + } + + /** + * _getDiffViewMode: Get the diff view (side-by-side or unified) based on + * the current state. + * + * The expected behavior is to use the mode specified in the user's + * preferences unless they have manually chosen the alternative view or they + * are on a mobile device. If the user navigates up to the change view, it + * should clear this choice and revert to the preference the next time a + * diff is viewed. + * + * Use side-by-side if the user is not logged in. + * + * @return {string} + */ + _getDiffViewMode() { + if (this.changeViewState.diffMode) { + return this.changeViewState.diffMode; + } else if (this._userPrefs) { + this.set('changeViewState.diffMode', this._userPrefs.default_diff_view); + return this._userPrefs.default_diff_view; + } else { + return 'SIDE_BY_SIDE'; + } + } + + _computeModeSelectHideClass(isImageDiff) { + return isImageDiff ? 'hide' : ''; + } + + _onLineSelected(e, detail) { + this.$.cursor.moveToLineNumber(detail.number, detail.side); + if (!this._change) { return; } + const cursorAddress = this.$.cursor.getAddress(); + const number = cursorAddress ? cursorAddress.number : undefined; + const leftSide = cursorAddress ? cursorAddress.leftSide : undefined; + const url = Gerrit.Nav.getUrlForDiffById(this._changeNum, + this._change.project, this._path, this._patchRange.patchNum, + this._patchRange.basePatchNum, number, leftSide); + history.replaceState(null, '', url); + } + + _computeDownloadDropdownLinks( + project, changeNum, patchRange, path, diff) { + if (!patchRange || !patchRange.patchNum) { return []; } + + const links = [ + { + url: this._computeDownloadPatchLink( + project, changeNum, patchRange, path), + name: 'Patch', + }, + ]; + + if (diff && diff.meta_a) { + let leftPath = path; + if (diff.change_type === 'RENAMED') { + leftPath = diff.meta_a.name; + } + links.push( + { + url: this._computeDownloadFileLink( + project, changeNum, patchRange, leftPath, true), + name: 'Left Content', + } + ); + } + + if (diff && diff.meta_b) { + links.push( + { + url: this._computeDownloadFileLink( + project, changeNum, patchRange, path, false), + name: 'Right Content', + } + ); + } + + return links; + } + + _computeDownloadFileLink( + project, changeNum, patchRange, path, isBase) { + let patchNum = patchRange.patchNum; + + const comparedAgainsParent = patchRange.basePatchNum === 'PARENT'; + + if (isBase && !comparedAgainsParent) { + patchNum = patchRange.basePatchNum; + } + + let url = this.changeBaseURL(project, changeNum, patchNum) + + `/files/${encodeURIComponent(path)}/download`; + + if (isBase && comparedAgainsParent) { + url += '?parent=1'; + } + + return url; + } + + _computeDownloadPatchLink(project, changeNum, patchRange, path) { + let url = this.changeBaseURL(project, changeNum, patchRange.patchNum); + url += '/patch?zip&path=' + encodeURIComponent(path); + return url; + } + + _loadComments() { + return this.$.commentAPI.loadAll(this._changeNum).then(comments => { + this._changeComments = comments; + this._commentMap = this._getPaths(this._patchRange); + + this._commentsForDiff = this._getCommentsForPath(this._path, + this._patchRange, this._projectConfig); + }); + } + + _getPaths(patchRange) { + return this._changeComments.getPaths(patchRange); + } + + _getCommentsForPath(path, patchRange, projectConfig) { + return this._changeComments.getCommentsBySideForPath(path, patchRange, + projectConfig); + } + + _getDiffDrafts() { + return this.$.restAPI.getDiffDrafts(this._changeNum); + } + + _computeCommentSkips(commentMap, fileList, path) { + // Polymer 2: check for undefined + if ([ + commentMap, + fileList, + path, + ].some(arg => arg === undefined)) { + return undefined; + } + + const skips = {previous: null, next: null}; + if (!fileList.length) { return skips; } + const pathIndex = fileList.indexOf(path); + + // Scan backward for the previous file. + for (let i = pathIndex - 1; i >= 0; i--) { + if (commentMap[fileList[i]]) { + skips.previous = fileList[i]; + break; + } + } + + // Scan forward for the next file. + for (let i = pathIndex + 1; i < fileList.length; i++) { + if (commentMap[fileList[i]]) { + skips.next = fileList[i]; + break; + } + } + + return skips; + } + + _computeDiffClass(panelFloatingDisabled) { + if (panelFloatingDisabled) { + return 'noOverflow'; + } + } + + /** + * @param {!Object} patchRangeRecord + */ + _computeEditMode(patchRangeRecord) { + const patchRange = patchRangeRecord.base || {}; + return this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME); + } + + /** + * @param {boolean} editMode + */ + _computeContainerClass(editMode) { + return editMode ? 'editMode' : ''; + } + + _computeBlameToggleLabel(loaded, loading) { + if (loaded) { return 'Hide blame'; } + return 'Show blame'; + } + + /** + * Load and display blame information if it has not already been loaded. + * Otherwise hide it. + */ + _toggleBlame() { + if (this._isBlameLoaded) { + this.$.diffHost.clearBlame(); + return; + } + + this._isBlameLoading = true; + this.fire('show-alert', {message: MSG_LOADING_BLAME}); + this.$.diffHost.loadBlame() + .then(() => { + this._isBlameLoading = false; + this.fire('show-alert', {message: MSG_LOADED_BLAME}); + }) + .catch(() => { + this._isBlameLoading = false; + }); + } + + _handleToggleBlame(e) { + if (this.shouldSuppressKeyboardShortcut(e) || + this.modifierPressed(e)) { return; } + this._toggleBlame(); + } + + _computeBlameLoaderClass(isImageDiff, path) { + return !this.isMagicPath(path) && !isImageDiff ? 'show' : ''; + } + + _getRevisionInfo(change) { + return new Gerrit.RevisionInfo(change); + } + + _computeFileNum(file, files) { + // Polymer 2: check for undefined + if ([file, files].some(arg => arg === undefined)) { + return undefined; + } + + return files.findIndex(({value}) => value === file) + 1; + } + + /** + * @param {number} fileNum + * @param {!Array<string>} files + * @return {string} + */ + _computeFileNumClass(fileNum, files) { + if (files && fileNum > 0) { + return 'show'; + } + return ''; + } + + _handleExpandAllDiffContext(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + this.$.diffHost.expandAllContext(); + } + + _computeDiffPrefsDisabled(disableDiffPrefs, loggedIn) { + return disableDiffPrefs || !loggedIn; + } + + _handleNextUnreviewedFile(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + this._setReviewed(true); + // Ensure that the currently viewed file always appears in unreviewedFiles + // so we resolve the right "next" file. + const unreviewedFiles = this._fileList + .filter(file => + (file === this._path || !this._reviewedFiles.has(file))); + this._navToFile(this._path, unreviewedFiles, 1); + } + + _handleReloadingDiffPreference() { + this._getDiffPreferences(); + } + + _onChangeHeaderPanelHeightChanged(e) { + this._scrollTopMargin = e.detail.value; + } + + _computeIsLoggedIn(loggedIn) { + return loggedIn ? true : false; + } +} + +customElements.define(GrDiffView.is, GrDiffView);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js index 947ccbd..cf4cf92 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.js
@@ -1,50 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html"> -<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../../shared/revision-info/revision-info.html"> -<link rel="import" href="../gr-comment-api/gr-comment-api.html"> -<link rel="import" href="../gr-diff-cursor/gr-diff-cursor.html"> -<link rel="import" href="../gr-apply-fix-dialog/gr-apply-fix-dialog.html"> -<link rel="import" href="../gr-diff-host/gr-diff-host.html"> -<link rel="import" href="../gr-diff-mode-selector/gr-diff-mode-selector.html"> -<link rel="import" href="../gr-diff-preferences-dialog/gr-diff-preferences-dialog.html"> -<link rel="import" href="../gr-patch-range-select/gr-patch-range-select.html"> - -<dom-module id="gr-diff-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background-color: var(--view-background-color); @@ -225,78 +197,43 @@ } } </style> - <gr-fixed-panel - class$="[[_computeContainerClass(_editMode)]]" - floating-disabled="[[_panelFloatingDisabled]]" - keep-on-scroll - ready-for-measure="[[!_loading]]" - on-floating-height-changed="_onChangeHeaderPanelHeightChanged" - > + <gr-fixed-panel class\$="[[_computeContainerClass(_editMode)]]" floating-disabled="[[_panelFloatingDisabled]]" keep-on-scroll="" ready-for-measure="[[!_loading]]" on-floating-height-changed="_onChangeHeaderPanelHeightChanged"> <header> <div> - <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!-- + <a href\$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!-- --><span class="changeNumberColon">:</span> <span class="headerSubject">[[_change.subject]]</span> - <input id="reviewed" - class="reviewed hideOnEdit" - type="checkbox" - on-change="_handleReviewedChange" - hidden$="[[!_loggedIn]]" hidden><!-- + <input id="reviewed" class="reviewed hideOnEdit" type="checkbox" on-change="_handleReviewedChange" hidden\$="[[!_loggedIn]]" hidden=""><!-- --><div class="jumpToFileContainer"> - <gr-dropdown-list - id="dropdown" - value="[[_path]]" - on-value-change="_handleFileChange" - items="[[_formattedFiles]]" - initial-count="75"> + <gr-dropdown-list id="dropdown" value="[[_path]]" on-value-change="_handleFileChange" items="[[_formattedFiles]]" initial-count="75"> </gr-dropdown-list> </div> </div> <div class="navLinks desktop"> - <span class$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]"> + <span class\$="fileNum [[_computeFileNumClass(_fileNum, _formattedFiles)]]"> File [[_fileNum]] of [[_formattedFiles.length]] <span class="separator"></span> </span> - <a class="navLink" - title="[[createTitle(Shortcut.PREV_FILE, - ShortcutSection.NAVIGATION)]]" - href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]"> + <a class="navLink" title="[[createTitle(Shortcut.PREV_FILE, + ShortcutSection.NAVIGATION)]]" href\$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]"> Prev</a> <span class="separator"></span> - <a class="navLink" - title="[[createTitle(Shortcut.UP_TO_CHANGE, - ShortcutSection.NAVIGATION)]]" - href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]"> + <a class="navLink" title="[[createTitle(Shortcut.UP_TO_CHANGE, + ShortcutSection.NAVIGATION)]]" href\$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]"> Up</a> <span class="separator"></span> - <a class="navLink" - title="[[createTitle(Shortcut.NEXT_FILE, - ShortcutSection.NAVIGATION)]]" - href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]"> + <a class="navLink" title="[[createTitle(Shortcut.NEXT_FILE, + ShortcutSection.NAVIGATION)]]" href\$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]"> Next</a> </div> </header> <div class="subHeader"> <div class="patchRangeLeft"> - <gr-patch-range-select - id="rangeSelect" - change-num="[[_changeNum]]" - change-comments="[[_changeComments]]" - patch-num="[[_patchRange.patchNum]]" - base-patch-num="[[_patchRange.basePatchNum]]" - files-weblinks="[[_filesWeblinks]]" - available-patches="[[_allPatchSets]]" - revisions="[[_change.revisions]]" - revision-info="[[_revisionInfo]]" - on-patch-range-change="_handlePatchChange"> + <gr-patch-range-select id="rangeSelect" change-num="[[_changeNum]]" change-comments="[[_changeComments]]" patch-num="[[_patchRange.patchNum]]" base-patch-num="[[_patchRange.basePatchNum]]" files-weblinks="[[_filesWeblinks]]" available-patches="[[_allPatchSets]]" revisions="[[_change.revisions]]" revision-info="[[_revisionInfo]]" on-patch-range-change="_handlePatchChange"> </gr-patch-range-select> <span class="download desktop"> <span class="separator"></span> - <gr-dropdown - link - down-arrow - items="[[_computeDownloadDropdownLinks(_change.project, _changeNum, _patchRange, _path, _diff)]]" - horizontal-align="left"> + <gr-dropdown link="" down-arrow="" items="[[_computeDownloadDropdownLinks(_change.project, _changeNum, _patchRange, _path, _diff)]]" horizontal-align="left"> <span class="downloadTitle"> Download </span> @@ -304,99 +241,54 @@ </span> </div> <div class="rightControls"> - <span class$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]"> - <gr-button - link - id='toggleBlame' - title="[[createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)]]" - disabled="[[_isBlameLoading]]" - on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button> + <span class\$="blameLoader [[_computeBlameLoaderClass(_isImageDiff, _path)]]"> + <gr-button link="" id="toggleBlame" title="[[createTitle(Shortcut.TOGGLE_BLAME, ShortcutSection.DIFFS)]]" disabled="[[_isBlameLoading]]" on-click="_toggleBlame">[[_computeBlameToggleLabel(_isBlameLoaded, _isBlameLoading)]]</gr-button> </span> <template is="dom-if" if="[[_computeIsLoggedIn(_loggedIn)]]"> <span class="separator"></span> <span class="editButton"> - <gr-button - link - title="Edit current file" - on-click="_goToEditFile">edit</gr-button> + <gr-button link="" title="Edit current file" on-click="_goToEditFile">edit</gr-button> </span> </template> <span class="separator"></span> - <div class$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]"> + <div class\$="diffModeSelector [[_computeModeSelectHideClass(_isImageDiff)]]"> <span>Diff view:</span> - <gr-diff-mode-selector - id="modeSelect" - save-on-change="[[!_diffPrefsDisabled]]" - mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector> + <gr-diff-mode-selector id="modeSelect" save-on-change="[[!_diffPrefsDisabled]]" mode="{{changeViewState.diffMode}}"></gr-diff-mode-selector> </div> - <span id="diffPrefsContainer" - hidden$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden> + <span id="diffPrefsContainer" hidden\$="[[_computePrefsButtonHidden(_prefs, _diffPrefsDisabled)]]" hidden=""> <span class="preferences desktop"> - <gr-button - link - class="prefsButton" - has-tooltip - title="Diff preferences" - on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button> + <gr-button link="" class="prefsButton" has-tooltip="" title="Diff preferences" on-click="_handlePrefsTap"><iron-icon icon="gr-icons:settings"></iron-icon></gr-button> </span> </span> <gr-endpoint-decorator name="annotation-toggler"> - <span hidden id="annotation-span"> + <span hidden="" id="annotation-span"> <label for="annotation-checkbox" id="annotation-label"></label> - <iron-input type="checkbox" disabled> - <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled> + <iron-input type="checkbox" disabled=""> + <input is="iron-input" type="checkbox" id="annotation-checkbox" disabled=""> </iron-input> </span> </gr-endpoint-decorator> </div> </div> <div class="fileNav mobile"> - <a class="mobileNavLink" - href$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]"> + <a class="mobileNavLink" href\$="[[_computeNavLinkURL(_change, _path, _fileList, -1, 1)]]"> <</a> <div class="fullFileName mobile">[[computeDisplayPath(_path)]] </div> - <a class="mobileNavLink" - href$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]"> + <a class="mobileNavLink" href\$="[[_computeNavLinkURL(_change, _path, _fileList, 1, 1)]]"> ></a> </div> </gr-fixed-panel> - <div class="loading" hidden$="[[!_loading]]">Loading...</div> - <gr-diff-host - id="diffHost" - hidden - hidden$="[[_loading]]" - class$="[[_computeDiffClass(_panelFloatingDisabled)]]" - is-image-diff="{{_isImageDiff}}" - files-weblinks="{{_filesWeblinks}}" - diff="{{_diff}}" - change-num="[[_changeNum]]" - commit-range="[[_commitRange]]" - patch-range="[[_patchRange]]" - path="[[_path]]" - prefs="[[_prefs]]" - project-name="[[_change.project]]" - view-mode="[[_diffMode]]" - is-blame-loaded="{{_isBlameLoaded}}" - on-comment-anchor-tap="_onLineSelected" - on-line-selected="_onLineSelected"> + <div class="loading" hidden\$="[[!_loading]]">Loading...</div> + <gr-diff-host id="diffHost" hidden="" hidden\$="[[_loading]]" class\$="[[_computeDiffClass(_panelFloatingDisabled)]]" is-image-diff="{{_isImageDiff}}" files-weblinks="{{_filesWeblinks}}" diff="{{_diff}}" change-num="[[_changeNum]]" commit-range="[[_commitRange]]" patch-range="[[_patchRange]]" path="[[_path]]" prefs="[[_prefs]]" project-name="[[_change.project]]" view-mode="[[_diffMode]]" is-blame-loaded="{{_isBlameLoaded}}" on-comment-anchor-tap="_onLineSelected" on-line-selected="_onLineSelected"> </gr-diff-host> - <gr-apply-fix-dialog - id="applyFixDialog" - prefs="[[_prefs]]" - change="[[_change]]" - change-num="[[_changeNum]]"> + <gr-apply-fix-dialog id="applyFixDialog" prefs="[[_prefs]]" change="[[_change]]" change-num="[[_changeNum]]"> </gr-apply-fix-dialog> - <gr-diff-preferences-dialog - id="diffPreferencesDialog" - diff-prefs="{{_prefs}}" - on-reload-diff-preference="_handleReloadingDiffPreference"> + <gr-diff-preferences-dialog id="diffPreferencesDialog" diff-prefs="{{_prefs}}" on-reload-diff-preference="_handleReloadingDiffPreference"> </gr-diff-preferences-dialog> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> <gr-diff-cursor id="cursor" scroll-top-margin="[[_scrollTopMargin]]"></gr-diff-cursor> <gr-comment-api id="commentAPI"></gr-comment-api> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-diff-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html index a992a6e..35aa664 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -19,18 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-diff-view.html"> +<script type="module" src="./gr-diff-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-diff-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -44,102 +50,533 @@ </template> </test-fixture> -<script> - suite('gr-diff-view tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-diff-view.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-diff-view tests', () => { + suite('basic tests', () => { + const kb = window.Gerrit.KeyboardShortcutBinder; + kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left'); + kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right'); + kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down'); + kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up'); + kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j'); + kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k'); + kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c'); + kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s'); + kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']'); + kb.bindShortcut(kb.Shortcut.PREV_FILE, '['); + kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n'); + kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n'); + kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p'); + kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p'); + kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a'); + kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); + kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u'); + kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ','); + kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm'); + kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r'); + kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x'); + kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e'); + kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e'); + kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m'); + kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b'); - suite('basic tests', async () => { - const kb = window.Gerrit.KeyboardShortcutBinder; - kb.bindShortcut(kb.Shortcut.LEFT_PANE, 'shift+left'); - kb.bindShortcut(kb.Shortcut.RIGHT_PANE, 'shift+right'); - kb.bindShortcut(kb.Shortcut.NEXT_LINE, 'j', 'down'); - kb.bindShortcut(kb.Shortcut.PREV_LINE, 'k', 'up'); - kb.bindShortcut(kb.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j'); - kb.bindShortcut(kb.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k'); - kb.bindShortcut(kb.Shortcut.NEW_COMMENT, 'c'); - kb.bindShortcut(kb.Shortcut.SAVE_COMMENT, 'ctrl+s'); - kb.bindShortcut(kb.Shortcut.NEXT_FILE, ']'); - kb.bindShortcut(kb.Shortcut.PREV_FILE, '['); - kb.bindShortcut(kb.Shortcut.NEXT_CHUNK, 'n'); - kb.bindShortcut(kb.Shortcut.NEXT_COMMENT_THREAD, 'shift+n'); - kb.bindShortcut(kb.Shortcut.PREV_CHUNK, 'p'); - kb.bindShortcut(kb.Shortcut.PREV_COMMENT_THREAD, 'shift+p'); - kb.bindShortcut(kb.Shortcut.OPEN_REPLY_DIALOG, 'a'); - kb.bindShortcut(kb.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); - kb.bindShortcut(kb.Shortcut.UP_TO_CHANGE, 'u'); - kb.bindShortcut(kb.Shortcut.OPEN_DIFF_PREFS, ','); - kb.bindShortcut(kb.Shortcut.TOGGLE_DIFF_MODE, 'm'); - kb.bindShortcut(kb.Shortcut.TOGGLE_FILE_REVIEWED, 'r'); - kb.bindShortcut(kb.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x'); - kb.bindShortcut(kb.Shortcut.EXPAND_ALL_COMMENT_THREADS, 'e'); - kb.bindShortcut(kb.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, 'shift+e'); - kb.bindShortcut(kb.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m'); - kb.bindShortcut(kb.Shortcut.TOGGLE_BLAME, 'b'); + let element; + let sandbox; - let element; - let sandbox; + const PARENT = 'PARENT'; - const PARENT = 'PARENT'; + function getFilesFromFileList(fileList) { + const changeFilesByPath = fileList.reduce((files, path) => { + files[path] = {}; + return files; + }, {}); + return { + sortedFileList: fileList, + changeFilesByPath, + }; + } - function getFilesFromFileList(fileList) { - const changeFilesByPath = fileList.reduce((files, path) => { - files[path] = {}; - return files; - }, {}); - return { - sortedFileList: fileList, - changeFilesByPath, - }; - } + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({change: {}}); }, + getLoggedIn() { return Promise.resolve(false); }, + getProjectConfig() { return Promise.resolve({}); }, + getDiffChangeDetail() { return Promise.resolve({}); }, + getChangeFiles() { return Promise.resolve({}); }, + saveFileReviewed() { return Promise.resolve(); }, + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + getReviewedFiles() { return Promise.resolve([]); }, + }); + element = fixture('basic'); + return element._loadComments(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('params change triggers diffViewDisplayed()', () => { + sandbox.stub(element.$.reporting, 'diffViewDisplayed'); + sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve()); + sandbox.spy(element, '_paramsChanged'); + element.params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + patchNum: '2', + basePatchNum: '1', + path: '/COMMIT_MSG', + }; + + return element._paramsChanged.returnValues[0].then(() => { + assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce); + }); + }); + + test('toggle left diff with a hotkey', () => { + const toggleLeftDiffStub = sandbox.stub( + element.$.diffHost, 'toggleLeftDiff'); + MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); + assert.isTrue(toggleLeftDiffStub.calledOnce); + }); + + test('keyboard shortcuts', () => { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: PARENT, + patchNum: '10', + }; + element._change = { + _number: 42, + revisions: { + a: {_number: 10, commit: {parents: []}}, + }, + }; + element._files = getFilesFromFileList( + ['chell.go', 'glados.txt', 'wheatley.md']); + element._path = 'glados.txt'; + element.changeViewState.selectedFileIndex = 1; + element._loggedIn = true; + + const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert(changeNavStub.lastCall.calledWith(element._change), + 'Should navigate to /c/42/'); + + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); + assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md', + '10', PARENT), 'Should navigate to /c/42/10/wheatley.md'); + element._path = 'wheatley.md'; + assert.equal(element.changeViewState.selectedFileIndex, 2); + assert.isTrue(element._loading); + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt', + '10', PARENT), 'Should navigate to /c/42/10/glados.txt'); + element._path = 'glados.txt'; + assert.equal(element.changeViewState.selectedFileIndex, 1); + assert.isTrue(element._loading); + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', '10', + PARENT), 'Should navigate to /c/42/10/chell.go'); + element._path = 'chell.go'; + assert.equal(element.changeViewState.selectedFileIndex, 0); + assert.isTrue(element._loading); + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert(changeNavStub.lastCall.calledWith(element._change), + 'Should navigate to /c/42/'); + assert.equal(element.changeViewState.selectedFileIndex, 0); + assert.isTrue(element._loading); + + const showPrefsStub = + sandbox.stub(element.$.diffPreferencesDialog, 'open', + () => Promise.resolve()); + + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); + assert(showPrefsStub.calledOnce); + + element.disableDiffPrefs = true; + MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); + assert(showPrefsStub.calledOnce); + + let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk'); + MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); + assert(scrollStub.calledOnce); + + scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk'); + MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p'); + assert(scrollStub.calledOnce); + + scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread'); + MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); + assert(scrollStub.calledOnce); + + scrollStub = sandbox.stub(element.$.cursor, + 'moveToPreviousCommentThread'); + MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p'); + assert(scrollStub.calledOnce); + + const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff, + '_computeContainerClass'); + MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); + assert(computeContainerClassStub.lastCall.calledWithExactly( + false, 'SIDE_BY_SIDE', true)); + + MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc'); + assert(computeContainerClassStub.lastCall.calledWithExactly( + false, 'SIDE_BY_SIDE', false)); + + sandbox.stub(element, '_setReviewed'); + element.$.reviewed.checked = false; + MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); + assert.isFalse(element._setReviewed.called); + + MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); + assert.isTrue(element._setReviewed.called); + assert.equal(element._setReviewed.lastCall.args[0], true); + }); + + test('shift+x shortcut expands all diff context', () => { + const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext'); + MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x'); + flushAsynchronousOperations(); + assert.isTrue(expandStub.called); + }); + + test('keyboard shortcuts with patch range', () => { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: '5', + patchNum: '10', + }; + element._change = { + _number: 42, + revisions: { + a: {_number: 10, commit: {parents: []}}, + b: {_number: 5, commit: {parents: []}}, + }, + }; + element._files = getFilesFromFileList( + ['chell.go', 'glados.txt', 'wheatley.md']); + element._path = 'glados.txt'; + + const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' + + 'should only work when the user is logged in.'); + assert.isNull(window.sessionStorage.getItem( + 'changeView.showReplyDialog')); + + element._loggedIn = true; + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + assert.isTrue(element.changeViewState.showReplyDialog); + + assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', + '5'), 'Should navigate to /c/42/5..10'); + + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', + '5'), 'Should navigate to /c/42/5..10'); + + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); + assert.isTrue(element._loading); + assert(diffNavStub.lastCall.calledWithExactly(element._change, + 'wheatley.md', '10', '5'), + 'Should navigate to /c/42/5..10/wheatley.md'); + element._path = 'wheatley.md'; + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert.isTrue(element._loading); + assert(diffNavStub.lastCall.calledWithExactly(element._change, + 'glados.txt', '10', '5'), + 'Should navigate to /c/42/5..10/glados.txt'); + element._path = 'glados.txt'; + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert.isTrue(element._loading); + assert(diffNavStub.lastCall.calledWithExactly( + element._change, + 'chell.go', + '10', + '5'), + 'Should navigate to /c/42/5..10/chell.go'); + element._path = 'chell.go'; + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert.isTrue(element._loading); + assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', + '5'), + 'Should navigate to /c/42/5..10'); + }); + + test('keyboard shortcuts with old patch number', () => { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: PARENT, + patchNum: '1', + }; + element._change = { + _number: 42, + revisions: { + a: {_number: 1, commit: {parents: []}}, + b: {_number: 2, commit: {parents: []}}, + }, + }; + element._files = getFilesFromFileList( + ['chell.go', 'glados.txt', 'wheatley.md']); + element._path = 'glados.txt'; + + const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' + + 'should only work when the user is logged in.'); + assert.isNull(window.sessionStorage.getItem( + 'changeView.showReplyDialog')); + + element._loggedIn = true; + MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); + assert.isTrue(element.changeViewState.showReplyDialog); + + assert(changeNavStub.lastCall.calledWithExactly(element._change, '1', + PARENT), 'Should navigate to /c/42/1'); + + MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); + assert(changeNavStub.lastCall.calledWithExactly(element._change, '1', + PARENT), 'Should navigate to /c/42/1'); + + MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); + assert(diffNavStub.lastCall.calledWithExactly(element._change, + 'wheatley.md', '1', PARENT), + 'Should navigate to /c/42/1/wheatley.md'); + element._path = 'wheatley.md'; + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert(diffNavStub.lastCall.calledWithExactly(element._change, + 'glados.txt', '1', PARENT), + 'Should navigate to /c/42/1/glados.txt'); + element._path = 'glados.txt'; + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert(diffNavStub.lastCall.calledWithExactly( + element._change, + 'chell.go', + '1', + PARENT), 'Should navigate to /c/42/1/chell.go'); + element._path = 'chell.go'; + + MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); + assert(changeNavStub.lastCall.calledWithExactly(element._change, '1', + PARENT), 'Should navigate to /c/42/1'); + }); + + test('edit should redirect to edit page', done => { + element._loggedIn = true; + element._path = 't.txt'; + element._patchRange = { + basePatchNum: PARENT, + patchNum: '1', + }; + element._change = { + _number: 42, + revisions: { + a: {_number: 1, commit: {parents: []}}, + b: {_number: 2, commit: {parents: []}}, + }, + }; + const redirectStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); + flush(() => { + const editBtn = element.shadowRoot + .querySelector('.editButton gr-button'); + assert.isTrue(!!editBtn); + MockInteractions.tap(editBtn); + assert.isTrue(redirectStub.called); + done(); + }); + }); + + test('edit hidden when not logged in', done => { + element._loggedIn = false; + element._path = 't.txt'; + element._patchRange = { + basePatchNum: PARENT, + patchNum: '1', + }; + element._change = { + _number: 42, + revisions: { + a: {_number: 1, commit: {parents: []}}, + b: {_number: 2, commit: {parents: []}}, + }, + }; + flush(() => { + const editBtn = element.shadowRoot + .querySelector('.editButton gr-button'); + assert.isFalse(!!editBtn); + done(); + }); + }); + + suite('diff prefs hidden', () => { + test('when no prefs or logged out', () => { + element.disableDiffPrefs = false; + element._loggedIn = false; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = true; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = false; + element._prefs = {font_size: '12'}; + flushAsynchronousOperations(); + assert.isTrue(element.$.diffPrefsContainer.hidden); + + element._loggedIn = true; + flushAsynchronousOperations(); + assert.isFalse(element.$.diffPrefsContainer.hidden); + }); + + test('when disableDiffPrefs is set', () => { + element._loggedIn = true; + element._prefs = {font_size: '12'}; + element.disableDiffPrefs = false; + flushAsynchronousOperations(); + + assert.isFalse(element.$.diffPrefsContainer.hidden); + element.disableDiffPrefs = true; + flushAsynchronousOperations(); + + assert.isTrue(element.$.diffPrefsContainer.hidden); + }); + }); + + test('prefsButton opens gr-diff-preferences', () => { + const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap'); + const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog, + 'open'); + const prefsButton = + dom(element.root).querySelector('.prefsButton'); + + MockInteractions.tap(prefsButton); + + assert.isTrue(handlePrefsTapSpy.called); + assert.isTrue(overlayOpenStub.called); + }); + + test('_computeCommentString', done => { + const path = '/test'; + element.$.commentAPI.loadAll().then(comments => { + const commentCountStub = + sandbox.stub(comments, 'computeCommentCount'); + const unresolvedCountStub = + sandbox.stub(comments, 'computeUnresolvedNum'); + commentCountStub.withArgs(1, path).returns(0); + commentCountStub.withArgs(2, path).returns(1); + commentCountStub.withArgs(3, path).returns(2); + commentCountStub.withArgs(4, path).returns(0); + unresolvedCountStub.withArgs(1, path).returns(1); + unresolvedCountStub.withArgs(2, path).returns(0); + unresolvedCountStub.withArgs(3, path).returns(2); + unresolvedCountStub.withArgs(4, path).returns(0); + + assert.equal(element._computeCommentString(comments, 1, path, {}), + '1 unresolved'); + assert.equal( + element._computeCommentString(comments, 2, path, {status: 'M'}), + '1 comment'); + assert.equal( + element._computeCommentString(comments, 2, path, {status: 'U'}), + 'no changes, 1 comment'); + assert.equal( + element._computeCommentString(comments, 3, path, {status: 'A'}), + '2 comments, 2 unresolved'); + assert.equal( + element._computeCommentString( + comments, 4, path, {status: 'M'} + ), ''); + assert.equal( + element._computeCommentString(comments, 4, path, {status: 'U'}), + 'no changes'); + done(); + }); + }); + + suite('url params', () => { setup(() => { - sandbox = sinon.sandbox.create(); - - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({change: {}}); }, - getLoggedIn() { return Promise.resolve(false); }, - getProjectConfig() { return Promise.resolve({}); }, - getDiffChangeDetail() { return Promise.resolve({}); }, - getChangeFiles() { return Promise.resolve({}); }, - saveFileReviewed() { return Promise.resolve(); }, - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - getReviewedFiles() { return Promise.resolve([]); }, - }); - element = fixture('basic'); - return element._loadComments(); + sandbox.stub( + Gerrit.Nav, + 'getUrlForDiff', + (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`); + sandbox.stub( + Gerrit.Nav + , 'getUrlForChange', + (c, pn, bpn) => `${c._number}-${pn}-${bpn}`); }); - teardown(() => { - sandbox.restore(); - }); - - test('params change triggers diffViewDisplayed()', () => { - sandbox.stub(element.$.reporting, 'diffViewDisplayed'); - sandbox.stub(element.$.diffHost, 'reload').returns(Promise.resolve()); - sandbox.spy(element, '_paramsChanged'); - element.params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - patchNum: '2', - basePatchNum: '1', - path: '/COMMIT_MSG', + test('_formattedFiles', () => { + element._changeNum = '42'; + element._patchRange = { + basePatchNum: PARENT, + patchNum: '10', }; + element._change = {_number: 42}; + element._files = getFilesFromFileList( + ['chell.go', 'glados.txt', 'wheatley.md', + '/COMMIT_MSG', '/MERGE_LIST']); + element._path = 'glados.txt'; + const expectedFormattedFiles = [ + { + text: 'chell.go', + mobileText: 'chell.go', + value: 'chell.go', + bottomText: '', + }, { + text: 'glados.txt', + mobileText: 'glados.txt', + value: 'glados.txt', + bottomText: '', + }, { + text: 'wheatley.md', + mobileText: 'wheatley.md', + value: 'wheatley.md', + bottomText: '', + }, + { + text: 'Commit message', + mobileText: 'Commit message', + value: '/COMMIT_MSG', + bottomText: '', + }, + { + text: 'Merge list', + mobileText: 'Merge list', + value: '/MERGE_LIST', + bottomText: '', + }, + ]; - return element._paramsChanged.returnValues[0].then(() => { - assert.isTrue(element.$.reporting.diffViewDisplayed.calledOnce); - }); + assert.deepEqual(element._formattedFiles, expectedFormattedFiles); + assert.equal(element._formattedFiles[1].value, element._path); }); - test('toggle left diff with a hotkey', () => { - const toggleLeftDiffStub = sandbox.stub( - element.$.diffHost, 'toggleLeftDiff'); - MockInteractions.pressAndReleaseKeyOn(element, 65, 'shift', 'a'); - assert.isTrue(toggleLeftDiffStub.calledOnce); - }); - - test('keyboard shortcuts', () => { + test('prev/up/next links', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: PARENT, @@ -154,99 +591,34 @@ element._files = getFilesFromFileList( ['chell.go', 'glados.txt', 'wheatley.md']); element._path = 'glados.txt'; - element.changeViewState.selectedFileIndex = 1; - element._loggedIn = true; - - const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - - MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); - assert(changeNavStub.lastCall.calledWith(element._change), - 'Should navigate to /c/42/'); - - MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); - assert(diffNavStub.lastCall.calledWith(element._change, 'wheatley.md', - '10', PARENT), 'Should navigate to /c/42/10/wheatley.md'); - element._path = 'wheatley.md'; - assert.equal(element.changeViewState.selectedFileIndex, 2); - assert.isTrue(element._loading); - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert(diffNavStub.lastCall.calledWith(element._change, 'glados.txt', - '10', PARENT), 'Should navigate to /c/42/10/glados.txt'); - element._path = 'glados.txt'; - assert.equal(element.changeViewState.selectedFileIndex, 1); - assert.isTrue(element._loading); - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert(diffNavStub.lastCall.calledWith(element._change, 'chell.go', '10', - PARENT), 'Should navigate to /c/42/10/chell.go'); - element._path = 'chell.go'; - assert.equal(element.changeViewState.selectedFileIndex, 0); - assert.isTrue(element._loading); - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert(changeNavStub.lastCall.calledWith(element._change), - 'Should navigate to /c/42/'); - assert.equal(element.changeViewState.selectedFileIndex, 0); - assert.isTrue(element._loading); - - const showPrefsStub = - sandbox.stub(element.$.diffPreferencesDialog, 'open', - () => Promise.resolve()); - - MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); - assert(showPrefsStub.calledOnce); - - element.disableDiffPrefs = true; - MockInteractions.pressAndReleaseKeyOn(element, 188, null, ','); - assert(showPrefsStub.calledOnce); - - let scrollStub = sandbox.stub(element.$.cursor, 'moveToNextChunk'); - MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n'); - assert(scrollStub.calledOnce); - - scrollStub = sandbox.stub(element.$.cursor, 'moveToPreviousChunk'); - MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p'); - assert(scrollStub.calledOnce); - - scrollStub = sandbox.stub(element.$.cursor, 'moveToNextCommentThread'); - MockInteractions.pressAndReleaseKeyOn(element, 78, 'shift', 'n'); - assert(scrollStub.calledOnce); - - scrollStub = sandbox.stub(element.$.cursor, - 'moveToPreviousCommentThread'); - MockInteractions.pressAndReleaseKeyOn(element, 80, 'shift', 'p'); - assert(scrollStub.calledOnce); - - const computeContainerClassStub = sandbox.stub(element.$.diffHost.$.diff, - '_computeContainerClass'); - MockInteractions.pressAndReleaseKeyOn(element, 74, null, 'j'); - assert(computeContainerClassStub.lastCall.calledWithExactly( - false, 'SIDE_BY_SIDE', true)); - - MockInteractions.pressAndReleaseKeyOn(element, 27, null, 'esc'); - assert(computeContainerClassStub.lastCall.calledWithExactly( - false, 'SIDE_BY_SIDE', false)); - - sandbox.stub(element, '_setReviewed'); - element.$.reviewed.checked = false; - MockInteractions.pressAndReleaseKeyOn(element, 82, 'shift', 'r'); - assert.isFalse(element._setReviewed.called); - - MockInteractions.pressAndReleaseKeyOn(element, 82, null, 'r'); - assert.isTrue(element._setReviewed.called); - assert.equal(element._setReviewed.lastCall.args[0], true); - }); - - test('shift+x shortcut expands all diff context', () => { - const expandStub = sandbox.stub(element.$.diffHost, 'expandAllContext'); - MockInteractions.pressAndReleaseKeyOn(element, 88, 'shift', 'x'); flushAsynchronousOperations(); - assert.isTrue(expandStub.called); + const linkEls = dom(element.root).querySelectorAll('.navLink'); + assert.equal(linkEls.length, 3); + assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT'); + assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); + assert.equal(linkEls[2].getAttribute('href'), + '42-wheatley.md-10-PARENT'); + element._path = 'wheatley.md'; + flushAsynchronousOperations(); + assert.equal(linkEls[0].getAttribute('href'), + '42-glados.txt-10-PARENT'); + assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); + assert.isFalse(linkEls[2].hasAttribute('href')); + element._path = 'chell.go'; + flushAsynchronousOperations(); + assert.isFalse(linkEls[0].hasAttribute('href')); + assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); + assert.equal(linkEls[2].getAttribute('href'), + '42-glados.txt-10-PARENT'); + element._path = 'not_a_real_file'; + flushAsynchronousOperations(); + assert.equal(linkEls[0].getAttribute('href'), + '42-wheatley.md-10-PARENT'); + assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); + assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT'); }); - test('keyboard shortcuts with patch range', () => { + test('prev/up/next links with patch range', () => { element._changeNum = '42'; element._patchRange = { basePatchNum: '5', @@ -255,1168 +627,805 @@ element._change = { _number: 42, revisions: { - a: {_number: 10, commit: {parents: []}}, - b: {_number: 5, commit: {parents: []}}, + a: {_number: 5, commit: {parents: []}}, + b: {_number: 10, commit: {parents: []}}, }, }; element._files = getFilesFromFileList( ['chell.go', 'glados.txt', 'wheatley.md']); element._path = 'glados.txt'; - - const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' + - 'should only work when the user is logged in.'); - assert.isNull(window.sessionStorage.getItem( - 'changeView.showReplyDialog')); - - element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - assert.isTrue(element.changeViewState.showReplyDialog); - - assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', - '5'), 'Should navigate to /c/42/5..10'); - - MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); - assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', - '5'), 'Should navigate to /c/42/5..10'); - - MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); - assert.isTrue(element._loading); - assert(diffNavStub.lastCall.calledWithExactly(element._change, - 'wheatley.md', '10', '5'), - 'Should navigate to /c/42/5..10/wheatley.md'); + flushAsynchronousOperations(); + const linkEls = dom(element.root).querySelectorAll('.navLink'); + assert.equal(linkEls.length, 3); + assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5'); + assert.equal(linkEls[1].getAttribute('href'), '42-10-5'); + assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5'); element._path = 'wheatley.md'; - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert.isTrue(element._loading); - assert(diffNavStub.lastCall.calledWithExactly(element._change, - 'glados.txt', '10', '5'), - 'Should navigate to /c/42/5..10/glados.txt'); - element._path = 'glados.txt'; - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert.isTrue(element._loading); - assert(diffNavStub.lastCall.calledWithExactly( - element._change, - 'chell.go', - '10', - '5'), - 'Should navigate to /c/42/5..10/chell.go'); + flushAsynchronousOperations(); + assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5'); + assert.equal(linkEls[1].getAttribute('href'), '42-10-5'); + assert.isFalse(linkEls[2].hasAttribute('href')); element._path = 'chell.go'; - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert.isTrue(element._loading); - assert(changeNavStub.lastCall.calledWithExactly(element._change, '10', - '5'), - 'Should navigate to /c/42/5..10'); - }); - - test('keyboard shortcuts with old patch number', () => { - element._changeNum = '42'; - element._patchRange = { - basePatchNum: PARENT, - patchNum: '1', - }; - element._change = { - _number: 42, - revisions: { - a: {_number: 1, commit: {parents: []}}, - b: {_number: 2, commit: {parents: []}}, - }, - }; - element._files = getFilesFromFileList( - ['chell.go', 'glados.txt', 'wheatley.md']); - element._path = 'glados.txt'; - - const diffNavStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - const changeNavStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - assert.isTrue(changeNavStub.notCalled, 'The `a` keyboard shortcut ' + - 'should only work when the user is logged in.'); - assert.isNull(window.sessionStorage.getItem( - 'changeView.showReplyDialog')); - - element._loggedIn = true; - MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a'); - assert.isTrue(element.changeViewState.showReplyDialog); - - assert(changeNavStub.lastCall.calledWithExactly(element._change, '1', - PARENT), 'Should navigate to /c/42/1'); - - MockInteractions.pressAndReleaseKeyOn(element, 85, null, 'u'); - assert(changeNavStub.lastCall.calledWithExactly(element._change, '1', - PARENT), 'Should navigate to /c/42/1'); - - MockInteractions.pressAndReleaseKeyOn(element, 221, null, ']'); - assert(diffNavStub.lastCall.calledWithExactly(element._change, - 'wheatley.md', '1', PARENT), - 'Should navigate to /c/42/1/wheatley.md'); - element._path = 'wheatley.md'; - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert(diffNavStub.lastCall.calledWithExactly(element._change, - 'glados.txt', '1', PARENT), - 'Should navigate to /c/42/1/glados.txt'); - element._path = 'glados.txt'; - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert(diffNavStub.lastCall.calledWithExactly( - element._change, - 'chell.go', - '1', - PARENT), 'Should navigate to /c/42/1/chell.go'); - element._path = 'chell.go'; - - MockInteractions.pressAndReleaseKeyOn(element, 219, null, '['); - assert(changeNavStub.lastCall.calledWithExactly(element._change, '1', - PARENT), 'Should navigate to /c/42/1'); - }); - - test('edit should redirect to edit page', done => { - element._loggedIn = true; - element._path = 't.txt'; - element._patchRange = { - basePatchNum: PARENT, - patchNum: '1', - }; - element._change = { - _number: 42, - revisions: { - a: {_number: 1, commit: {parents: []}}, - b: {_number: 2, commit: {parents: []}}, - }, - }; - const redirectStub = sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'); - flush(() => { - const editBtn = element.shadowRoot - .querySelector('.editButton gr-button'); - assert.isTrue(!!editBtn); - MockInteractions.tap(editBtn); - assert.isTrue(redirectStub.called); - done(); - }); - }); - - test('edit hidden when not logged in', done => { - element._loggedIn = false; - element._path = 't.txt'; - element._patchRange = { - basePatchNum: PARENT, - patchNum: '1', - }; - element._change = { - _number: 42, - revisions: { - a: {_number: 1, commit: {parents: []}}, - b: {_number: 2, commit: {parents: []}}, - }, - }; - flush(() => { - const editBtn = element.shadowRoot - .querySelector('.editButton gr-button'); - assert.isFalse(!!editBtn); - done(); - }); - }); - - suite('diff prefs hidden', () => { - test('when no prefs or logged out', () => { - element.disableDiffPrefs = false; - element._loggedIn = false; - flushAsynchronousOperations(); - assert.isTrue(element.$.diffPrefsContainer.hidden); - - element._loggedIn = true; - flushAsynchronousOperations(); - assert.isTrue(element.$.diffPrefsContainer.hidden); - - element._loggedIn = false; - element._prefs = {font_size: '12'}; - flushAsynchronousOperations(); - assert.isTrue(element.$.diffPrefsContainer.hidden); - - element._loggedIn = true; - flushAsynchronousOperations(); - assert.isFalse(element.$.diffPrefsContainer.hidden); - }); - - test('when disableDiffPrefs is set', () => { - element._loggedIn = true; - element._prefs = {font_size: '12'}; - element.disableDiffPrefs = false; - flushAsynchronousOperations(); - - assert.isFalse(element.$.diffPrefsContainer.hidden); - element.disableDiffPrefs = true; - flushAsynchronousOperations(); - - assert.isTrue(element.$.diffPrefsContainer.hidden); - }); - }); - - test('prefsButton opens gr-diff-preferences', () => { - const handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap'); - const overlayOpenStub = sandbox.stub(element.$.diffPreferencesDialog, - 'open'); - const prefsButton = - Polymer.dom(element.root).querySelector('.prefsButton'); - - MockInteractions.tap(prefsButton); - - assert.isTrue(handlePrefsTapSpy.called); - assert.isTrue(overlayOpenStub.called); - }); - - test('_computeCommentString', done => { - const path = '/test'; - element.$.commentAPI.loadAll().then(comments => { - const commentCountStub = - sandbox.stub(comments, 'computeCommentCount'); - const unresolvedCountStub = - sandbox.stub(comments, 'computeUnresolvedNum'); - commentCountStub.withArgs(1, path).returns(0); - commentCountStub.withArgs(2, path).returns(1); - commentCountStub.withArgs(3, path).returns(2); - commentCountStub.withArgs(4, path).returns(0); - unresolvedCountStub.withArgs(1, path).returns(1); - unresolvedCountStub.withArgs(2, path).returns(0); - unresolvedCountStub.withArgs(3, path).returns(2); - unresolvedCountStub.withArgs(4, path).returns(0); - - assert.equal(element._computeCommentString(comments, 1, path, {}), - '1 unresolved'); - assert.equal( - element._computeCommentString(comments, 2, path, {status: 'M'}), - '1 comment'); - assert.equal( - element._computeCommentString(comments, 2, path, {status: 'U'}), - 'no changes, 1 comment'); - assert.equal( - element._computeCommentString(comments, 3, path, {status: 'A'}), - '2 comments, 2 unresolved'); - assert.equal( - element._computeCommentString( - comments, 4, path, {status: 'M'} - ), ''); - assert.equal( - element._computeCommentString(comments, 4, path, {status: 'U'}), - 'no changes'); - done(); - }); - }); - - suite('url params', () => { - setup(() => { - sandbox.stub( - Gerrit.Nav, - 'getUrlForDiff', - (c, p, pn, bpn) => `${c._number}-${p}-${pn}-${bpn}`); - sandbox.stub( - Gerrit.Nav - , 'getUrlForChange', - (c, pn, bpn) => `${c._number}-${pn}-${bpn}`); - }); - - test('_formattedFiles', () => { - element._changeNum = '42'; - element._patchRange = { - basePatchNum: PARENT, - patchNum: '10', - }; - element._change = {_number: 42}; - element._files = getFilesFromFileList( - ['chell.go', 'glados.txt', 'wheatley.md', - '/COMMIT_MSG', '/MERGE_LIST']); - element._path = 'glados.txt'; - const expectedFormattedFiles = [ - { - text: 'chell.go', - mobileText: 'chell.go', - value: 'chell.go', - bottomText: '', - }, { - text: 'glados.txt', - mobileText: 'glados.txt', - value: 'glados.txt', - bottomText: '', - }, { - text: 'wheatley.md', - mobileText: 'wheatley.md', - value: 'wheatley.md', - bottomText: '', - }, - { - text: 'Commit message', - mobileText: 'Commit message', - value: '/COMMIT_MSG', - bottomText: '', - }, - { - text: 'Merge list', - mobileText: 'Merge list', - value: '/MERGE_LIST', - bottomText: '', - }, - ]; - - assert.deepEqual(element._formattedFiles, expectedFormattedFiles); - assert.equal(element._formattedFiles[1].value, element._path); - }); - - test('prev/up/next links', () => { - element._changeNum = '42'; - element._patchRange = { - basePatchNum: PARENT, - patchNum: '10', - }; - element._change = { - _number: 42, - revisions: { - a: {_number: 10, commit: {parents: []}}, - }, - }; - element._files = getFilesFromFileList( - ['chell.go', 'glados.txt', 'wheatley.md']); - element._path = 'glados.txt'; - flushAsynchronousOperations(); - const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); - assert.equal(linkEls.length, 3); - assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-PARENT'); - assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); - assert.equal(linkEls[2].getAttribute('href'), - '42-wheatley.md-10-PARENT'); - element._path = 'wheatley.md'; - flushAsynchronousOperations(); - assert.equal(linkEls[0].getAttribute('href'), - '42-glados.txt-10-PARENT'); - assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); - assert.isFalse(linkEls[2].hasAttribute('href')); - element._path = 'chell.go'; - flushAsynchronousOperations(); - assert.isFalse(linkEls[0].hasAttribute('href')); - assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); - assert.equal(linkEls[2].getAttribute('href'), - '42-glados.txt-10-PARENT'); - element._path = 'not_a_real_file'; - flushAsynchronousOperations(); - assert.equal(linkEls[0].getAttribute('href'), - '42-wheatley.md-10-PARENT'); - assert.equal(linkEls[1].getAttribute('href'), '42-undefined-undefined'); - assert.equal(linkEls[2].getAttribute('href'), '42-chell.go-10-PARENT'); - }); - - test('prev/up/next links with patch range', () => { - element._changeNum = '42'; - element._patchRange = { - basePatchNum: '5', - patchNum: '10', - }; - element._change = { - _number: 42, - revisions: { - a: {_number: 5, commit: {parents: []}}, - b: {_number: 10, commit: {parents: []}}, - }, - }; - element._files = getFilesFromFileList( - ['chell.go', 'glados.txt', 'wheatley.md']); - element._path = 'glados.txt'; - flushAsynchronousOperations(); - const linkEls = Polymer.dom(element.root).querySelectorAll('.navLink'); - assert.equal(linkEls.length, 3); - assert.equal(linkEls[0].getAttribute('href'), '42-chell.go-10-5'); - assert.equal(linkEls[1].getAttribute('href'), '42-10-5'); - assert.equal(linkEls[2].getAttribute('href'), '42-wheatley.md-10-5'); - element._path = 'wheatley.md'; - flushAsynchronousOperations(); - assert.equal(linkEls[0].getAttribute('href'), '42-glados.txt-10-5'); - assert.equal(linkEls[1].getAttribute('href'), '42-10-5'); - assert.isFalse(linkEls[2].hasAttribute('href')); - element._path = 'chell.go'; - flushAsynchronousOperations(); - assert.isFalse(linkEls[0].hasAttribute('href')); - assert.equal(linkEls[1].getAttribute('href'), '42-10-5'); - assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5'); - }); - }); - - test('_handlePatchChange calls navigateToDiff correctly', () => { - const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - element._change = {_number: 321, project: 'foo/bar'}; - element._path = 'path/to/file.txt'; - - element._patchRange = { - basePatchNum: 'PARENT', - patchNum: '3', - }; - - const detail = { - basePatchNum: 'PARENT', - patchNum: '1', - }; - - element.$.rangeSelect.dispatchEvent( - new CustomEvent('patch-range-change', {detail, bubbles: false})); - - assert(navigateStub.lastCall.calledWithExactly(element._change, - element._path, '1', 'PARENT')); - }); - - test('_prefs.manual_review is respected', () => { - const saveReviewedStub = sandbox.stub(element, '_saveReviewedState', - () => Promise.resolve()); - const getReviewedStub = sandbox.stub(element, '_getReviewedStatus', - () => Promise.resolve()); - - sandbox.stub(element.$.diffHost, 'reload'); - element._loggedIn = true; - element.params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - patchNum: '2', - basePatchNum: '1', - path: '/COMMIT_MSG', - }; - element._prefs = {manual_review: true}; flushAsynchronousOperations(); - - assert.isFalse(saveReviewedStub.called); - assert.isTrue(getReviewedStub.called); - - element._prefs = {}; - flushAsynchronousOperations(); - - assert.isTrue(saveReviewedStub.called); - assert.isTrue(getReviewedStub.calledOnce); + assert.isFalse(linkEls[0].hasAttribute('href')); + assert.equal(linkEls[1].getAttribute('href'), '42-10-5'); + assert.equal(linkEls[2].getAttribute('href'), '42-glados.txt-10-5'); }); + }); - test('file review status', () => { - const saveReviewedStub = sandbox.stub(element, '_saveReviewedState', - () => Promise.resolve()); - sandbox.stub(element.$.diffHost, 'reload'); + test('_handlePatchChange calls navigateToDiff correctly', () => { + const navigateStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + element._change = {_number: 321, project: 'foo/bar'}; + element._path = 'path/to/file.txt'; - element._loggedIn = true; - element.params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - patchNum: '2', - basePatchNum: '1', - path: '/COMMIT_MSG', - }; - element._prefs = {}; - flushAsynchronousOperations(); + element._patchRange = { + basePatchNum: 'PARENT', + patchNum: '3', + }; - const commitMsg = Polymer.dom(element.root).querySelector( - 'input[type="checkbox"]'); + const detail = { + basePatchNum: 'PARENT', + patchNum: '1', + }; - assert.isTrue(commitMsg.checked); - MockInteractions.tap(commitMsg); - assert.isFalse(commitMsg.checked); - assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false)); + element.$.rangeSelect.dispatchEvent( + new CustomEvent('patch-range-change', {detail, bubbles: false})); - MockInteractions.tap(commitMsg); - assert.isTrue(commitMsg.checked); - assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true)); - const callCount = saveReviewedStub.callCount; + assert(navigateStub.lastCall.calledWithExactly(element._change, + element._path, '1', 'PARENT')); + }); - element.set('params.view', Gerrit.Nav.View.CHANGE); - flushAsynchronousOperations(); + test('_prefs.manual_review is respected', () => { + const saveReviewedStub = sandbox.stub(element, '_saveReviewedState', + () => Promise.resolve()); + const getReviewedStub = sandbox.stub(element, '_getReviewedStatus', + () => Promise.resolve()); - // saveReviewedState observer observes params, but should not fire when - // view !== Gerrit.Nav.View.DIFF. - assert.equal(saveReviewedStub.callCount, callCount); + sandbox.stub(element.$.diffHost, 'reload'); + element._loggedIn = true; + element.params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + patchNum: '2', + basePatchNum: '1', + path: '/COMMIT_MSG', + }; + element._prefs = {manual_review: true}; + flushAsynchronousOperations(); + + assert.isFalse(saveReviewedStub.called); + assert.isTrue(getReviewedStub.called); + + element._prefs = {}; + flushAsynchronousOperations(); + + assert.isTrue(saveReviewedStub.called); + assert.isTrue(getReviewedStub.calledOnce); + }); + + test('file review status', () => { + const saveReviewedStub = sandbox.stub(element, '_saveReviewedState', + () => Promise.resolve()); + sandbox.stub(element.$.diffHost, 'reload'); + + element._loggedIn = true; + element.params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + patchNum: '2', + basePatchNum: '1', + path: '/COMMIT_MSG', + }; + element._prefs = {}; + flushAsynchronousOperations(); + + const commitMsg = dom(element.root).querySelector( + 'input[type="checkbox"]'); + + assert.isTrue(commitMsg.checked); + MockInteractions.tap(commitMsg); + assert.isFalse(commitMsg.checked); + assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(false)); + + MockInteractions.tap(commitMsg); + assert.isTrue(commitMsg.checked); + assert.isTrue(saveReviewedStub.lastCall.calledWithExactly(true)); + const callCount = saveReviewedStub.callCount; + + element.set('params.view', Gerrit.Nav.View.CHANGE); + flushAsynchronousOperations(); + + // saveReviewedState observer observes params, but should not fire when + // view !== Gerrit.Nav.View.DIFF. + assert.equal(saveReviewedStub.callCount, callCount); + }); + + test('file review status with edit loaded', () => { + const saveReviewedStub = sandbox.stub(element, '_saveReviewedState'); + + element._patchRange = {patchNum: element.EDIT_NAME}; + flushAsynchronousOperations(); + + assert.isTrue(element._editMode); + element._setReviewed(); + assert.isFalse(saveReviewedStub.called); + }); + + test('hash is determined from params', done => { + sandbox.stub(element.$.diffHost, 'reload'); + sandbox.stub(element, '_initCursor'); + + element._loggedIn = true; + element.params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + patchNum: '2', + basePatchNum: '1', + path: '/COMMIT_MSG', + hash: 10, + }; + + flush(() => { + assert.isTrue(element._initCursor.calledOnce); + done(); }); + }); - test('file review status with edit loaded', () => { - const saveReviewedStub = sandbox.stub(element, '_saveReviewedState'); + test('diff mode selector correctly toggles the diff', () => { + const select = element.$.modeSelect; + const diffDisplay = element.$.diffHost; + element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; - element._patchRange = {patchNum: element.EDIT_NAME}; - flushAsynchronousOperations(); + // The mode selected in the view state reflects the selected option. + assert.equal(element._getDiffViewMode(), select.mode); - assert.isTrue(element._editMode); - element._setReviewed(); - assert.isFalse(saveReviewedStub.called); + // The mode selected in the view state reflects the view rednered in the + // diff. + assert.equal(select.mode, diffDisplay.viewMode); + + // We will simulate a user change of the selected mode. + const newMode = 'UNIFIED_DIFF'; + + // Set the mode, and simulate the change event. + element.set('changeViewState.diffMode', newMode); + + // Make sure the handler was called and the state is still coherent. + assert.equal(element._getDiffViewMode(), newMode); + assert.equal(element._getDiffViewMode(), select.mode); + assert.equal(element._getDiffViewMode(), diffDisplay.viewMode); + }); + + test('diff mode selector initializes from preferences', () => { + let resolvePrefs; + const prefsPromise = new Promise(resolve => { + resolvePrefs = resolve; }); + sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise); - test('hash is determined from params', done => { + // Attach a new gr-diff-view so we can intercept the preferences fetch. + const view = document.createElement('gr-diff-view'); + fixture('blank').appendChild(view); + flushAsynchronousOperations(); + + // At this point the diff mode doesn't yet have the user's preference. + assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE'); + + // Receive the overriding preference. + resolvePrefs({default_diff_view: 'UNIFIED'}); + flushAsynchronousOperations(); + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + }); + + suite('_commitRange', () => { + setup(() => { sandbox.stub(element.$.diffHost, 'reload'); sandbox.stub(element, '_initCursor'); + sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({ + _number: 42, + revisions: { + 'commit-sha-1': { + _number: 1, + commit: { + parents: [{commit: 'sha-1-parent'}], + }, + }, + 'commit-sha-2': {_number: 2}, + 'commit-sha-3': {_number: 3}, + 'commit-sha-4': {_number: 4}, + 'commit-sha-5': { + _number: 5, + commit: { + parents: [{commit: 'sha-5-parent'}], + }, + }, + }, + })); + }); - element._loggedIn = true; + test('uses the patchNum and basePatchNum ', done => { element.params = { view: Gerrit.Nav.View.DIFF, changeNum: '42', - patchNum: '2', - basePatchNum: '1', + patchNum: '4', + basePatchNum: '2', path: '/COMMIT_MSG', - hash: 10, }; - flush(() => { - assert.isTrue(element._initCursor.calledOnce); + assert.deepEqual(element._commitRange, { + baseCommit: 'commit-sha-2', + commit: 'commit-sha-4', + }); done(); }); }); - test('diff mode selector correctly toggles the diff', () => { - const select = element.$.modeSelect; - const diffDisplay = element.$.diffHost; - element._userPrefs = {default_diff_view: 'SIDE_BY_SIDE'}; - - // The mode selected in the view state reflects the selected option. - assert.equal(element._getDiffViewMode(), select.mode); - - // The mode selected in the view state reflects the view rednered in the - // diff. - assert.equal(select.mode, diffDisplay.viewMode); - - // We will simulate a user change of the selected mode. - const newMode = 'UNIFIED_DIFF'; - - // Set the mode, and simulate the change event. - element.set('changeViewState.diffMode', newMode); - - // Make sure the handler was called and the state is still coherent. - assert.equal(element._getDiffViewMode(), newMode); - assert.equal(element._getDiffViewMode(), select.mode); - assert.equal(element._getDiffViewMode(), diffDisplay.viewMode); - }); - - test('diff mode selector initializes from preferences', () => { - let resolvePrefs; - const prefsPromise = new Promise(resolve => { - resolvePrefs = resolve; - }); - sandbox.stub(element.$.restAPI, 'getPreferences', () => prefsPromise); - - // Attach a new gr-diff-view so we can intercept the preferences fetch. - const view = document.createElement('gr-diff-view'); - fixture('blank').appendChild(view); - flushAsynchronousOperations(); - - // At this point the diff mode doesn't yet have the user's preference. - assert.equal(view._getDiffViewMode(), 'SIDE_BY_SIDE'); - - // Receive the overriding preference. - resolvePrefs({default_diff_view: 'UNIFIED'}); - flushAsynchronousOperations(); - assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); - }); - - suite('_commitRange', () => { - setup(() => { - sandbox.stub(element.$.diffHost, 'reload'); - sandbox.stub(element, '_initCursor'); - sandbox.stub(element, '_getChangeDetail').returns(Promise.resolve({ - _number: 42, - revisions: { - 'commit-sha-1': { - _number: 1, - commit: { - parents: [{commit: 'sha-1-parent'}], - }, - }, - 'commit-sha-2': {_number: 2}, - 'commit-sha-3': {_number: 3}, - 'commit-sha-4': {_number: 4}, - 'commit-sha-5': { - _number: 5, - commit: { - parents: [{commit: 'sha-5-parent'}], - }, - }, - }, - })); - }); - - test('uses the patchNum and basePatchNum ', done => { - element.params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - patchNum: '4', - basePatchNum: '2', - path: '/COMMIT_MSG', - }; - flush(() => { - assert.deepEqual(element._commitRange, { - baseCommit: 'commit-sha-2', - commit: 'commit-sha-4', - }); - done(); + test('uses the parent when there is no base patch num ', done => { + element.params = { + view: Gerrit.Nav.View.DIFF, + changeNum: '42', + patchNum: '5', + path: '/COMMIT_MSG', + }; + flush(() => { + assert.deepEqual(element._commitRange, { + commit: 'commit-sha-5', + baseCommit: 'sha-5-parent', }); + done(); }); + }); + }); - test('uses the parent when there is no base patch num ', done => { - element.params = { - view: Gerrit.Nav.View.DIFF, - changeNum: '42', - patchNum: '5', - path: '/COMMIT_MSG', - }; - flush(() => { - assert.deepEqual(element._commitRange, { - commit: 'commit-sha-5', - baseCommit: 'sha-5-parent', - }); - done(); - }); + test('_initCursor', () => { + assert.isNotOk(element.$.cursor.initialLineNumber); + + // Does nothing when params specify no cursor address: + element._initCursor({}); + assert.isNotOk(element.$.cursor.initialLineNumber); + + // Does nothing when params specify side but no number: + element._initCursor({leftSide: true}); + assert.isNotOk(element.$.cursor.initialLineNumber); + + // Revision hash: specifies lineNum but not side. + element._initCursor({lineNum: 234}); + assert.equal(element.$.cursor.initialLineNumber, 234); + assert.equal(element.$.cursor.side, 'right'); + + // Base hash: specifies lineNum and side. + element._initCursor({leftSide: true, lineNum: 345}); + assert.equal(element.$.cursor.initialLineNumber, 345); + assert.equal(element.$.cursor.side, 'left'); + + // Specifies right side: + element._initCursor({leftSide: false, lineNum: 123}); + assert.equal(element.$.cursor.initialLineNumber, 123); + assert.equal(element.$.cursor.side, 'right'); + }); + + test('_getLineOfInterest', () => { + assert.isNull(element._getLineOfInterest({})); + + let result = element._getLineOfInterest({lineNum: 12}); + assert.equal(result.number, 12); + assert.isNotOk(result.leftSide); + + result = element._getLineOfInterest({lineNum: 12, leftSide: true}); + assert.equal(result.number, 12); + assert.isOk(result.leftSide); + }); + + test('_onLineSelected', () => { + const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); + const replaceStateStub = sandbox.stub(history, 'replaceState'); + const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber'); + sandbox.stub(element.$.cursor, 'getAddress') + .returns({number: 123, isLeftSide: false}); + + element._changeNum = 321; + element._change = {_number: 321, project: 'foo/bar'}; + element._patchRange = { + basePatchNum: '3', + patchNum: '5', + }; + const e = {}; + const detail = {number: 123, side: 'right'}; + + element._onLineSelected(e, detail); + + assert.isTrue(moveStub.called); + assert.equal(moveStub.lastCall.args[0], detail.number); + assert.equal(moveStub.lastCall.args[1], detail.side); + + assert.isTrue(replaceStateStub.called); + assert.isTrue(getUrlStub.called); + }); + + test('_onLineSelected w/o line address', () => { + const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); + sandbox.stub(history, 'replaceState'); + sandbox.stub(element.$.cursor, 'moveToLineNumber'); + sandbox.stub(element.$.cursor, 'getAddress').returns(null); + element._changeNum = 321; + element._change = {_number: 321, project: 'foo/bar'}; + element._patchRange = {basePatchNum: '3', patchNum: '5'}; + element._onLineSelected({}, {number: 123, side: 'right'}); + assert.isTrue(getUrlStub.calledOnce); + assert.isUndefined(getUrlStub.lastCall.args[5]); + assert.isUndefined(getUrlStub.lastCall.args[6]); + }); + + test('_getDiffViewMode', () => { + // No user prefs or change view state set. + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + + // User prefs but no change view state set. + element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'}; + assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF'); + + // User prefs and change view state set. + element.changeViewState = {diffMode: 'SIDE_BY_SIDE'}; + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + }); + + test('_handleToggleDiffMode', () => { + sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); + const e = {preventDefault: () => {}}; + // Initial state. + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + + element._handleToggleDiffMode(e); + assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF'); + + element._handleToggleDiffMode(e); + assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); + }); + + suite('_loadComments', () => { + test('empty', done => { + element._loadComments().then(() => { + assert.equal(Object.keys(element._commentMap).length, 0); + done(); }); }); - test('_initCursor', () => { - assert.isNotOk(element.$.cursor.initialLineNumber); - - // Does nothing when params specify no cursor address: - element._initCursor({}); - assert.isNotOk(element.$.cursor.initialLineNumber); - - // Does nothing when params specify side but no number: - element._initCursor({leftSide: true}); - assert.isNotOk(element.$.cursor.initialLineNumber); - - // Revision hash: specifies lineNum but not side. - element._initCursor({lineNum: 234}); - assert.equal(element.$.cursor.initialLineNumber, 234); - assert.equal(element.$.cursor.side, 'right'); - - // Base hash: specifies lineNum and side. - element._initCursor({leftSide: true, lineNum: 345}); - assert.equal(element.$.cursor.initialLineNumber, 345); - assert.equal(element.$.cursor.side, 'left'); - - // Specifies right side: - element._initCursor({leftSide: false, lineNum: 123}); - assert.equal(element.$.cursor.initialLineNumber, 123); - assert.equal(element.$.cursor.side, 'right'); - }); - - test('_getLineOfInterest', () => { - assert.isNull(element._getLineOfInterest({})); - - let result = element._getLineOfInterest({lineNum: 12}); - assert.equal(result.number, 12); - assert.isNotOk(result.leftSide); - - result = element._getLineOfInterest({lineNum: 12, leftSide: true}); - assert.equal(result.number, 12); - assert.isOk(result.leftSide); - }); - - test('_onLineSelected', () => { - const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); - const replaceStateStub = sandbox.stub(history, 'replaceState'); - const moveStub = sandbox.stub(element.$.cursor, 'moveToLineNumber'); - sandbox.stub(element.$.cursor, 'getAddress') - .returns({number: 123, isLeftSide: false}); - - element._changeNum = 321; - element._change = {_number: 321, project: 'foo/bar'}; + test('has paths', done => { + sandbox.stub(element, '_getPaths').returns({ + 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}], + 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}], + }); + sandbox.stub(element, '_getCommentsForPath').returns({meta: {}}); + element._changeNum = '42'; element._patchRange = { basePatchNum: '3', patchNum: '5', }; - const e = {}; - const detail = {number: 123, side: 'right'}; - - element._onLineSelected(e, detail); - - assert.isTrue(moveStub.called); - assert.equal(moveStub.lastCall.args[0], detail.number); - assert.equal(moveStub.lastCall.args[1], detail.side); - - assert.isTrue(replaceStateStub.called); - assert.isTrue(getUrlStub.called); - }); - - test('_onLineSelected w/o line address', () => { - const getUrlStub = sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); - sandbox.stub(history, 'replaceState'); - sandbox.stub(element.$.cursor, 'moveToLineNumber'); - sandbox.stub(element.$.cursor, 'getAddress').returns(null); - element._changeNum = 321; - element._change = {_number: 321, project: 'foo/bar'}; - element._patchRange = {basePatchNum: '3', patchNum: '5'}; - element._onLineSelected({}, {number: 123, side: 'right'}); - assert.isTrue(getUrlStub.calledOnce); - assert.isUndefined(getUrlStub.lastCall.args[5]); - assert.isUndefined(getUrlStub.lastCall.args[6]); - }); - - test('_getDiffViewMode', () => { - // No user prefs or change view state set. - assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); - - // User prefs but no change view state set. - element._userPrefs = {default_diff_view: 'UNIFIED_DIFF'}; - assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF'); - - // User prefs and change view state set. - element.changeViewState = {diffMode: 'SIDE_BY_SIDE'}; - assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); - }); - - test('_handleToggleDiffMode', () => { - sandbox.stub(element, 'shouldSuppressKeyboardShortcut').returns(false); - const e = {preventDefault: () => {}}; - // Initial state. - assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); - - element._handleToggleDiffMode(e); - assert.equal(element._getDiffViewMode(), 'UNIFIED_DIFF'); - - element._handleToggleDiffMode(e); - assert.equal(element._getDiffViewMode(), 'SIDE_BY_SIDE'); - }); - - suite('_loadComments', () => { - test('empty', done => { - element._loadComments().then(() => { - assert.equal(Object.keys(element._commentMap).length, 0); - done(); - }); - }); - - test('has paths', done => { - sandbox.stub(element, '_getPaths').returns({ - 'path/to/file/one.cpp': [{patch_set: 3, message: 'lorem'}], - 'path-to/file/two.py': [{patch_set: 5, message: 'ipsum'}], - }); - sandbox.stub(element, '_getCommentsForPath').returns({meta: {}}); - element._changeNum = '42'; - element._patchRange = { - basePatchNum: '3', - patchNum: '5', - }; - element._loadComments().then(() => { - assert.deepEqual(Object.keys(element._commentMap), - ['path/to/file/one.cpp', 'path-to/file/two.py']); - done(); - }); + element._loadComments().then(() => { + assert.deepEqual(Object.keys(element._commentMap), + ['path/to/file/one.cpp', 'path-to/file/two.py']); + done(); }); }); + }); - suite('_computeCommentSkips', () => { - test('empty file list', () => { - const commentMap = { - 'path/one.jpg': true, - 'path/three.wav': true, - }; - const path = 'path/two.m4v'; - const fileList = []; - const result = element._computeCommentSkips(commentMap, fileList, path); - assert.isNull(result.previous); - assert.isNull(result.next); - }); - - test('finds skips', () => { - const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; - let path = fileList[1]; - const commentMap = {}; - commentMap[fileList[0]] = true; - commentMap[fileList[1]] = false; - commentMap[fileList[2]] = true; - - let result = element._computeCommentSkips(commentMap, fileList, path); - assert.equal(result.previous, fileList[0]); - assert.equal(result.next, fileList[2]); - - commentMap[fileList[1]] = true; - - result = element._computeCommentSkips(commentMap, fileList, path); - assert.equal(result.previous, fileList[0]); - assert.equal(result.next, fileList[2]); - - path = fileList[0]; - - result = element._computeCommentSkips(commentMap, fileList, path); - assert.isNull(result.previous); - assert.equal(result.next, fileList[1]); - - path = fileList[2]; - - result = element._computeCommentSkips(commentMap, fileList, path); - assert.equal(result.previous, fileList[1]); - assert.isNull(result.next); - }); - - suite('skip next/previous', () => { - let navToChangeStub; - let navToDiffStub; - - setup(() => { - navToChangeStub = sandbox.stub(element, '_navToChangeView'); - navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - element._files = getFilesFromFileList([ - 'path/one.jpg', 'path/two.m4v', 'path/three.wav', - ]); - element._patchRange = {patchNum: '2', basePatchNum: '1'}; - }); - - suite('_moveToPreviousFileWithComment', () => { - test('no skips', () => { - element._moveToPreviousFileWithComment(); - assert.isFalse(navToChangeStub.called); - assert.isFalse(navToDiffStub.called); - }); - - test('no previous', () => { - const commentMap = {}; - commentMap[element._fileList[0]] = false; - commentMap[element._fileList[1]] = false; - commentMap[element._fileList[2]] = true; - element._commentMap = commentMap; - element._path = element._fileList[1]; - - element._moveToPreviousFileWithComment(); - assert.isTrue(navToChangeStub.calledOnce); - assert.isFalse(navToDiffStub.called); - }); - - test('w/ previous', () => { - const commentMap = {}; - commentMap[element._fileList[0]] = true; - commentMap[element._fileList[1]] = false; - commentMap[element._fileList[2]] = true; - element._commentMap = commentMap; - element._path = element._fileList[1]; - - element._moveToPreviousFileWithComment(); - assert.isFalse(navToChangeStub.called); - assert.isTrue(navToDiffStub.calledOnce); - }); - }); - - suite('_moveToNextFileWithComment', () => { - test('no skips', () => { - element._moveToNextFileWithComment(); - assert.isFalse(navToChangeStub.called); - assert.isFalse(navToDiffStub.called); - }); - - test('no previous', () => { - const commentMap = {}; - commentMap[element._fileList[0]] = true; - commentMap[element._fileList[1]] = false; - commentMap[element._fileList[2]] = false; - element._commentMap = commentMap; - element._path = element._fileList[1]; - - element._moveToNextFileWithComment(); - assert.isTrue(navToChangeStub.calledOnce); - assert.isFalse(navToDiffStub.called); - }); - - test('w/ previous', () => { - const commentMap = {}; - commentMap[element._fileList[0]] = true; - commentMap[element._fileList[1]] = false; - commentMap[element._fileList[2]] = true; - element._commentMap = commentMap; - element._path = element._fileList[1]; - - element._moveToNextFileWithComment(); - assert.isFalse(navToChangeStub.called); - assert.isTrue(navToDiffStub.calledOnce); - }); - }); - }); - }); - - test('_computeEditMode', () => { - const callCompute = range => element._computeEditMode({base: range}); - assert.isFalse(callCompute({})); - assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1})); - assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1})); - assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'})); - }); - - test('_computeFileNum', () => { - assert.equal(element._computeFileNum('/foo', - [{value: '/foo'}, {value: '/bar'}]), 1); - assert.equal(element._computeFileNum('/bar', - [{value: '/foo'}, {value: '/bar'}]), 2); - }); - - test('_computeFileNumClass', () => { - assert.equal(element._computeFileNumClass(0, []), ''); - assert.equal(element._computeFileNumClass(1, - [{value: '/foo'}, {value: '/bar'}]), 'show'); - }); - - test('_getReviewedStatus', () => { - const promises = []; - element.$.restAPI.getReviewedFiles.restore(); - - sandbox.stub(element.$.restAPI, 'getReviewedFiles') - .returns(Promise.resolve(['path'])); - - promises.push(element._getReviewedStatus(true, null, null, 'path') - .then(reviewed => assert.isFalse(reviewed))); - - promises.push(element._getReviewedStatus(false, null, null, 'otherPath') - .then(reviewed => assert.isFalse(reviewed))); - - promises.push(element._getReviewedStatus(false, null, null, 'path') - .then(reviewed => assert.isTrue(reviewed))); - - return Promise.all(promises); - }); - - suite('blame', () => { - test('toggle blame with button', () => { - const toggleBlame = sandbox.stub( - element.$.diffHost, 'loadBlame', () => Promise.resolve()); - MockInteractions.tap(element.$.toggleBlame); - assert.isTrue(toggleBlame.calledOnce); - }); - test('toggle blame with shortcut', () => { - const toggleBlame = sandbox.stub( - element.$.diffHost, 'loadBlame', () => Promise.resolve()); - MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b'); - assert.isTrue(toggleBlame.calledOnce); - }); - }); - - suite('editMode behavior', () => { - setup(() => { - element._loggedIn = true; - }); - - const isVisible = el => { - assert.ok(el); - return getComputedStyle(el).getPropertyValue('display') !== 'none'; + suite('_computeCommentSkips', () => { + test('empty file list', () => { + const commentMap = { + 'path/one.jpg': true, + 'path/three.wav': true, }; - - test('reviewed checkbox', () => { - sandbox.stub(element, '_handlePatchChange'); - element._patchRange = {patchNum: '1'}; - // Reviewed checkbox should be shown. - assert.isTrue(isVisible(element.$.reviewed)); - element.set('_patchRange.patchNum', element.EDIT_NAME); - flushAsynchronousOperations(); - - assert.isFalse(isVisible(element.$.reviewed)); - }); + const path = 'path/two.m4v'; + const fileList = []; + const result = element._computeCommentSkips(commentMap, fileList, path); + assert.isNull(result.previous); + assert.isNull(result.next); }); - test('_paramsChanged sets in projectLookup', () => { - sandbox.stub(element, '_getLineOfInterest'); - sandbox.stub(element, '_initCursor'); - const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup'); - element._paramsChanged({ - view: Gerrit.Nav.View.DIFF, - changeNum: 101, - project: 'test-project', - path: '', - }); - assert.isTrue(setStub.calledOnce); - assert.isTrue(setStub.calledWith(101, 'test-project')); + test('finds skips', () => { + const fileList = ['path/one.jpg', 'path/two.m4v', 'path/three.wav']; + let path = fileList[1]; + const commentMap = {}; + commentMap[fileList[0]] = true; + commentMap[fileList[1]] = false; + commentMap[fileList[2]] = true; + + let result = element._computeCommentSkips(commentMap, fileList, path); + assert.equal(result.previous, fileList[0]); + assert.equal(result.next, fileList[2]); + + commentMap[fileList[1]] = true; + + result = element._computeCommentSkips(commentMap, fileList, path); + assert.equal(result.previous, fileList[0]); + assert.equal(result.next, fileList[2]); + + path = fileList[0]; + + result = element._computeCommentSkips(commentMap, fileList, path); + assert.isNull(result.previous); + assert.equal(result.next, fileList[1]); + + path = fileList[2]; + + result = element._computeCommentSkips(commentMap, fileList, path); + assert.equal(result.previous, fileList[1]); + assert.isNull(result.next); }); - test('shift+m navigates to next unreviewed file', () => { - element._files = getFilesFromFileList(['file1', 'file2', 'file3']); - element._reviewedFiles = new Set(['file1', 'file2']); - element._path = 'file1'; - const reviewedStub = sandbox.stub(element, '_setReviewed'); - const navStub = sandbox.stub(element, '_navToFile'); - MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm'); + suite('skip next/previous', () => { + let navToChangeStub; + let navToDiffStub; + + setup(() => { + navToChangeStub = sandbox.stub(element, '_navToChangeView'); + navToDiffStub = sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + element._files = getFilesFromFileList([ + 'path/one.jpg', 'path/two.m4v', 'path/three.wav', + ]); + element._patchRange = {patchNum: '2', basePatchNum: '1'}; + }); + + suite('_moveToPreviousFileWithComment', () => { + test('no skips', () => { + element._moveToPreviousFileWithComment(); + assert.isFalse(navToChangeStub.called); + assert.isFalse(navToDiffStub.called); + }); + + test('no previous', () => { + const commentMap = {}; + commentMap[element._fileList[0]] = false; + commentMap[element._fileList[1]] = false; + commentMap[element._fileList[2]] = true; + element._commentMap = commentMap; + element._path = element._fileList[1]; + + element._moveToPreviousFileWithComment(); + assert.isTrue(navToChangeStub.calledOnce); + assert.isFalse(navToDiffStub.called); + }); + + test('w/ previous', () => { + const commentMap = {}; + commentMap[element._fileList[0]] = true; + commentMap[element._fileList[1]] = false; + commentMap[element._fileList[2]] = true; + element._commentMap = commentMap; + element._path = element._fileList[1]; + + element._moveToPreviousFileWithComment(); + assert.isFalse(navToChangeStub.called); + assert.isTrue(navToDiffStub.calledOnce); + }); + }); + + suite('_moveToNextFileWithComment', () => { + test('no skips', () => { + element._moveToNextFileWithComment(); + assert.isFalse(navToChangeStub.called); + assert.isFalse(navToDiffStub.called); + }); + + test('no previous', () => { + const commentMap = {}; + commentMap[element._fileList[0]] = true; + commentMap[element._fileList[1]] = false; + commentMap[element._fileList[2]] = false; + element._commentMap = commentMap; + element._path = element._fileList[1]; + + element._moveToNextFileWithComment(); + assert.isTrue(navToChangeStub.calledOnce); + assert.isFalse(navToDiffStub.called); + }); + + test('w/ previous', () => { + const commentMap = {}; + commentMap[element._fileList[0]] = true; + commentMap[element._fileList[1]] = false; + commentMap[element._fileList[2]] = true; + element._commentMap = commentMap; + element._path = element._fileList[1]; + + element._moveToNextFileWithComment(); + assert.isFalse(navToChangeStub.called); + assert.isTrue(navToDiffStub.calledOnce); + }); + }); + }); + }); + + test('_computeEditMode', () => { + const callCompute = range => element._computeEditMode({base: range}); + assert.isFalse(callCompute({})); + assert.isFalse(callCompute({basePatchNum: 'PARENT', patchNum: 1})); + assert.isFalse(callCompute({basePatchNum: 'edit', patchNum: 1})); + assert.isTrue(callCompute({basePatchNum: 1, patchNum: 'edit'})); + }); + + test('_computeFileNum', () => { + assert.equal(element._computeFileNum('/foo', + [{value: '/foo'}, {value: '/bar'}]), 1); + assert.equal(element._computeFileNum('/bar', + [{value: '/foo'}, {value: '/bar'}]), 2); + }); + + test('_computeFileNumClass', () => { + assert.equal(element._computeFileNumClass(0, []), ''); + assert.equal(element._computeFileNumClass(1, + [{value: '/foo'}, {value: '/bar'}]), 'show'); + }); + + test('_getReviewedStatus', () => { + const promises = []; + element.$.restAPI.getReviewedFiles.restore(); + + sandbox.stub(element.$.restAPI, 'getReviewedFiles') + .returns(Promise.resolve(['path'])); + + promises.push(element._getReviewedStatus(true, null, null, 'path') + .then(reviewed => assert.isFalse(reviewed))); + + promises.push(element._getReviewedStatus(false, null, null, 'otherPath') + .then(reviewed => assert.isFalse(reviewed))); + + promises.push(element._getReviewedStatus(false, null, null, 'path') + .then(reviewed => assert.isTrue(reviewed))); + + return Promise.all(promises); + }); + + suite('blame', () => { + test('toggle blame with button', () => { + const toggleBlame = sandbox.stub( + element.$.diffHost, 'loadBlame', () => Promise.resolve()); + MockInteractions.tap(element.$.toggleBlame); + assert.isTrue(toggleBlame.calledOnce); + }); + test('toggle blame with shortcut', () => { + const toggleBlame = sandbox.stub( + element.$.diffHost, 'loadBlame', () => Promise.resolve()); + MockInteractions.pressAndReleaseKeyOn(element, 66, null, 'b'); + assert.isTrue(toggleBlame.calledOnce); + }); + }); + + suite('editMode behavior', () => { + setup(() => { + element._loggedIn = true; + }); + + const isVisible = el => { + assert.ok(el); + return getComputedStyle(el).getPropertyValue('display') !== 'none'; + }; + + test('reviewed checkbox', () => { + sandbox.stub(element, '_handlePatchChange'); + element._patchRange = {patchNum: '1'}; + // Reviewed checkbox should be shown. + assert.isTrue(isVisible(element.$.reviewed)); + element.set('_patchRange.patchNum', element.EDIT_NAME); flushAsynchronousOperations(); - assert.isTrue(reviewedStub.lastCall.args[0]); - assert.deepEqual(navStub.lastCall.args, [ - 'file1', - ['file1', 'file3'], - 1, - ]); - }); - - test('File change should trigger navigateToDiff once', () => { - element._files = getFilesFromFileList(['file1', 'file2', 'file3']); - sandbox.stub(element, '_getLineOfInterest'); - sandbox.stub(element, '_initCursor'); - sandbox.stub(Gerrit.Nav, 'navigateToDiff'); - - // Load file1 - element._paramsChanged({ - view: Gerrit.Nav.View.DIFF, - patchNum: 1, - changeNum: 101, - project: 'test-project', - path: 'file1', - }); - assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled); - - // Switch to file2 - element.$.dropdown.value = 'file2'; - assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce); - - // This is to mock the param change triggered by above navigate - element._paramsChanged({ - view: Gerrit.Nav.View.DIFF, - patchNum: 1, - changeNum: 101, - project: 'test-project', - path: 'file2', - }); - - // No extra call - assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce); - }); - - test('_computeDownloadDropdownLinks', () => { - const downloadLinks = [ - { - url: '/changes/test~12/revisions/1/patch?zip&path=index.php', - name: 'Patch', - }, - { - url: '/changes/test~12/revisions/1' + - '/files/index.php/download?parent=1', - name: 'Left Content', - }, - { - url: '/changes/test~12/revisions/1' + - '/files/index.php/download', - name: 'Right Content', - }, - ]; - - const side = { - meta_a: true, - meta_b: true, - }; - - const base = { - patchNum: 1, - basePatchNum: 'PARENT', - }; - - assert.deepEqual( - element._computeDownloadDropdownLinks( - 'test', 12, base, 'index.php', side), - downloadLinks); - }); - - test('_computeDownloadDropdownLinks diff returns renamed', () => { - const downloadLinks = [ - { - url: '/changes/test~12/revisions/3/patch?zip&path=index.php', - name: 'Patch', - }, - { - url: '/changes/test~12/revisions/2' + - '/files/index2.php/download', - name: 'Left Content', - }, - { - url: '/changes/test~12/revisions/3' + - '/files/index.php/download', - name: 'Right Content', - }, - ]; - - const side = { - change_type: 'RENAMED', - meta_a: { - name: 'index2.php', - }, - meta_b: true, - }; - - const base = { - patchNum: 3, - basePatchNum: 2, - }; - - assert.deepEqual( - element._computeDownloadDropdownLinks( - 'test', 12, base, 'index.php', side), - downloadLinks); - }); - - test('_computeDownloadFileLink', () => { - const base = { - patchNum: 1, - basePatchNum: 'PARENT', - }; - - assert.equal( - element._computeDownloadFileLink( - 'test', 12, base, 'index.php', true), - '/changes/test~12/revisions/1/files/index.php/download?parent=1'); - - assert.equal( - element._computeDownloadFileLink( - 'test', 12, base, 'index.php', false), - '/changes/test~12/revisions/1/files/index.php/download'); - }); - - test('_computeDownloadPatchLink', () => { - assert.equal( - element._computeDownloadPatchLink( - 'test', 12, {patchNum: 1}, 'index.php'), - '/changes/test~12/revisions/1/patch?zip&path=index.php'); + assert.isFalse(isVisible(element.$.reviewed)); }); }); - suite('gr-diff-view tests unmodified files with comments', () => { - let sandbox; - let element; - setup(() => { - sandbox = sinon.sandbox.create(); - const changedFiles = { - 'file1.txt': {}, - 'a/b/test.c': {}, - }; - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({change: {}}); }, - getLoggedIn() { return Promise.resolve(false); }, - getProjectConfig() { return Promise.resolve({}); }, - getDiffChangeDetail() { return Promise.resolve({}); }, - getChangeFiles() { return Promise.resolve(changedFiles); }, - saveFileReviewed() { return Promise.resolve(); }, - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, - getReviewedFiles() { return Promise.resolve([]); }, - }); - element = fixture('basic'); - return element._loadComments(); + test('_paramsChanged sets in projectLookup', () => { + sandbox.stub(element, '_getLineOfInterest'); + sandbox.stub(element, '_initCursor'); + const setStub = sandbox.stub(element.$.restAPI, 'setInProjectLookup'); + element._paramsChanged({ + view: Gerrit.Nav.View.DIFF, + changeNum: 101, + project: 'test-project', + path: '', + }); + assert.isTrue(setStub.calledOnce); + assert.isTrue(setStub.calledWith(101, 'test-project')); + }); + + test('shift+m navigates to next unreviewed file', () => { + element._files = getFilesFromFileList(['file1', 'file2', 'file3']); + element._reviewedFiles = new Set(['file1', 'file2']); + element._path = 'file1'; + const reviewedStub = sandbox.stub(element, '_setReviewed'); + const navStub = sandbox.stub(element, '_navToFile'); + MockInteractions.pressAndReleaseKeyOn(element, 77, 'shift', 'm'); + flushAsynchronousOperations(); + + assert.isTrue(reviewedStub.lastCall.args[0]); + assert.deepEqual(navStub.lastCall.args, [ + 'file1', + ['file1', 'file3'], + 1, + ]); + }); + + test('File change should trigger navigateToDiff once', () => { + element._files = getFilesFromFileList(['file1', 'file2', 'file3']); + sandbox.stub(element, '_getLineOfInterest'); + sandbox.stub(element, '_initCursor'); + sandbox.stub(Gerrit.Nav, 'navigateToDiff'); + + // Load file1 + element._paramsChanged({ + view: Gerrit.Nav.View.DIFF, + patchNum: 1, + changeNum: 101, + project: 'test-project', + path: 'file1', + }); + assert.isTrue(Gerrit.Nav.navigateToDiff.notCalled); + + // Switch to file2 + element.$.dropdown.value = 'file2'; + assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce); + + // This is to mock the param change triggered by above navigate + element._paramsChanged({ + view: Gerrit.Nav.View.DIFF, + patchNum: 1, + changeNum: 101, + project: 'test-project', + path: 'file2', }); - teardown(() => { - sandbox.restore(); - }); + // No extra call + assert.isTrue(Gerrit.Nav.navigateToDiff.calledOnce); + }); - test('_getFiles add files with comments without changes', () => { - const patchChangeRecord = { - base: { - basePatchNum: '5', - patchNum: '10', - }, - }; - const changeComments = { - getPaths: sandbox.stub().returns({ - 'file2.txt': {}, - 'file1.txt': {}, - }), - }; - return element._getFiles(23, patchChangeRecord, changeComments) - .then(() => { - assert.deepEqual(element._files, { - sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'], - changeFilesByPath: { - 'file1.txt': {}, - 'file2.txt': {status: 'U'}, - 'a/b/test.c': {}, - }, - }); - }); - }); + test('_computeDownloadDropdownLinks', () => { + const downloadLinks = [ + { + url: '/changes/test~12/revisions/1/patch?zip&path=index.php', + name: 'Patch', + }, + { + url: '/changes/test~12/revisions/1' + + '/files/index.php/download?parent=1', + name: 'Left Content', + }, + { + url: '/changes/test~12/revisions/1' + + '/files/index.php/download', + name: 'Right Content', + }, + ]; + + const side = { + meta_a: true, + meta_b: true, + }; + + const base = { + patchNum: 1, + basePatchNum: 'PARENT', + }; + + assert.deepEqual( + element._computeDownloadDropdownLinks( + 'test', 12, base, 'index.php', side), + downloadLinks); + }); + + test('_computeDownloadDropdownLinks diff returns renamed', () => { + const downloadLinks = [ + { + url: '/changes/test~12/revisions/3/patch?zip&path=index.php', + name: 'Patch', + }, + { + url: '/changes/test~12/revisions/2' + + '/files/index2.php/download', + name: 'Left Content', + }, + { + url: '/changes/test~12/revisions/3' + + '/files/index.php/download', + name: 'Right Content', + }, + ]; + + const side = { + change_type: 'RENAMED', + meta_a: { + name: 'index2.php', + }, + meta_b: true, + }; + + const base = { + patchNum: 3, + basePatchNum: 2, + }; + + assert.deepEqual( + element._computeDownloadDropdownLinks( + 'test', 12, base, 'index.php', side), + downloadLinks); + }); + + test('_computeDownloadFileLink', () => { + const base = { + patchNum: 1, + basePatchNum: 'PARENT', + }; + + assert.equal( + element._computeDownloadFileLink( + 'test', 12, base, 'index.php', true), + '/changes/test~12/revisions/1/files/index.php/download?parent=1'); + + assert.equal( + element._computeDownloadFileLink( + 'test', 12, base, 'index.php', false), + '/changes/test~12/revisions/1/files/index.php/download'); + }); + + test('_computeDownloadPatchLink', () => { + assert.equal( + element._computeDownloadPatchLink( + 'test', 12, {patchNum: 1}, 'index.php'), + '/changes/test~12/revisions/1/patch?zip&path=index.php'); }); }); + + suite('gr-diff-view tests unmodified files with comments', () => { + let sandbox; + let element; + setup(() => { + sandbox = sinon.sandbox.create(); + const changedFiles = { + 'file1.txt': {}, + 'a/b/test.c': {}, + }; + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({change: {}}); }, + getLoggedIn() { return Promise.resolve(false); }, + getProjectConfig() { return Promise.resolve({}); }, + getDiffChangeDetail() { return Promise.resolve({}); }, + getChangeFiles() { return Promise.resolve(changedFiles); }, + saveFileReviewed() { return Promise.resolve(); }, + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + getReviewedFiles() { return Promise.resolve([]); }, + }); + element = fixture('basic'); + return element._loadComments(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_getFiles add files with comments without changes', () => { + const patchChangeRecord = { + base: { + basePatchNum: '5', + patchNum: '10', + }, + }; + const changeComments = { + getPaths: sandbox.stub().returns({ + 'file2.txt': {}, + 'file1.txt': {}, + }), + }; + return element._getFiles(23, patchChangeRecord, changeComments) + .then(() => { + assert.deepEqual(element._files, { + sortedFileList: ['a/b/test.c', 'file1.txt', 'file2.txt'], + changeFilesByPath: { + 'file1.txt': {}, + 'file2.txt': {status: 'U'}, + 'a/b/test.c': {}, + }, + }); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html index 00907d6..c461e93 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff-group_test.html
@@ -19,193 +19,195 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-group</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="gr-diff-line.js"></script> -<script src="gr-diff-group.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-diff-line.js"></script> +<script type="module" src="./gr-diff-group.js"></script> -<script> - suite('gr-diff-group tests', async () => { - await readyToTest(); - test('delta line pairs', () => { - let group = new GrDiffGroup(GrDiffGroup.Type.DELTA); - const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128); - const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129); - const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0); - group.addLine(l1); - group.addLine(l2); - group.addLine(l3); - assert.deepEqual(group.lines, [l1, l2, l3]); - assert.deepEqual(group.adds, [l1, l2]); - assert.deepEqual(group.removes, [l3]); - assert.deepEqual(group.lineRange, { - left: {start: 64, end: 64}, - right: {start: 128, end: 129}, - }); - - let pairs = group.getSideBySidePairs(); - assert.deepEqual(pairs, [ - {left: l3, right: l1}, - {left: GrDiffLine.BLANK_LINE, right: l2}, - ]); - - group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]); - assert.deepEqual(group.lines, [l1, l2, l3]); - assert.deepEqual(group.adds, [l1, l2]); - assert.deepEqual(group.removes, [l3]); - - pairs = group.getSideBySidePairs(); - assert.deepEqual(pairs, [ - {left: l3, right: l1}, - {left: GrDiffLine.BLANK_LINE, right: l2}, - ]); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-line.js'; +import './gr-diff-group.js'; +suite('gr-diff-group tests', () => { + test('delta line pairs', () => { + let group = new GrDiffGroup(GrDiffGroup.Type.DELTA); + const l1 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 128); + const l2 = new GrDiffLine(GrDiffLine.Type.ADD, 0, 129); + const l3 = new GrDiffLine(GrDiffLine.Type.REMOVE, 64, 0); + group.addLine(l1); + group.addLine(l2); + group.addLine(l3); + assert.deepEqual(group.lines, [l1, l2, l3]); + assert.deepEqual(group.adds, [l1, l2]); + assert.deepEqual(group.removes, [l3]); + assert.deepEqual(group.lineRange, { + left: {start: 64, end: 64}, + right: {start: 128, end: 129}, }); - test('group/header line pairs', () => { - const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128); - const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129); - const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130); + let pairs = group.getSideBySidePairs(); + assert.deepEqual(pairs, [ + {left: l3, right: l1}, + {left: GrDiffLine.BLANK_LINE, right: l2}, + ]); - let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]); + group = new GrDiffGroup(GrDiffGroup.Type.DELTA, [l1, l2, l3]); + assert.deepEqual(group.lines, [l1, l2, l3]); + assert.deepEqual(group.adds, [l1, l2]); + assert.deepEqual(group.removes, [l3]); - assert.deepEqual(group.lines, [l1, l2, l3]); - assert.deepEqual(group.adds, []); - assert.deepEqual(group.removes, []); - - assert.deepEqual(group.lineRange, { - left: {start: 64, end: 66}, - right: {start: 128, end: 130}, - }); - - let pairs = group.getSideBySidePairs(); - assert.deepEqual(pairs, [ - {left: l1, right: l1}, - {left: l2, right: l2}, - {left: l3, right: l3}, - ]); - - group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]); - assert.deepEqual(group.lines, [l1, l2, l3]); - assert.deepEqual(group.adds, []); - assert.deepEqual(group.removes, []); - - pairs = group.getSideBySidePairs(); - assert.deepEqual(pairs, [ - {left: l1, right: l1}, - {left: l2, right: l2}, - {left: l3, right: l3}, - ]); - }); - - test('adding delta lines to non-delta group', () => { - const l1 = new GrDiffLine(GrDiffLine.Type.ADD); - const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE); - const l3 = new GrDiffLine(GrDiffLine.Type.BOTH); - - let group = new GrDiffGroup(GrDiffGroup.Type.BOTH); - assert.throws(group.addLine.bind(group, l1)); - assert.throws(group.addLine.bind(group, l2)); - assert.doesNotThrow(group.addLine.bind(group, l3)); - - group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL); - assert.throws(group.addLine.bind(group, l1)); - assert.throws(group.addLine.bind(group, l2)); - assert.doesNotThrow(group.addLine.bind(group, l3)); - }); - - suite('hideInContextControl', () => { - let groups; - setup(() => { - groups = [ - new GrDiffGroup(GrDiffGroup.Type.BOTH, [ - new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7), - new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8), - new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9), - ]), - new GrDiffGroup(GrDiffGroup.Type.DELTA, [ - new GrDiffLine(GrDiffLine.Type.REMOVE, 8), - new GrDiffLine(GrDiffLine.Type.ADD, 0, 10), - new GrDiffLine(GrDiffLine.Type.REMOVE, 9), - new GrDiffLine(GrDiffLine.Type.ADD, 0, 11), - new GrDiffLine(GrDiffLine.Type.REMOVE, 10), - new GrDiffLine(GrDiffLine.Type.ADD, 0, 12), - ]), - new GrDiffGroup(GrDiffGroup.Type.BOTH, [ - new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13), - new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14), - new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15), - ]), - ]; - }); - - test('hides hidden groups in context control', () => { - const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6); - assert.equal(collapsedGroups.length, 3); - - assert.equal(collapsedGroups[0], groups[0]); - - assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL); - assert.equal(collapsedGroups[1].lines.length, 1); - assert.equal( - collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL); - assert.equal( - collapsedGroups[1].lines[0].contextGroups.length, 1); - assert.equal( - collapsedGroups[1].lines[0].contextGroups[0], groups[1]); - - assert.equal(collapsedGroups[2], groups[2]); - }); - - test('splits partially hidden groups', () => { - const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7); - assert.equal(collapsedGroups.length, 4); - assert.equal(collapsedGroups[0], groups[0]); - - assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA); - assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]); - assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]); - - assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL); - assert.equal(collapsedGroups[2].lines.length, 1); - assert.equal( - collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL); - assert.equal( - collapsedGroups[2].lines[0].contextGroups.length, 2); - - assert.equal( - collapsedGroups[2].lines[0].contextGroups[0].type, - GrDiffGroup.Type.DELTA); - assert.deepEqual( - collapsedGroups[2].lines[0].contextGroups[0].adds, - groups[1].adds.slice(1)); - assert.deepEqual( - collapsedGroups[2].lines[0].contextGroups[0].removes, - groups[1].removes.slice(1)); - - assert.equal( - collapsedGroups[2].lines[0].contextGroups[1].type, - GrDiffGroup.Type.BOTH); - assert.deepEqual( - collapsedGroups[2].lines[0].contextGroups[1].lines, - [groups[2].lines[0]]); - - assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH); - assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1)); - }); - - test('groups unchanged if the hidden range is empty', () => { - assert.deepEqual( - GrDiffGroup.hideInContextControl(groups, 0, 0), groups); - }); - - test('groups unchanged if there is only 1 line to hide', () => { - assert.deepEqual( - GrDiffGroup.hideInContextControl(groups, 3, 4), groups); - }); - }); + pairs = group.getSideBySidePairs(); + assert.deepEqual(pairs, [ + {left: l3, right: l1}, + {left: GrDiffLine.BLANK_LINE, right: l2}, + ]); }); + test('group/header line pairs', () => { + const l1 = new GrDiffLine(GrDiffLine.Type.BOTH, 64, 128); + const l2 = new GrDiffLine(GrDiffLine.Type.BOTH, 65, 129); + const l3 = new GrDiffLine(GrDiffLine.Type.BOTH, 66, 130); + + let group = new GrDiffGroup(GrDiffGroup.Type.BOTH, [l1, l2, l3]); + + assert.deepEqual(group.lines, [l1, l2, l3]); + assert.deepEqual(group.adds, []); + assert.deepEqual(group.removes, []); + + assert.deepEqual(group.lineRange, { + left: {start: 64, end: 66}, + right: {start: 128, end: 130}, + }); + + let pairs = group.getSideBySidePairs(); + assert.deepEqual(pairs, [ + {left: l1, right: l1}, + {left: l2, right: l2}, + {left: l3, right: l3}, + ]); + + group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL, [l1, l2, l3]); + assert.deepEqual(group.lines, [l1, l2, l3]); + assert.deepEqual(group.adds, []); + assert.deepEqual(group.removes, []); + + pairs = group.getSideBySidePairs(); + assert.deepEqual(pairs, [ + {left: l1, right: l1}, + {left: l2, right: l2}, + {left: l3, right: l3}, + ]); + }); + + test('adding delta lines to non-delta group', () => { + const l1 = new GrDiffLine(GrDiffLine.Type.ADD); + const l2 = new GrDiffLine(GrDiffLine.Type.REMOVE); + const l3 = new GrDiffLine(GrDiffLine.Type.BOTH); + + let group = new GrDiffGroup(GrDiffGroup.Type.BOTH); + assert.throws(group.addLine.bind(group, l1)); + assert.throws(group.addLine.bind(group, l2)); + assert.doesNotThrow(group.addLine.bind(group, l3)); + + group = new GrDiffGroup(GrDiffGroup.Type.CONTEXT_CONTROL); + assert.throws(group.addLine.bind(group, l1)); + assert.throws(group.addLine.bind(group, l2)); + assert.doesNotThrow(group.addLine.bind(group, l3)); + }); + + suite('hideInContextControl', () => { + let groups; + setup(() => { + groups = [ + new GrDiffGroup(GrDiffGroup.Type.BOTH, [ + new GrDiffLine(GrDiffLine.Type.BOTH, 5, 7), + new GrDiffLine(GrDiffLine.Type.BOTH, 6, 8), + new GrDiffLine(GrDiffLine.Type.BOTH, 7, 9), + ]), + new GrDiffGroup(GrDiffGroup.Type.DELTA, [ + new GrDiffLine(GrDiffLine.Type.REMOVE, 8), + new GrDiffLine(GrDiffLine.Type.ADD, 0, 10), + new GrDiffLine(GrDiffLine.Type.REMOVE, 9), + new GrDiffLine(GrDiffLine.Type.ADD, 0, 11), + new GrDiffLine(GrDiffLine.Type.REMOVE, 10), + new GrDiffLine(GrDiffLine.Type.ADD, 0, 12), + ]), + new GrDiffGroup(GrDiffGroup.Type.BOTH, [ + new GrDiffLine(GrDiffLine.Type.BOTH, 11, 13), + new GrDiffLine(GrDiffLine.Type.BOTH, 12, 14), + new GrDiffLine(GrDiffLine.Type.BOTH, 13, 15), + ]), + ]; + }); + + test('hides hidden groups in context control', () => { + const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 3, 6); + assert.equal(collapsedGroups.length, 3); + + assert.equal(collapsedGroups[0], groups[0]); + + assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.CONTEXT_CONTROL); + assert.equal(collapsedGroups[1].lines.length, 1); + assert.equal( + collapsedGroups[1].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL); + assert.equal( + collapsedGroups[1].lines[0].contextGroups.length, 1); + assert.equal( + collapsedGroups[1].lines[0].contextGroups[0], groups[1]); + + assert.equal(collapsedGroups[2], groups[2]); + }); + + test('splits partially hidden groups', () => { + const collapsedGroups = GrDiffGroup.hideInContextControl(groups, 4, 7); + assert.equal(collapsedGroups.length, 4); + assert.equal(collapsedGroups[0], groups[0]); + + assert.equal(collapsedGroups[1].type, GrDiffGroup.Type.DELTA); + assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]); + assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]); + + assert.equal(collapsedGroups[2].type, GrDiffGroup.Type.CONTEXT_CONTROL); + assert.equal(collapsedGroups[2].lines.length, 1); + assert.equal( + collapsedGroups[2].lines[0].type, GrDiffLine.Type.CONTEXT_CONTROL); + assert.equal( + collapsedGroups[2].lines[0].contextGroups.length, 2); + + assert.equal( + collapsedGroups[2].lines[0].contextGroups[0].type, + GrDiffGroup.Type.DELTA); + assert.deepEqual( + collapsedGroups[2].lines[0].contextGroups[0].adds, + groups[1].adds.slice(1)); + assert.deepEqual( + collapsedGroups[2].lines[0].contextGroups[0].removes, + groups[1].removes.slice(1)); + + assert.equal( + collapsedGroups[2].lines[0].contextGroups[1].type, + GrDiffGroup.Type.BOTH); + assert.deepEqual( + collapsedGroups[2].lines[0].contextGroups[1].lines, + [groups[2].lines[0]]); + + assert.equal(collapsedGroups[3].type, GrDiffGroup.Type.BOTH); + assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1)); + }); + + test('groups unchanged if the hidden range is empty', () => { + assert.deepEqual( + GrDiffGroup.hideInContextControl(groups, 0, 0), groups); + }); + + test('groups unchanged if there is only 1 line to hide', () => { + assert.deepEqual( + GrDiffGroup.hideInContextControl(groups, 3, 4), groups); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js index 8e96147..5bca7be 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,964 +14,983 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.'; - const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' + - 'of an edit.'; - const ERR_INVALID_LINE = 'Invalid line number: '; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../gr-diff-builder/gr-diff-builder-element.js'; +import '../gr-diff-highlight/gr-diff-highlight.js'; +import '../gr-diff-selection/gr-diff-selection.js'; +import '../gr-syntax-themes/gr-syntax-theme.js'; +import '../gr-ranged-comment-themes/gr-ranged-comment-theme.js'; +import '../../../scripts/hiddenscroll.js'; +import './gr-diff-line.js'; +import './gr-diff-group.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {htmlTemplate} from './gr-diff_html.js'; - const NO_NEWLINE_BASE = 'No newline at end of base file.'; - const NO_NEWLINE_REVISION = 'No newline at end of revision file.'; +const ERR_COMMENT_ON_EDIT = 'You cannot comment on an edit.'; +const ERR_COMMENT_ON_EDIT_BASE = 'You cannot comment on the base patch set ' + + 'of an edit.'; +const ERR_INVALID_LINE = 'Invalid line number: '; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; +const NO_NEWLINE_BASE = 'No newline at end of base file.'; +const NO_NEWLINE_REVISION = 'No newline at end of revision file.'; - const DiffSide = { - LEFT: 'left', - RIGHT: 'right', - }; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; - const LARGE_DIFF_THRESHOLD_LINES = 10000; - const FULL_CONTEXT = -1; - const LIMITED_CONTEXT = 10; +const DiffSide = { + LEFT: 'left', + RIGHT: 'right', +}; + +const LARGE_DIFF_THRESHOLD_LINES = 10000; +const FULL_CONTEXT = -1; +const LIMITED_CONTEXT = 10; + +/** + * Compare two ranges. Either argument may be falsy, but will only return + * true if both are falsy or if neither are falsy and have the same position + * values. + * + * @param {Gerrit.Range=} a range 1 + * @param {Gerrit.Range=} b range 2 + * @return {boolean} + */ +Gerrit.rangesEqual = function(a, b) { + 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; +}; + +function isThreadEl(node) { + return node.nodeType === Node.ELEMENT_NODE && + node.classList.contains('comment-thread'); +} + +/** + * Turn a slot element into the corresponding content element. + * Slots are only fully supported in Polymer 2 - in Polymer 1, they are + * replaced with content elements during template parsing. This conversion is + * not applied for imperatively created slot elements, so this method + * implements the same behavior as the template parsing for imperative slots. + */ +Gerrit.slotToContent = function(slot) { + if (PolymerElement) { + return slot; + } + const content = document.createElement('content'); + content.name = slot.name; + content.setAttribute('select', `[slot='${slot.name}']`); + return content; +}; + +const COMMIT_MSG_PATH = '/COMMIT_MSG'; +/** + * 72 is the inofficial length standard for git commit messages. + * Derived from the fact that git log/show appends 4 ws in the beginning of + * each line when displaying commit messages. To center the commit message + * in an 80 char terminal a 4 ws border is added to the rightmost side: + * 4 + 72 + 4 + */ +const COMMIT_MSG_LINE_LENGTH = 72; + +const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PatchSetMixin + * @extends Polymer.Element + */ +class GrDiff extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PatchSetBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-diff'; } + /** + * Fired when the user selects a line. + * + * @event line-selected + */ /** - * Compare two ranges. Either argument may be falsy, but will only return - * true if both are falsy or if neither are falsy and have the same position - * values. + * Fired if being logged in is required. * - * @param {Gerrit.Range=} a range 1 - * @param {Gerrit.Range=} b range 2 - * @return {boolean} + * @event show-auth-required */ - Gerrit.rangesEqual = function(a, b) { - 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; - }; - function isThreadEl(node) { - return node.nodeType === Node.ELEMENT_NODE && - node.classList.contains('comment-thread'); + /** + * Fired when a comment is created + * + * @event create-comment + */ + + /** + * Fired when rendering, including syntax highlighting, is done. Also fired + * when no rendering can be done because required preferences are not set. + * + * @event render + */ + + /** + * Fired for interaction reporting when a diff context is expanded. + * Contains an event.detail with numLines about the number of lines that + * were expanded. + * + * @event diff-context-expanded + */ + + static get properties() { + return { + changeNum: String, + noAutoRender: { + type: Boolean, + value: false, + }, + /** @type {?} */ + patchRange: Object, + path: { + type: String, + observer: '_pathObserver', + }, + prefs: { + type: Object, + observer: '_prefsObserver', + }, + projectName: String, + displayLine: { + type: Boolean, + value: false, + }, + isImageDiff: { + type: Boolean, + }, + commitRange: Object, + hidden: { + type: Boolean, + reflectToAttribute: true, + }, + noRenderOnPrefsChange: Boolean, + /** @type {!Array<!Gerrit.HoveredRange>} */ + _commentRanges: { + type: Array, + value: () => [], + }, + /** @type {!Array<!Gerrit.CoverageRange>} */ + coverageRanges: { + type: Array, + value: () => [], + }, + lineWrapping: { + type: Boolean, + value: false, + observer: '_lineWrappingObserver', + }, + viewMode: { + type: String, + value: DiffViewMode.SIDE_BY_SIDE, + observer: '_viewModeObserver', + }, + + /** @type {?Gerrit.LineOfInterest} */ + lineOfInterest: Object, + + loading: { + type: Boolean, + value: false, + observer: '_loadingChanged', + }, + + loggedIn: { + type: Boolean, + value: false, + }, + diff: { + type: Object, + observer: '_diffChanged', + }, + _diffHeaderItems: { + type: Array, + value: [], + computed: '_computeDiffHeaderItems(diff.*)', + }, + _diffTableClass: { + type: String, + value: '', + }, + /** @type {?Object} */ + baseImage: Object, + /** @type {?Object} */ + revisionImage: Object, + + /** + * Whether the safety check for large diffs when whole-file is set has + * been bypassed. If the value is null, then the safety has not been + * bypassed. If the value is a number, then that number represents the + * context preference to use when rendering the bypassed diff. + * + * @type {number|null} + */ + _safetyBypass: { + type: Number, + value: null, + }, + + _showWarning: Boolean, + + /** @type {?string} */ + errorMessage: { + type: String, + value: null, + }, + + /** @type {?Object} */ + blame: { + type: Object, + value: null, + observer: '_blameChanged', + }, + + parentIndex: Number, + + showNewlineWarningLeft: { + type: Boolean, + value: false, + }, + showNewlineWarningRight: { + type: Boolean, + value: false, + }, + + _newlineWarning: { + type: String, + computed: '_computeNewlineWarning(' + + 'showNewlineWarningLeft, showNewlineWarningRight)', + }, + + _diffLength: Number, + + /** + * Observes comment nodes added or removed after the initial render. + * Can be used to unregister when the entire diff is (re-)rendered or upon + * detachment. + * + * @type {?PolymerDomApi.ObserveHandle} + */ + _incrementalNodeObserver: Object, + + /** + * Observes comment nodes added or removed at any point. + * Can be used to unregister upon detachment. + * + * @type {?PolymerDomApi.ObserveHandle} + */ + _nodeObserver: Object, + + /** Set by Polymer. */ + isAttached: Boolean, + layers: Array, + }; + } + + static get observers() { + return [ + '_enableSelectionObserver(loggedIn, isAttached)', + ]; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('create-range-comment', + e => this._handleCreateRangeComment(e)); + this.addEventListener('render-content', + () => this._handleRenderContent()); + } + + /** @override */ + attached() { + super.attached(); + this._observeNodes(); + } + + /** @override */ + detached() { + super.detached(); + this._unobserveIncrementalNodes(); + this._unobserveNodes(); + } + + showNoChangeMessage(loading, prefs, diffLength) { + return !loading && + prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' && + diffLength === 0; + } + + _enableSelectionObserver(loggedIn, isAttached) { + // Polymer 2: check for undefined + if ([loggedIn, isAttached].some(arg => arg === undefined)) { + return; + } + + if (loggedIn && isAttached) { + this.listen(document, 'selectionchange', '_handleSelectionChange'); + this.listen(document, 'mouseup', '_handleMouseUp'); + } else { + this.unlisten(document, 'selectionchange', '_handleSelectionChange'); + this.unlisten(document, 'mouseup', '_handleMouseUp'); + } + } + + _handleSelectionChange() { + // Because of shadow DOM selections, we handle the selectionchange here, + // 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); + } + + _handleMouseUp(e) { + // To handle double-click outside of text creating comments, we check on + // 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); + } + + /** Gets the current selection, preferring the shadow DOM selection. */ + _getShadowOrDocumentSelection() { + // When using native shadow DOM, the selection returned by + // document.getSelection() cannot reference the actual DOM elements making + // up the diff, because they are in the shadow DOM of the gr-diff element. + // This takes the shadow DOM selection if one exists. + return this.root.getSelection ? + this.root.getSelection() : + document.getSelection(); + } + + _observeNodes() { + this._nodeObserver = dom(this).observeNodes(info => { + const addedThreadEls = info.addedNodes.filter(isThreadEl); + const removedThreadEls = info.removedNodes.filter(isThreadEl); + this._updateRanges(addedThreadEls, removedThreadEls); + this._redispatchHoverEvents(addedThreadEls); + }); + } + + _updateRanges(addedThreadEls, removedThreadEls) { + function commentRangeFromThreadEl(threadEl) { + const side = threadEl.getAttribute('comment-side'); + const range = JSON.parse(threadEl.getAttribute('range')); + return {side, range, hovering: false}; + } + + const addedCommentRanges = addedThreadEls + .map(commentRangeFromThreadEl) + .filter(({range}) => range); + const removedCommentRanges = removedThreadEls + .map(commentRangeFromThreadEl) + .filter(({range}) => range); + for (const removedCommentRange of removedCommentRanges) { + const i = this._commentRanges + .findIndex( + cr => cr.side === removedCommentRange.side && + Gerrit.rangesEqual(cr.range, removedCommentRange.range) + ); + this.splice('_commentRanges', i, 1); + } + + if (addedCommentRanges && addedCommentRanges.length) { + this.push('_commentRanges', ...addedCommentRanges); + } } /** - * Turn a slot element into the corresponding content element. - * Slots are only fully supported in Polymer 2 - in Polymer 1, they are - * replaced with content elements during template parsing. This conversion is - * not applied for imperatively created slot elements, so this method - * implements the same behavior as the template parsing for imperative slots. + * The key locations based on the comments and line of interests, + * where lines should not be collapsed. + * + * @return {{left: Object<(string|number), boolean>, + * right: Object<(string|number), boolean>}} */ - Gerrit.slotToContent = function(slot) { - if (Polymer.Element) { - return slot; + _computeKeyLocations() { + const keyLocations = {left: {}, right: {}}; + if (this.lineOfInterest) { + const side = this.lineOfInterest.leftSide ? 'left' : 'right'; + keyLocations[side][this.lineOfInterest.number] = true; } - const content = document.createElement('content'); - content.name = slot.name; - content.setAttribute('select', `[slot='${slot.name}']`); - return content; - }; + const threadEls = dom(this).getEffectiveChildNodes() + .filter(isThreadEl); - const COMMIT_MSG_PATH = '/COMMIT_MSG'; - /** - * 72 is the inofficial length standard for git commit messages. - * Derived from the fact that git log/show appends 4 ws in the beginning of - * each line when displaying commit messages. To center the commit message - * in an 80 char terminal a 4 ws border is added to the rightmost side: - * 4 + 72 + 4 - */ - const COMMIT_MSG_LINE_LENGTH = 72; + for (const threadEl of threadEls) { + const commentSide = threadEl.getAttribute('comment-side'); + const lineNum = Number(threadEl.getAttribute('line-num')) || + GrDiffLine.FILE; + const commentRange = threadEl.range || {}; + keyLocations[commentSide][lineNum] = true; + // Add start_line as well if exists, + // the being and end of the range should not be collapsed. + if (commentRange.start_line) { + keyLocations[commentSide][commentRange.start_line] = true; + } + } + return keyLocations; + } - const RENDER_DIFF_TABLE_DEBOUNCE_NAME = 'renderDiffTable'; + // Dispatch events that are handled by the gr-diff-highlight. + _redispatchHoverEvents(addedThreadEls) { + for (const threadEl of addedThreadEls) { + threadEl.addEventListener('mouseenter', () => { + threadEl.dispatchEvent(new CustomEvent( + 'comment-thread-mouseenter', {bubbles: true, composed: true})); + }); + threadEl.addEventListener('mouseleave', () => { + threadEl.dispatchEvent(new CustomEvent( + 'comment-thread-mouseleave', {bubbles: true, composed: true})); + }); + } + } - /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PatchSetMixin - * @extends Polymer.Element - */ - class GrDiff extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PatchSetBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-diff'; } - /** - * Fired when the user selects a line. - * - * @event line-selected - */ + /** Cancel any remaining diff builder rendering work. */ + cancel() { + this.$.diffBuilder.cancel(); + this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME); + } - /** - * Fired if being logged in is required. - * - * @event show-auth-required - */ - - /** - * Fired when a comment is created - * - * @event create-comment - */ - - /** - * Fired when rendering, including syntax highlighting, is done. Also fired - * when no rendering can be done because required preferences are not set. - * - * @event render - */ - - /** - * Fired for interaction reporting when a diff context is expanded. - * Contains an event.detail with numLines about the number of lines that - * were expanded. - * - * @event diff-context-expanded - */ - - static get properties() { - return { - changeNum: String, - noAutoRender: { - type: Boolean, - value: false, - }, - /** @type {?} */ - patchRange: Object, - path: { - type: String, - observer: '_pathObserver', - }, - prefs: { - type: Object, - observer: '_prefsObserver', - }, - projectName: String, - displayLine: { - type: Boolean, - value: false, - }, - isImageDiff: { - type: Boolean, - }, - commitRange: Object, - hidden: { - type: Boolean, - reflectToAttribute: true, - }, - noRenderOnPrefsChange: Boolean, - /** @type {!Array<!Gerrit.HoveredRange>} */ - _commentRanges: { - type: Array, - value: () => [], - }, - /** @type {!Array<!Gerrit.CoverageRange>} */ - coverageRanges: { - type: Array, - value: () => [], - }, - lineWrapping: { - type: Boolean, - value: false, - observer: '_lineWrappingObserver', - }, - viewMode: { - type: String, - value: DiffViewMode.SIDE_BY_SIDE, - observer: '_viewModeObserver', - }, - - /** @type {?Gerrit.LineOfInterest} */ - lineOfInterest: Object, - - loading: { - type: Boolean, - value: false, - observer: '_loadingChanged', - }, - - loggedIn: { - type: Boolean, - value: false, - }, - diff: { - type: Object, - observer: '_diffChanged', - }, - _diffHeaderItems: { - type: Array, - value: [], - computed: '_computeDiffHeaderItems(diff.*)', - }, - _diffTableClass: { - type: String, - value: '', - }, - /** @type {?Object} */ - baseImage: Object, - /** @type {?Object} */ - revisionImage: Object, - - /** - * Whether the safety check for large diffs when whole-file is set has - * been bypassed. If the value is null, then the safety has not been - * bypassed. If the value is a number, then that number represents the - * context preference to use when rendering the bypassed diff. - * - * @type {number|null} - */ - _safetyBypass: { - type: Number, - value: null, - }, - - _showWarning: Boolean, - - /** @type {?string} */ - errorMessage: { - type: String, - value: null, - }, - - /** @type {?Object} */ - blame: { - type: Object, - value: null, - observer: '_blameChanged', - }, - - parentIndex: Number, - - showNewlineWarningLeft: { - type: Boolean, - value: false, - }, - showNewlineWarningRight: { - type: Boolean, - value: false, - }, - - _newlineWarning: { - type: String, - computed: '_computeNewlineWarning(' + - 'showNewlineWarningLeft, showNewlineWarningRight)', - }, - - _diffLength: Number, - - /** - * Observes comment nodes added or removed after the initial render. - * Can be used to unregister when the entire diff is (re-)rendered or upon - * detachment. - * - * @type {?PolymerDomApi.ObserveHandle} - */ - _incrementalNodeObserver: Object, - - /** - * Observes comment nodes added or removed at any point. - * Can be used to unregister upon detachment. - * - * @type {?PolymerDomApi.ObserveHandle} - */ - _nodeObserver: Object, - - /** Set by Polymer. */ - isAttached: Boolean, - layers: Array, - }; + /** @return {!Array<!HTMLElement>} */ + getCursorStops() { + if (this.hidden && this.noAutoRender) { + return []; } - static get observers() { - return [ - '_enableSelectionObserver(loggedIn, isAttached)', - ]; - } + return Array.from( + dom(this.root).querySelectorAll('.diff-row')); + } - /** @override */ - created() { - super.created(); - this.addEventListener('create-range-comment', - e => this._handleCreateRangeComment(e)); - this.addEventListener('render-content', - () => this._handleRenderContent()); - } + /** @return {boolean} */ + isRangeSelected() { + return !!this.$.highlights.selectedRange; + } - /** @override */ - attached() { - super.attached(); - this._observeNodes(); - } + toggleLeftDiff() { + this.toggleClass('no-left'); + } - /** @override */ - detached() { - super.detached(); - this._unobserveIncrementalNodes(); - this._unobserveNodes(); + _blameChanged(newValue) { + this.$.diffBuilder.setBlame(newValue); + if (newValue) { + this.classList.add('showBlame'); + } else { + this.classList.remove('showBlame'); } + } - showNoChangeMessage(loading, prefs, diffLength) { - return !loading && - prefs && prefs.ignore_whitespace !== 'IGNORE_NONE' && - diffLength === 0; + /** @return {string} */ + _computeContainerClass(loggedIn, viewMode, displayLine) { + const classes = ['diffContainer']; + switch (viewMode) { + case DiffViewMode.UNIFIED: + classes.push('unified'); + break; + case DiffViewMode.SIDE_BY_SIDE: + classes.push('sideBySide'); + break; + default: + throw Error('Invalid view mode: ', viewMode); } + if (Gerrit.hiddenscroll) { + classes.push('hiddenscroll'); + } + if (loggedIn) { + classes.push('canComment'); + } + if (displayLine) { + classes.push('displayLine'); + } + return classes.join(' '); + } - _enableSelectionObserver(loggedIn, isAttached) { - // Polymer 2: check for undefined - if ([loggedIn, isAttached].some(arg => arg === undefined)) { + _handleTap(e) { + const el = dom(e).localTarget; + + if (el.classList.contains('showContext')) { + this.fire('diff-context-expanded', { + numLines: e.detail.numLines, + }); + this.$.diffBuilder.showContext(e.detail.groups, e.detail.section); + } else if (el.classList.contains('lineNum')) { + this.addDraftAtLine(el); + } else if (el.tagName === 'HL' || + el.classList.contains('content') || + el.classList.contains('contentText')) { + const target = this.$.diffBuilder.getLineElByChild(el); + if (target) { this._selectLine(target); } + } + } + + _selectLine(el) { + this.fire('line-selected', { + side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT, + number: el.getAttribute('data-value'), + path: this.path, + }); + } + + addDraftAtLine(el) { + this._selectLine(el); + if (!this._isValidElForComment(el)) { return; } + + const value = el.getAttribute('data-value'); + let lineNum; + if (value !== GrDiffLine.FILE) { + lineNum = parseInt(value, 10); + if (isNaN(lineNum)) { + this.fire('show-alert', {message: ERR_INVALID_LINE + value}); return; } - - if (loggedIn && isAttached) { - this.listen(document, 'selectionchange', '_handleSelectionChange'); - this.listen(document, 'mouseup', '_handleMouseUp'); - } else { - this.unlisten(document, 'selectionchange', '_handleSelectionChange'); - this.unlisten(document, 'mouseup', '_handleMouseUp'); - } } + this._createComment(el, lineNum); + } - _handleSelectionChange() { - // Because of shadow DOM selections, we handle the selectionchange here, - // 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); + createRangeComment() { + if (!this.isRangeSelected()) { + throw Error('Selection is needed for new range comment'); } + const {side, range} = this.$.highlights.selectedRange; + this._createCommentForSelection(side, range); + } - _handleMouseUp(e) { - // To handle double-click outside of text creating comments, we check on - // 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); + _createCommentForSelection(side, range) { + const lineNum = range.end_line; + const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side); + if (this._isValidElForComment(lineEl)) { + this._createComment(lineEl, lineNum, side, range); } + } - /** Gets the current selection, preferring the shadow DOM selection. */ - _getShadowOrDocumentSelection() { - // When using native shadow DOM, the selection returned by - // document.getSelection() cannot reference the actual DOM elements making - // up the diff, because they are in the shadow DOM of the gr-diff element. - // This takes the shadow DOM selection if one exists. - return this.root.getSelection ? - this.root.getSelection() : - document.getSelection(); - } + _handleCreateRangeComment(e) { + const range = e.detail.range; + const side = e.detail.side; + this._createCommentForSelection(side, range); + } - _observeNodes() { - this._nodeObserver = Polymer.dom(this).observeNodes(info => { - const addedThreadEls = info.addedNodes.filter(isThreadEl); - const removedThreadEls = info.removedNodes.filter(isThreadEl); - this._updateRanges(addedThreadEls, removedThreadEls); - this._redispatchHoverEvents(addedThreadEls); - }); - } - - _updateRanges(addedThreadEls, removedThreadEls) { - function commentRangeFromThreadEl(threadEl) { - const side = threadEl.getAttribute('comment-side'); - const range = JSON.parse(threadEl.getAttribute('range')); - return {side, range, hovering: false}; - } - - const addedCommentRanges = addedThreadEls - .map(commentRangeFromThreadEl) - .filter(({range}) => range); - const removedCommentRanges = removedThreadEls - .map(commentRangeFromThreadEl) - .filter(({range}) => range); - for (const removedCommentRange of removedCommentRanges) { - const i = this._commentRanges - .findIndex( - cr => cr.side === removedCommentRange.side && - Gerrit.rangesEqual(cr.range, removedCommentRange.range) - ); - this.splice('_commentRanges', i, 1); - } - - if (addedCommentRanges && addedCommentRanges.length) { - this.push('_commentRanges', ...addedCommentRanges); - } - } - - /** - * The key locations based on the comments and line of interests, - * where lines should not be collapsed. - * - * @return {{left: Object<(string|number), boolean>, - * right: Object<(string|number), boolean>}} - */ - _computeKeyLocations() { - const keyLocations = {left: {}, right: {}}; - if (this.lineOfInterest) { - const side = this.lineOfInterest.leftSide ? 'left' : 'right'; - keyLocations[side][this.lineOfInterest.number] = true; - } - const threadEls = Polymer.dom(this).getEffectiveChildNodes() - .filter(isThreadEl); - - for (const threadEl of threadEls) { - const commentSide = threadEl.getAttribute('comment-side'); - const lineNum = Number(threadEl.getAttribute('line-num')) || - GrDiffLine.FILE; - const commentRange = threadEl.range || {}; - keyLocations[commentSide][lineNum] = true; - // Add start_line as well if exists, - // the being and end of the range should not be collapsed. - if (commentRange.start_line) { - keyLocations[commentSide][commentRange.start_line] = true; - } - } - return keyLocations; - } - - // Dispatch events that are handled by the gr-diff-highlight. - _redispatchHoverEvents(addedThreadEls) { - for (const threadEl of addedThreadEls) { - threadEl.addEventListener('mouseenter', () => { - threadEl.dispatchEvent(new CustomEvent( - 'comment-thread-mouseenter', {bubbles: true, composed: true})); - }); - threadEl.addEventListener('mouseleave', () => { - threadEl.dispatchEvent(new CustomEvent( - 'comment-thread-mouseleave', {bubbles: true, composed: true})); - }); - } - } - - /** Cancel any remaining diff builder rendering work. */ - cancel() { - this.$.diffBuilder.cancel(); - this.cancelDebouncer(RENDER_DIFF_TABLE_DEBOUNCE_NAME); - } - - /** @return {!Array<!HTMLElement>} */ - getCursorStops() { - if (this.hidden && this.noAutoRender) { - return []; - } - - return Array.from( - Polymer.dom(this.root).querySelectorAll('.diff-row')); - } - - /** @return {boolean} */ - isRangeSelected() { - return !!this.$.highlights.selectedRange; - } - - toggleLeftDiff() { - this.toggleClass('no-left'); - } - - _blameChanged(newValue) { - this.$.diffBuilder.setBlame(newValue); - if (newValue) { - this.classList.add('showBlame'); - } else { - this.classList.remove('showBlame'); - } - } - - /** @return {string} */ - _computeContainerClass(loggedIn, viewMode, displayLine) { - const classes = ['diffContainer']; - switch (viewMode) { - case DiffViewMode.UNIFIED: - classes.push('unified'); - break; - case DiffViewMode.SIDE_BY_SIDE: - classes.push('sideBySide'); - break; - default: - throw Error('Invalid view mode: ', viewMode); - } - if (Gerrit.hiddenscroll) { - classes.push('hiddenscroll'); - } - if (loggedIn) { - classes.push('canComment'); - } - if (displayLine) { - classes.push('displayLine'); - } - return classes.join(' '); - } - - _handleTap(e) { - const el = Polymer.dom(e).localTarget; - - if (el.classList.contains('showContext')) { - this.fire('diff-context-expanded', { - numLines: e.detail.numLines, - }); - this.$.diffBuilder.showContext(e.detail.groups, e.detail.section); - } else if (el.classList.contains('lineNum')) { - this.addDraftAtLine(el); - } else if (el.tagName === 'HL' || - el.classList.contains('content') || - el.classList.contains('contentText')) { - const target = this.$.diffBuilder.getLineElByChild(el); - if (target) { this._selectLine(target); } - } - } - - _selectLine(el) { - this.fire('line-selected', { - side: el.classList.contains('left') ? DiffSide.LEFT : DiffSide.RIGHT, - number: el.getAttribute('data-value'), - path: this.path, - }); - } - - addDraftAtLine(el) { - this._selectLine(el); - if (!this._isValidElForComment(el)) { return; } - - const value = el.getAttribute('data-value'); - let lineNum; - if (value !== GrDiffLine.FILE) { - lineNum = parseInt(value, 10); - if (isNaN(lineNum)) { - this.fire('show-alert', {message: ERR_INVALID_LINE + value}); - return; - } - } - this._createComment(el, lineNum); - } - - createRangeComment() { - if (!this.isRangeSelected()) { - throw Error('Selection is needed for new range comment'); - } - const {side, range} = this.$.highlights.selectedRange; - this._createCommentForSelection(side, range); - } - - _createCommentForSelection(side, range) { - const lineNum = range.end_line; - const lineEl = this.$.diffBuilder.getLineElByNumber(lineNum, side); - if (this._isValidElForComment(lineEl)) { - this._createComment(lineEl, lineNum, side, range); - } - } - - _handleCreateRangeComment(e) { - const range = e.detail.range; - const side = e.detail.side; - this._createCommentForSelection(side, range); - } - - /** @return {boolean} */ - _isValidElForComment(el) { - if (!this.loggedIn) { - this.fire('show-auth-required'); - return false; - } - const patchNum = el.classList.contains(DiffSide.LEFT) ? - this.patchRange.basePatchNum : - this.patchRange.patchNum; - - const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME); - const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) && - this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME); - - if (isEdit) { - this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT}); - return false; - } else if (isEditBase) { - this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE}); - return false; - } - return true; - } - - /** - * @param {!Object} lineEl - * @param {number=} lineNum - * @param {string=} side - * @param {!Object=} range - */ - _createComment(lineEl, lineNum, side, range) { - const contentText = this.$.diffBuilder.getContentByLineEl(lineEl); - const contentEl = contentText.parentElement; - side = side || - this._getCommentSideByLineAndContent(lineEl, contentEl); - const patchForNewThreads = this._getPatchNumByLineAndContent( - lineEl, contentEl); - const isOnParent = - this._getIsParentCommentByLineAndContent(lineEl, contentEl); - this.dispatchEvent(new CustomEvent('create-comment', { - bubbles: true, - composed: true, - detail: { - lineNum, - side, - patchNum: patchForNewThreads, - isOnParent, - range, - }, - })); - } - - _getThreadGroupForLine(contentEl) { - return contentEl.querySelector('.thread-group'); - } - - /** - * Gets or creates a comment thread group for a specific line and side on a - * diff. - * - * @param {!Object} contentEl - * @param {!Gerrit.DiffSide} commentSide - * @return {!Node} - */ - _getOrCreateThreadGroup(contentEl, commentSide) { - // Check if thread group exists. - let threadGroupEl = this._getThreadGroupForLine(contentEl); - if (!threadGroupEl) { - threadGroupEl = document.createElement('div'); - threadGroupEl.className = 'thread-group'; - threadGroupEl.setAttribute('data-side', commentSide); - contentEl.appendChild(threadGroupEl); - } - return threadGroupEl; - } - - /** - * The value to be used for the patch number of new comments created at the - * given line and content elements. - * - * In two cases of creating a comment on the left side, the patch number to - * be used should actually be right side of the patch range: - * - When the patch range is against the parent comment of a normal change. - * Such comments declare themmselves to be on the left using side=PARENT. - * - If the patch range is against the indexed parent of a merge change. - * Such comments declare themselves to be on the given parent by - * specifying the parent index via parent=i. - * - * @return {number} - */ - _getPatchNumByLineAndContent(lineEl, contentEl) { - let patchNum = this.patchRange.patchNum; - - if ((lineEl.classList.contains(DiffSide.LEFT) || - contentEl.classList.contains('remove')) && - this.patchRange.basePatchNum !== 'PARENT' && - !this.isMergeParent(this.patchRange.basePatchNum)) { - patchNum = this.patchRange.basePatchNum; - } - return patchNum; - } - - /** @return {boolean} */ - _getIsParentCommentByLineAndContent(lineEl, contentEl) { - if ((lineEl.classList.contains(DiffSide.LEFT) || - contentEl.classList.contains('remove')) && - (this.patchRange.basePatchNum === 'PARENT' || - this.isMergeParent(this.patchRange.basePatchNum))) { - return true; - } + /** @return {boolean} */ + _isValidElForComment(el) { + if (!this.loggedIn) { + this.fire('show-auth-required'); return false; } + const patchNum = el.classList.contains(DiffSide.LEFT) ? + this.patchRange.basePatchNum : + this.patchRange.patchNum; - /** @return {string} */ - _getCommentSideByLineAndContent(lineEl, contentEl) { - let side = 'right'; - if (lineEl.classList.contains(DiffSide.LEFT) || - contentEl.classList.contains('remove')) { - side = 'left'; - } - return side; + const isEdit = this.patchNumEquals(patchNum, this.EDIT_NAME); + const isEditBase = this.patchNumEquals(patchNum, this.PARENT_NAME) && + this.patchNumEquals(this.patchRange.patchNum, this.EDIT_NAME); + + if (isEdit) { + this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT}); + return false; + } else if (isEditBase) { + this.fire('show-alert', {message: ERR_COMMENT_ON_EDIT_BASE}); + return false; } + return true; + } - _prefsObserver(newPrefs, oldPrefs) { - // Scan the preference objects one level deep to see if they differ. - let differ = !oldPrefs; - if (newPrefs && oldPrefs) { - for (const key in newPrefs) { - if (newPrefs[key] !== oldPrefs[key]) { - differ = true; - } + /** + * @param {!Object} lineEl + * @param {number=} lineNum + * @param {string=} side + * @param {!Object=} range + */ + _createComment(lineEl, lineNum, side, range) { + const contentText = this.$.diffBuilder.getContentByLineEl(lineEl); + const contentEl = contentText.parentElement; + side = side || + this._getCommentSideByLineAndContent(lineEl, contentEl); + const patchForNewThreads = this._getPatchNumByLineAndContent( + lineEl, contentEl); + const isOnParent = + this._getIsParentCommentByLineAndContent(lineEl, contentEl); + this.dispatchEvent(new CustomEvent('create-comment', { + bubbles: true, + composed: true, + detail: { + lineNum, + side, + patchNum: patchForNewThreads, + isOnParent, + range, + }, + })); + } + + _getThreadGroupForLine(contentEl) { + return contentEl.querySelector('.thread-group'); + } + + /** + * Gets or creates a comment thread group for a specific line and side on a + * diff. + * + * @param {!Object} contentEl + * @param {!Gerrit.DiffSide} commentSide + * @return {!Node} + */ + _getOrCreateThreadGroup(contentEl, commentSide) { + // Check if thread group exists. + let threadGroupEl = this._getThreadGroupForLine(contentEl); + if (!threadGroupEl) { + threadGroupEl = document.createElement('div'); + threadGroupEl.className = 'thread-group'; + threadGroupEl.setAttribute('data-side', commentSide); + contentEl.appendChild(threadGroupEl); + } + return threadGroupEl; + } + + /** + * The value to be used for the patch number of new comments created at the + * given line and content elements. + * + * In two cases of creating a comment on the left side, the patch number to + * be used should actually be right side of the patch range: + * - When the patch range is against the parent comment of a normal change. + * Such comments declare themmselves to be on the left using side=PARENT. + * - If the patch range is against the indexed parent of a merge change. + * Such comments declare themselves to be on the given parent by + * specifying the parent index via parent=i. + * + * @return {number} + */ + _getPatchNumByLineAndContent(lineEl, contentEl) { + let patchNum = this.patchRange.patchNum; + + if ((lineEl.classList.contains(DiffSide.LEFT) || + contentEl.classList.contains('remove')) && + this.patchRange.basePatchNum !== 'PARENT' && + !this.isMergeParent(this.patchRange.basePatchNum)) { + patchNum = this.patchRange.basePatchNum; + } + return patchNum; + } + + /** @return {boolean} */ + _getIsParentCommentByLineAndContent(lineEl, contentEl) { + if ((lineEl.classList.contains(DiffSide.LEFT) || + contentEl.classList.contains('remove')) && + (this.patchRange.basePatchNum === 'PARENT' || + this.isMergeParent(this.patchRange.basePatchNum))) { + return true; + } + return false; + } + + /** @return {string} */ + _getCommentSideByLineAndContent(lineEl, contentEl) { + let side = 'right'; + if (lineEl.classList.contains(DiffSide.LEFT) || + contentEl.classList.contains('remove')) { + side = 'left'; + } + return side; + } + + _prefsObserver(newPrefs, oldPrefs) { + // Scan the preference objects one level deep to see if they differ. + let differ = !oldPrefs; + if (newPrefs && oldPrefs) { + for (const key in newPrefs) { + if (newPrefs[key] !== oldPrefs[key]) { + differ = true; } } - - if (differ) { - this._prefsChanged(newPrefs); - } } - _pathObserver() { - // Call _prefsChanged(), because line-limit style value depends on path. - this._prefsChanged(this.prefs); - } - - _viewModeObserver() { - this._prefsChanged(this.prefs); - } - - /** @param {boolean} newValue */ - _loadingChanged(newValue) { - if (newValue) { - this.cancel(); - this._blame = null; - this._safetyBypass = null; - this._showWarning = false; - this.clearDiffContent(); - } - } - - _lineWrappingObserver() { - this._prefsChanged(this.prefs); - } - - _prefsChanged(prefs) { - if (!prefs) { return; } - - this._blame = null; - - const lineLength = this.path === COMMIT_MSG_PATH ? - COMMIT_MSG_LINE_LENGTH : prefs.line_length; - const stylesToUpdate = {}; - - if (prefs.line_wrapping) { - this._diffTableClass = 'full-width'; - if (this.viewMode === 'SIDE_BY_SIDE') { - stylesToUpdate['--content-width'] = 'none'; - stylesToUpdate['--line-limit'] = lineLength + 'ch'; - } - } else { - this._diffTableClass = ''; - stylesToUpdate['--content-width'] = lineLength + 'ch'; - } - - if (prefs.font_size) { - stylesToUpdate['--font-size'] = prefs.font_size + 'px'; - } - - this.updateStyles(stylesToUpdate); - - if (this.diff && !this.noRenderOnPrefsChange) { - this._debounceRenderDiffTable(); - } - } - - _diffChanged(newValue) { - if (newValue) { - this._diffLength = this.getDiffLength(newValue); - this._debounceRenderDiffTable(); - } - } - - /** - * When called multiple times from the same microtask, will call - * _renderDiffTable only once, in the next microtask, unless it is cancelled - * before that microtask runs. - * - * This should be used instead of calling _renderDiffTable directly to - * render the diff in response to an input change, because there may be - * multiple inputs changing in the same microtask, but we only want to - * render once. - */ - _debounceRenderDiffTable() { - this.debounce( - RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable()); - } - - _renderDiffTable() { - this._unobserveIncrementalNodes(); - if (!this.prefs) { - this.dispatchEvent( - new CustomEvent('render', {bubbles: true, composed: true})); - return; - } - if (this.prefs.context === -1 && - this._diffLength >= LARGE_DIFF_THRESHOLD_LINES && - this._safetyBypass === null) { - this._showWarning = true; - this.dispatchEvent( - new CustomEvent('render', {bubbles: true, composed: true})); - return; - } - - this._showWarning = false; - - const keyLocations = this._computeKeyLocations(); - this.$.diffBuilder.render(keyLocations, this._getBypassPrefs()) - .then(() => { - this.dispatchEvent( - new CustomEvent('render', { - bubbles: true, - composed: true, - detail: {contentRendered: true}, - })); - }); - } - - _handleRenderContent() { - this._incrementalNodeObserver = Polymer.dom(this).observeNodes(info => { - const addedThreadEls = info.addedNodes.filter(isThreadEl); - // Removed nodes do not need to be handled because all this code does is - // adding a slot for the added thread elements, and the extra slots do - // not hurt. It's probably a bigger performance cost to remove them than - // to keep them around. Medium term we can even consider to add one slot - // for each line from the start. - let lastEl; - for (const threadEl of addedThreadEls) { - const lineNumString = threadEl.getAttribute('line-num') || 'FILE'; - const commentSide = threadEl.getAttribute('comment-side'); - const lineEl = this.$.diffBuilder.getLineElByNumber( - lineNumString, commentSide); - const contentText = this.$.diffBuilder.getContentByLineEl(lineEl); - const contentEl = contentText.parentElement; - const threadGroupEl = this._getOrCreateThreadGroup( - contentEl, commentSide); - // Create a slot for the thread and attach it to the thread group. - // The Polyfill has some bugs and this only works if the slot is - // attached to the group after the group is attached to the DOM. - // The thread group may already have a slot with the right name, but - // that is okay because the first matching slot is used and the rest - // are ignored. - const slot = document.createElement('slot'); - slot.name = threadEl.getAttribute('slot'); - Polymer.dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot)); - lastEl = threadEl; - } - - // Safari is not binding newly created comment-thread - // with the slot somehow, replace itself will rebind it - // @see Issue 11182 - if (lastEl && lastEl.replaceWith) { - lastEl.replaceWith(lastEl); - } - }); - } - - _unobserveIncrementalNodes() { - if (this._incrementalNodeObserver) { - Polymer.dom(this).unobserveNodes(this._incrementalNodeObserver); - } - } - - _unobserveNodes() { - if (this._nodeObserver) { - Polymer.dom(this).unobserveNodes(this._nodeObserver); - } - } - - /** - * Get the preferences object including the safety bypass context (if any). - */ - _getBypassPrefs() { - if (this._safetyBypass !== null) { - return Object.assign({}, this.prefs, {context: this._safetyBypass}); - } - return this.prefs; - } - - clearDiffContent() { - this._unobserveIncrementalNodes(); - this.$.diffTable.innerHTML = null; - } - - /** @return {!Array} */ - _computeDiffHeaderItems(diffInfoRecord) { - const diffInfo = diffInfoRecord.base; - if (!diffInfo || !diffInfo.diff_header) { return []; } - return diffInfo.diff_header - .filter(item => !(item.startsWith('diff --git ') || - item.startsWith('index ') || - item.startsWith('+++ ') || - item.startsWith('--- ') || - item === 'Binary files differ')); - } - - /** @return {boolean} */ - _computeDiffHeaderHidden(items) { - return items.length === 0; - } - - _handleFullBypass() { - this._safetyBypass = FULL_CONTEXT; - this._debounceRenderDiffTable(); - } - - _handleLimitedBypass() { - this._safetyBypass = LIMITED_CONTEXT; - this._debounceRenderDiffTable(); - } - - /** @return {string} */ - _computeWarningClass(showWarning) { - return showWarning ? 'warn' : ''; - } - - /** - * @param {string} errorMessage - * @return {string} - */ - _computeErrorClass(errorMessage) { - return errorMessage ? 'showError' : ''; - } - - expandAllContext() { - this._handleFullBypass(); - } - - /** - * @param {!boolean} warnLeft - * @param {!boolean} warnRight - * @return {string|null} - */ - _computeNewlineWarning(warnLeft, warnRight) { - const messages = []; - if (warnLeft) { - messages.push(NO_NEWLINE_BASE); - } - if (warnRight) { - messages.push(NO_NEWLINE_REVISION); - } - if (!messages.length) { return null; } - return messages.join(' — '); - } - - /** - * @param {string} warning - * @param {boolean} loading - * @return {string} - */ - _computeNewlineWarningClass(warning, loading) { - if (loading || !warning) { return 'newlineWarning hidden'; } - return 'newlineWarning'; - } - - /** - * Get the approximate length of the diff as the sum of the maximum - * length of the chunks. - * - * @param {Object} diff object - * @return {number} - */ - getDiffLength(diff) { - if (!diff) return 0; - return diff.content.reduce((sum, sec) => { - if (sec.hasOwnProperty('ab')) { - return sum + sec.ab.length; - } else { - return sum + Math.max( - sec.hasOwnProperty('a') ? sec.a.length : 0, - sec.hasOwnProperty('b') ? sec.b.length : 0); - } - }, 0); + if (differ) { + this._prefsChanged(newPrefs); } } - customElements.define(GrDiff.is, GrDiff); -})(); + _pathObserver() { + // Call _prefsChanged(), because line-limit style value depends on path. + this._prefsChanged(this.prefs); + } + + _viewModeObserver() { + this._prefsChanged(this.prefs); + } + + /** @param {boolean} newValue */ + _loadingChanged(newValue) { + if (newValue) { + this.cancel(); + this._blame = null; + this._safetyBypass = null; + this._showWarning = false; + this.clearDiffContent(); + } + } + + _lineWrappingObserver() { + this._prefsChanged(this.prefs); + } + + _prefsChanged(prefs) { + if (!prefs) { return; } + + this._blame = null; + + const lineLength = this.path === COMMIT_MSG_PATH ? + COMMIT_MSG_LINE_LENGTH : prefs.line_length; + const stylesToUpdate = {}; + + if (prefs.line_wrapping) { + this._diffTableClass = 'full-width'; + if (this.viewMode === 'SIDE_BY_SIDE') { + stylesToUpdate['--content-width'] = 'none'; + stylesToUpdate['--line-limit'] = lineLength + 'ch'; + } + } else { + this._diffTableClass = ''; + stylesToUpdate['--content-width'] = lineLength + 'ch'; + } + + if (prefs.font_size) { + stylesToUpdate['--font-size'] = prefs.font_size + 'px'; + } + + this.updateStyles(stylesToUpdate); + + if (this.diff && !this.noRenderOnPrefsChange) { + this._debounceRenderDiffTable(); + } + } + + _diffChanged(newValue) { + if (newValue) { + this._diffLength = this.getDiffLength(newValue); + this._debounceRenderDiffTable(); + } + } + + /** + * When called multiple times from the same microtask, will call + * _renderDiffTable only once, in the next microtask, unless it is cancelled + * before that microtask runs. + * + * This should be used instead of calling _renderDiffTable directly to + * render the diff in response to an input change, because there may be + * multiple inputs changing in the same microtask, but we only want to + * render once. + */ + _debounceRenderDiffTable() { + this.debounce( + RENDER_DIFF_TABLE_DEBOUNCE_NAME, () => this._renderDiffTable()); + } + + _renderDiffTable() { + this._unobserveIncrementalNodes(); + if (!this.prefs) { + this.dispatchEvent( + new CustomEvent('render', {bubbles: true, composed: true})); + return; + } + if (this.prefs.context === -1 && + this._diffLength >= LARGE_DIFF_THRESHOLD_LINES && + this._safetyBypass === null) { + this._showWarning = true; + this.dispatchEvent( + new CustomEvent('render', {bubbles: true, composed: true})); + return; + } + + this._showWarning = false; + + const keyLocations = this._computeKeyLocations(); + this.$.diffBuilder.render(keyLocations, this._getBypassPrefs()) + .then(() => { + this.dispatchEvent( + new CustomEvent('render', { + bubbles: true, + composed: true, + detail: {contentRendered: true}, + })); + }); + } + + _handleRenderContent() { + this._incrementalNodeObserver = dom(this).observeNodes(info => { + const addedThreadEls = info.addedNodes.filter(isThreadEl); + // Removed nodes do not need to be handled because all this code does is + // adding a slot for the added thread elements, and the extra slots do + // not hurt. It's probably a bigger performance cost to remove them than + // to keep them around. Medium term we can even consider to add one slot + // for each line from the start. + let lastEl; + for (const threadEl of addedThreadEls) { + const lineNumString = threadEl.getAttribute('line-num') || 'FILE'; + const commentSide = threadEl.getAttribute('comment-side'); + const lineEl = this.$.diffBuilder.getLineElByNumber( + lineNumString, commentSide); + const contentText = this.$.diffBuilder.getContentByLineEl(lineEl); + const contentEl = contentText.parentElement; + const threadGroupEl = this._getOrCreateThreadGroup( + contentEl, commentSide); + // Create a slot for the thread and attach it to the thread group. + // The Polyfill has some bugs and this only works if the slot is + // attached to the group after the group is attached to the DOM. + // The thread group may already have a slot with the right name, but + // that is okay because the first matching slot is used and the rest + // are ignored. + const slot = document.createElement('slot'); + slot.name = threadEl.getAttribute('slot'); + dom(threadGroupEl).appendChild(Gerrit.slotToContent(slot)); + lastEl = threadEl; + } + + // Safari is not binding newly created comment-thread + // with the slot somehow, replace itself will rebind it + // @see Issue 11182 + if (lastEl && lastEl.replaceWith) { + lastEl.replaceWith(lastEl); + } + }); + } + + _unobserveIncrementalNodes() { + if (this._incrementalNodeObserver) { + dom(this).unobserveNodes(this._incrementalNodeObserver); + } + } + + _unobserveNodes() { + if (this._nodeObserver) { + dom(this).unobserveNodes(this._nodeObserver); + } + } + + /** + * Get the preferences object including the safety bypass context (if any). + */ + _getBypassPrefs() { + if (this._safetyBypass !== null) { + return Object.assign({}, this.prefs, {context: this._safetyBypass}); + } + return this.prefs; + } + + clearDiffContent() { + this._unobserveIncrementalNodes(); + this.$.diffTable.innerHTML = null; + } + + /** @return {!Array} */ + _computeDiffHeaderItems(diffInfoRecord) { + const diffInfo = diffInfoRecord.base; + if (!diffInfo || !diffInfo.diff_header) { return []; } + return diffInfo.diff_header + .filter(item => !(item.startsWith('diff --git ') || + item.startsWith('index ') || + item.startsWith('+++ ') || + item.startsWith('--- ') || + item === 'Binary files differ')); + } + + /** @return {boolean} */ + _computeDiffHeaderHidden(items) { + return items.length === 0; + } + + _handleFullBypass() { + this._safetyBypass = FULL_CONTEXT; + this._debounceRenderDiffTable(); + } + + _handleLimitedBypass() { + this._safetyBypass = LIMITED_CONTEXT; + this._debounceRenderDiffTable(); + } + + /** @return {string} */ + _computeWarningClass(showWarning) { + return showWarning ? 'warn' : ''; + } + + /** + * @param {string} errorMessage + * @return {string} + */ + _computeErrorClass(errorMessage) { + return errorMessage ? 'showError' : ''; + } + + expandAllContext() { + this._handleFullBypass(); + } + + /** + * @param {!boolean} warnLeft + * @param {!boolean} warnRight + * @return {string|null} + */ + _computeNewlineWarning(warnLeft, warnRight) { + const messages = []; + if (warnLeft) { + messages.push(NO_NEWLINE_BASE); + } + if (warnRight) { + messages.push(NO_NEWLINE_REVISION); + } + if (!messages.length) { return null; } + return messages.join(' — '); + } + + /** + * @param {string} warning + * @param {boolean} loading + * @return {string} + */ + _computeNewlineWarningClass(warning, loading) { + if (loading || !warning) { return 'newlineWarning hidden'; } + return 'newlineWarning'; + } + + /** + * Get the approximate length of the diff as the sum of the maximum + * length of the chunks. + * + * @param {Object} diff object + * @return {number} + */ + getDiffLength(diff) { + if (!diff) return 0; + return diff.content.reduce((sum, sec) => { + if (sec.hasOwnProperty('ab')) { + return sum + sec.ab.length; + } else { + return sum + Math.max( + sec.hasOwnProperty('a') ? sec.a.length : 0, + sec.hasOwnProperty('b') ? sec.b.length : 0); + } + }, 0); + } +} + +customElements.define(GrDiff.is, GrDiff);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js index 38f6265..c6a7e98 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.js
@@ -1,37 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../gr-diff-builder/gr-diff-builder-element.html"> -<link rel="import" href="../gr-diff-highlight/gr-diff-highlight.html"> -<link rel="import" href="../gr-diff-selection/gr-diff-selection.html"> -<link rel="import" href="../gr-syntax-themes/gr-syntax-theme.html"> -<link rel="import" href="../gr-ranged-comment-themes/gr-ranged-comment-theme.html"> - -<script src="../../../scripts/hiddenscroll.js"></script> -<script src="gr-diff-line.js"></script> -<script src="gr-diff-group.js"></script> - -<dom-module id="gr-diff"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host(.no-left) .sideBySide .left, :host(.no-left) .sideBySide .left + td, @@ -184,7 +169,7 @@ .content .contentText:empty:after { /* Newline, to ensure empty lines are one line-height tall. */ - content: '\A'; + content: '\\A'; } .contextControl { background-color: var(--diff-context-control-background-color); @@ -214,7 +199,7 @@ } .br:after { /* Line feed */ - content: '\A'; + content: '\\A'; } .tab { display: inline-block; @@ -222,7 +207,7 @@ .tab-indicator:before { color: var(--diff-tab-indicator-color); /* >> character */ - content: '\00BB'; + content: '\\00BB'; position: absolute; } /* Is defined after other background-colors, such that this @@ -363,38 +348,16 @@ <style include="gr-ranged-comment-theme"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <div id="diffHeader" hidden$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]"> - <template - is="dom-repeat" - items="[[_diffHeaderItems]]"> + <div id="diffHeader" hidden\$="[[_computeDiffHeaderHidden(_diffHeaderItems)]]"> + <template is="dom-repeat" items="[[_diffHeaderItems]]"> <div>[[item]]</div> </template> </div> - <div class$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]" - on-tap="_handleTap"> + <div class\$="[[_computeContainerClass(loggedIn, viewMode, displayLine)]]" on-tap="_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]]" - project-name="[[projectName]]" - diff="[[diff]]" - path="[[path]]" - change-num="[[changeNum]]" - patch-num="[[patchRange.patchNum]]" - view-mode="[[viewMode]]" - is-image-diff="[[isImageDiff]]" - base-image="[[baseImage]]" - layers="[[layers]]" - revision-image="[[revisionImage]]"> - <table - id="diffTable" - class$="[[_diffTableClass]]" - role="presentation"></table> + <gr-diff-highlight id="highlights" logged-in="[[loggedIn]]" comment-ranges="{{_commentRanges}}"> + <gr-diff-builder id="diffBuilder" comment-ranges="[[_commentRanges]]" coverage-ranges="[[coverageRanges]]" project-name="[[projectName]]" diff="[[diff]]" path="[[path]]" change-num="[[changeNum]]" patch-num="[[patchRange.patchNum]]" view-mode="[[viewMode]]" is-image-diff="[[isImageDiff]]" base-image="[[baseImage]]" layers="[[layers]]" revision-image="[[revisionImage]]"> + <table id="diffTable" class\$="[[_diffTableClass]]" role="presentation"></table> <template is="dom-if" if="[[showNoChangeMessage(loading, prefs, _diffLength)]]"> <div class="whitespace-change-only-message"> @@ -406,13 +369,13 @@ </gr-diff-highlight> </gr-diff-selection> </div> - <div class$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]"> + <div class\$="[[_computeNewlineWarningClass(_newlineWarning, loading)]]"> [[_newlineWarning]] </div> - <div id="loadingError" class$="[[_computeErrorClass(errorMessage)]]"> + <div id="loadingError" class\$="[[_computeErrorClass(errorMessage)]]"> [[errorMessage]] </div> - <div id="sizeWarning" class$="[[_computeWarningClass(_showWarning)]]"> + <div id="sizeWarning" class\$="[[_computeWarningClass(_showWarning)]]"> <p> Prevented render because "Whole file" is enabled and this diff is very large (about [[_diffLength]] lines). @@ -424,6 +387,4 @@ Render anyway (may be slow) </gr-button> </div> - </template> - <script src="gr-diff.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html index ba46549..884c729 100644 --- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html +++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -19,20 +19,28 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> <script src="/components/web-component-tester/data/a11ySuite.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> -<link rel="import" href="gr-diff.html"> +<script type="module" src="../../shared/gr-rest-api-interface/gr-rest-api-interface.js"></script> +<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script> +<script type="module" src="./gr-diff.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-diff.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -40,932 +48,248 @@ </template> </test-fixture> -<script> - suite('gr-diff tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-diff.js'; +import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-diff tests', () => { + let element; + let sandbox; - const MINIMAL_PREFS = {tab_size: 2, line_length: 80}; + const MINIMAL_PREFS = {tab_size: 2, line_length: 80}; + + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('selectionchange event handling', () => { + const emulateSelection = function() { + document.dispatchEvent(new CustomEvent('selectionchange')); + }; setup(() => { - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - suite('selectionchange event handling', () => { - const emulateSelection = function() { - document.dispatchEvent(new CustomEvent('selectionchange')); - }; - - setup(() => { - element = fixture('basic'); - sandbox.stub(element.$.highlights, 'handleSelectionChange'); - }); - - test('enabled if logged in', () => { - element.loggedIn = true; - emulateSelection(); - assert.isTrue(element.$.highlights.handleSelectionChange.called); - }); - - test('ignored if logged out', () => { - element.loggedIn = false; - emulateSelection(); - assert.isFalse(element.$.highlights.handleSelectionChange.called); - }); - }); - - test('cancel', () => { element = fixture('basic'); - const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel'); - element.cancel(); - assert.isTrue(cancelStub.calledOnce); + sandbox.stub(element.$.highlights, 'handleSelectionChange'); }); - test('line limit with line_wrapping', () => { + test('enabled if logged in', () => { + element.loggedIn = true; + emulateSelection(); + assert.isTrue(element.$.highlights.handleSelectionChange.called); + }); + + test('ignored if logged out', () => { + element.loggedIn = false; + emulateSelection(); + assert.isFalse(element.$.highlights.handleSelectionChange.called); + }); + }); + + test('cancel', () => { + element = fixture('basic'); + const cancelStub = sandbox.stub(element.$.diffBuilder, 'cancel'); + element.cancel(); + assert.isTrue(cancelStub.calledOnce); + }); + + test('line limit with line_wrapping', () => { + element = fixture('basic'); + element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true}); + flushAsynchronousOperations(); + assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch'); + }); + + test('line limit without line_wrapping', () => { + element = fixture('basic'); + element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false}); + flushAsynchronousOperations(); + assert.isNotOk(util.getComputedStyleValue('--line-limit', element)); + }); + + suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => { + let lineEl; + let contentEl; + + setup(() => { element = fixture('basic'); - element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true}); - flushAsynchronousOperations(); - assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch'); + lineEl = document.createElement('td'); + contentEl = document.createElement('span'); }); - test('line limit without line_wrapping', () => { - element = fixture('basic'); - element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false}); - flushAsynchronousOperations(); - assert.isNotOk(util.getComputedStyleValue('--line-limit', element)); - }); - - suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => { - let lineEl; - let contentEl; - - setup(() => { - element = fixture('basic'); - lineEl = document.createElement('td'); - contentEl = document.createElement('span'); + suite('_getPatchNumByLineAndContent', () => { + test('right side', () => { + element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; + lineEl.classList.add('right'); + assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), + 4); }); - suite('_getPatchNumByLineAndContent', () => { - test('right side', () => { - element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; - lineEl.classList.add('right'); - assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), - 4); - }); - - test('left side parent by linenum', () => { - element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; - lineEl.classList.add('left'); - assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), - 4); - }); - - test('left side parent by content', () => { - element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; - contentEl.classList.add('remove'); - assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), - 4); - }); - - test('left side merge parent', () => { - element.patchRange = {patchNum: 4, basePatchNum: -2}; - contentEl.classList.add('remove'); - assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), - 4); - }); - - test('left side non parent', () => { - element.patchRange = {patchNum: 4, basePatchNum: 3}; - contentEl.classList.add('remove'); - assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), - 3); - }); + test('left side parent by linenum', () => { + element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; + lineEl.classList.add('left'); + assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), + 4); }); - suite('_getIsParentCommentByLineAndContent', () => { - test('right side', () => { - element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; - lineEl.classList.add('right'); - assert.isFalse( - element._getIsParentCommentByLineAndContent(lineEl, contentEl)); - }); + test('left side parent by content', () => { + element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; + contentEl.classList.add('remove'); + assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), + 4); + }); - test('left side parent by linenum', () => { - element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; - lineEl.classList.add('left'); - assert.isTrue( - element._getIsParentCommentByLineAndContent(lineEl, contentEl)); - }); + test('left side merge parent', () => { + element.patchRange = {patchNum: 4, basePatchNum: -2}; + contentEl.classList.add('remove'); + assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), + 4); + }); - test('left side parent by content', () => { - element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; - contentEl.classList.add('remove'); - assert.isTrue( - element._getIsParentCommentByLineAndContent(lineEl, contentEl)); - }); - - test('left side merge parent', () => { - element.patchRange = {patchNum: 4, basePatchNum: -2}; - contentEl.classList.add('remove'); - assert.isTrue( - element._getIsParentCommentByLineAndContent(lineEl, contentEl)); - }); - - test('left side non parent', () => { - element.patchRange = {patchNum: 4, basePatchNum: 3}; - contentEl.classList.add('remove'); - assert.isFalse( - element._getIsParentCommentByLineAndContent(lineEl, contentEl)); - }); + test('left side non parent', () => { + element.patchRange = {patchNum: 4, basePatchNum: 3}; + contentEl.classList.add('remove'); + assert.equal(element._getPatchNumByLineAndContent(lineEl, contentEl), + 3); }); }); - suite('not logged in', () => { - setup(() => { - const getLoggedInPromise = Promise.resolve(false); - stub('gr-rest-api-interface', { - getLoggedIn() { return getLoggedInPromise; }, - }); - element = fixture('basic'); - return getLoggedInPromise; - }); - - test('toggleLeftDiff', () => { - element.toggleLeftDiff(); - assert.isTrue(element.classList.contains('no-left')); - element.toggleLeftDiff(); - assert.isFalse(element.classList.contains('no-left')); - }); - - test('addDraftAtLine', () => { - sandbox.stub(element, '_selectLine'); - const loggedInErrorSpy = sandbox.spy(); - element.addEventListener('show-auth-required', loggedInErrorSpy); - element.addDraftAtLine(); - assert.isTrue(loggedInErrorSpy.called); - }); - - test('view does not start with displayLine classList', () => { + suite('_getIsParentCommentByLineAndContent', () => { + test('right side', () => { + element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; + lineEl.classList.add('right'); assert.isFalse( - element.shadowRoot - .querySelector('.diffContainer') - .classList - .contains('displayLine')); + element._getIsParentCommentByLineAndContent(lineEl, contentEl)); }); - test('displayLine class added called when displayLine is true', () => { - const spy = sandbox.spy(element, '_computeContainerClass'); - element.displayLine = true; - assert.isTrue(spy.called); + test('left side parent by linenum', () => { + element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; + lineEl.classList.add('left'); assert.isTrue( - element.shadowRoot - .querySelector('.diffContainer') - .classList - .contains('displayLine')); + element._getIsParentCommentByLineAndContent(lineEl, contentEl)); }); - test('thread groups', () => { - const contentEl = document.createElement('div'); - - element.changeNum = 123; - element.patchRange = {basePatchNum: 1, patchNum: 2}; - element.path = 'file.txt'; - - const mock = document.createElement('mock-diff-response'); - element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder( - mock.diffResponse, Object.assign({}, MINIMAL_PREFS)); - - // No thread groups. - assert.isNotOk(element._getThreadGroupForLine(contentEl)); - - // A thread group gets created. - const threadGroupEl = element._getOrCreateThreadGroup(contentEl); - assert.isOk(threadGroupEl); - - // The new thread group can be fetched. - assert.isOk(element._getThreadGroupForLine(contentEl)); - - assert.equal(contentEl.querySelectorAll('.thread-group').length, 1); + test('left side parent by content', () => { + element.patchRange = {patchNum: 4, basePatchNum: 'PARENT'}; + contentEl.classList.add('remove'); + assert.isTrue( + element._getIsParentCommentByLineAndContent(lineEl, contentEl)); }); - suite('image diffs', () => { - let mockFile1; - let mockFile2; - setup(() => { - mockFile1 = { - body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + - 'wsAAAAAAAAAAAAAAAAA/w==', - type: 'image/bmp', - }; - mockFile2 = { - body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + - 'wsAAAAAAAAAAAAA/////w==', - type: 'image/bmp', - }; - - element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; - element.isImageDiff = true; - element.prefs = { - auto_hide_diff_table_header: true, - context: 10, - cursor_blink_rate: 0, - font_size: 12, - ignore_whitespace: 'IGNORE_NONE', - intraline_difference: true, - line_length: 100, - line_wrapping: false, - show_line_endings: true, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - tab_size: 8, - theme: 'DEFAULT', - }; - }); - - test('renders image diffs with same file name', done => { - const rendered = () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diffBuilder._builder, GrDiffBuilderImage); - - // Left image rendered with the parent commit's version of the file. - const leftImage = element.$.diffTable.querySelector('td.left img'); - const leftLabel = - element.$.diffTable.querySelector('td.left label'); - const leftLabelContent = leftLabel.querySelector('.label'); - const leftLabelName = leftLabel.querySelector('.name'); - - const rightImage = - element.$.diffTable.querySelector('td.right img'); - const rightLabel = element.$.diffTable.querySelector( - 'td.right label'); - const rightLabelContent = rightLabel.querySelector('.label'); - const rightLabelName = rightLabel.querySelector('.name'); - - assert.isNotOk(rightLabelName); - assert.isNotOk(leftLabelName); - - let leftLoaded = false; - let rightLoaded = false; - - leftImage.addEventListener('load', () => { - assert.isOk(leftImage); - assert.equal(leftImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile1.body); - assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); - leftLoaded = true; - if (rightLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - - rightImage.addEventListener('load', () => { - assert.isOk(rightImage); - assert.equal(rightImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile2.body); - assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); - - rightLoaded = true; - if (leftLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - }; - - element.addEventListener('render', rendered); - - element.baseImage = mockFile1; - element.revisionImage = mockFile2; - element.diff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'MODIFIED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index 2adc47d..f9c2f2c 100644', - '--- a/carrot.jpg', - '+++ b/carrot.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - }); - - test('renders image diffs with a different file name', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'MODIFIED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot2.jpg', - 'index 2adc47d..f9c2f2c 100644', - '--- a/carrot.jpg', - '+++ b/carrot2.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - - const rendered = () => { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diffBuilder._builder, GrDiffBuilderImage); - - // Left image rendered with the parent commit's version of the file. - const leftImage = element.$.diffTable.querySelector('td.left img'); - const leftLabel = - element.$.diffTable.querySelector('td.left label'); - const leftLabelContent = leftLabel.querySelector('.label'); - const leftLabelName = leftLabel.querySelector('.name'); - - const rightImage = - element.$.diffTable.querySelector('td.right img'); - const rightLabel = element.$.diffTable.querySelector( - 'td.right label'); - const rightLabelContent = rightLabel.querySelector('.label'); - const rightLabelName = rightLabel.querySelector('.name'); - - assert.isOk(rightLabelName); - assert.isOk(leftLabelName); - assert.equal(leftLabelName.textContent, mockDiff.meta_a.name); - assert.equal(rightLabelName.textContent, mockDiff.meta_b.name); - - let leftLoaded = false; - let rightLoaded = false; - - leftImage.addEventListener('load', () => { - assert.isOk(leftImage); - assert.equal(leftImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile1.body); - assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); - leftLoaded = true; - if (rightLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - - rightImage.addEventListener('load', () => { - assert.isOk(rightImage); - assert.equal(rightImage.getAttribute('src'), - 'data:image/bmp;base64, ' + mockFile2.body); - assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); - - rightLoaded = true; - if (leftLoaded) { - element.removeEventListener('render', rendered); - done(); - } - }); - }; - - element.addEventListener('render', rendered); - - element.baseImage = mockFile1; - element.baseImage._name = mockDiff.meta_a.name; - element.revisionImage = mockFile2; - element.revisionImage._name = mockDiff.meta_b.name; - element.diff = mockDiff; - }); - - test('renders added image', done => { - const mockDiff = { - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'ADDED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index 0000000..f9c2f2c 100644', - '--- /dev/null', - '+++ b/carrot.jpg', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - - function rendered() { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diffBuilder._builder, GrDiffBuilderImage); - - const leftImage = element.$.diffTable.querySelector('td.left img'); - const rightImage = element.$.diffTable.querySelector('td.right img'); - - assert.isNotOk(leftImage); - assert.isOk(rightImage); - done(); - element.removeEventListener('render', rendered); - } - element.addEventListener('render', rendered); - - element.revisionImage = mockFile2; - element.diff = mockDiff; - }); - - test('renders removed image', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - intraline_status: 'OK', - change_type: 'DELETED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index f9c2f2c..0000000 100644', - '--- a/carrot.jpg', - '+++ /dev/null', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - - function rendered() { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diffBuilder._builder, GrDiffBuilderImage); - - const leftImage = element.$.diffTable.querySelector('td.left img'); - const rightImage = element.$.diffTable.querySelector('td.right img'); - - assert.isOk(leftImage); - assert.isNotOk(rightImage); - done(); - element.removeEventListener('render', rendered); - } - element.addEventListener('render', rendered); - - element.baseImage = mockFile1; - element.diff = mockDiff; - }); - - test('does not render disallowed image type', done => { - const mockDiff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil', - lines: 560}, - intraline_status: 'OK', - change_type: 'DELETED', - diff_header: [ - 'diff --git a/carrot.jpg b/carrot.jpg', - 'index f9c2f2c..0000000 100644', - '--- a/carrot.jpg', - '+++ /dev/null', - 'Binary files differ', - ], - content: [{skip: 66}], - binary: true, - }; - mockFile1.type = 'image/jpeg-evil'; - - function rendered() { - // Recognizes that it should be an image diff. - assert.isTrue(element.isImageDiff); - assert.instanceOf( - element.$.diffBuilder._builder, GrDiffBuilderImage); - const leftImage = element.$.diffTable.querySelector('td.left img'); - assert.isNotOk(leftImage); - done(); - element.removeEventListener('render', rendered); - } - element.addEventListener('render', rendered); - - element.baseImage = mockFile1; - element.diff = mockDiff; - }); + test('left side merge parent', () => { + element.patchRange = {patchNum: 4, basePatchNum: -2}; + contentEl.classList.add('remove'); + assert.isTrue( + element._getIsParentCommentByLineAndContent(lineEl, contentEl)); }); - test('_handleTap lineNum', done => { - const addDraftStub = sandbox.stub(element, 'addDraftAtLine'); - const el = document.createElement('div'); - el.className = 'lineNum'; - el.addEventListener('click', e => { - element._handleTap(e); - assert.isTrue(addDraftStub.called); - assert.equal(addDraftStub.lastCall.args[0], el); - done(); - }); - el.click(); + test('left side non parent', () => { + element.patchRange = {patchNum: 4, basePatchNum: 3}; + contentEl.classList.add('remove'); + assert.isFalse( + element._getIsParentCommentByLineAndContent(lineEl, contentEl)); }); + }); + }); - test('_handleTap context', done => { - const showContextStub = - sandbox.stub(element.$.diffBuilder, 'showContext'); - const el = document.createElement('div'); - el.className = 'showContext'; - el.addEventListener('click', e => { - element._handleTap(e); - assert.isTrue(showContextStub.called); - done(); - }); - el.click(); + suite('not logged in', () => { + setup(() => { + const getLoggedInPromise = Promise.resolve(false); + stub('gr-rest-api-interface', { + getLoggedIn() { return getLoggedInPromise; }, }); + element = fixture('basic'); + return getLoggedInPromise; + }); - test('_handleTap content', done => { - const content = document.createElement('div'); - const lineEl = document.createElement('div'); + test('toggleLeftDiff', () => { + element.toggleLeftDiff(); + assert.isTrue(element.classList.contains('no-left')); + element.toggleLeftDiff(); + assert.isFalse(element.classList.contains('no-left')); + }); - const selectStub = sandbox.stub(element, '_selectLine'); - sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl); + test('addDraftAtLine', () => { + sandbox.stub(element, '_selectLine'); + const loggedInErrorSpy = sandbox.spy(); + element.addEventListener('show-auth-required', loggedInErrorSpy); + element.addDraftAtLine(); + assert.isTrue(loggedInErrorSpy.called); + }); - content.className = 'content'; - content.addEventListener('click', e => { - element._handleTap(e); - assert.isTrue(selectStub.called); - assert.equal(selectStub.lastCall.args[0], lineEl); - done(); - }); - content.click(); - }); + test('view does not start with displayLine classList', () => { + assert.isFalse( + element.shadowRoot + .querySelector('.diffContainer') + .classList + .contains('displayLine')); + }); - suite('getCursorStops', () => { - const setupDiff = function() { - const mock = document.createElement('mock-diff-response'); - element.diff = mock.diffResponse; - element.prefs = { - context: 10, - tab_size: 8, - font_size: 12, - line_length: 100, - cursor_blink_rate: 0, - line_wrapping: false, - intraline_difference: true, - show_line_endings: true, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - auto_hide_diff_table_header: true, - theme: 'DEFAULT', - ignore_whitespace: 'IGNORE_NONE', - }; + test('displayLine class added called when displayLine is true', () => { + const spy = sandbox.spy(element, '_computeContainerClass'); + element.displayLine = true; + assert.isTrue(spy.called); + assert.isTrue( + element.shadowRoot + .querySelector('.diffContainer') + .classList + .contains('displayLine')); + }); - element._renderDiffTable(); - flushAsynchronousOperations(); + test('thread groups', () => { + const contentEl = document.createElement('div'); + + element.changeNum = 123; + element.patchRange = {basePatchNum: 1, patchNum: 2}; + element.path = 'file.txt'; + + const mock = document.createElement('mock-diff-response'); + element.$.diffBuilder._builder = element.$.diffBuilder._getDiffBuilder( + mock.diffResponse, Object.assign({}, MINIMAL_PREFS)); + + // No thread groups. + assert.isNotOk(element._getThreadGroupForLine(contentEl)); + + // A thread group gets created. + const threadGroupEl = element._getOrCreateThreadGroup(contentEl); + assert.isOk(threadGroupEl); + + // The new thread group can be fetched. + assert.isOk(element._getThreadGroupForLine(contentEl)); + + assert.equal(contentEl.querySelectorAll('.thread-group').length, 1); + }); + + suite('image diffs', () => { + let mockFile1; + let mockFile2; + setup(() => { + mockFile1 = { + body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + + 'wsAAAAAAAAAAAAAAAAA/w==', + type: 'image/bmp', + }; + mockFile2 = { + body: 'Qk06AAAAAAAAADYAAAAoAAAAAQAAAP////8BACAAAAAAAAAAAAATCwAAE' + + 'wsAAAAAAAAAAAAA/////w==', + type: 'image/bmp', }; - test('getCursorStops returns [] when hidden and noAutoRender', () => { - element.noAutoRender = true; - setupDiff(); - element.hidden = true; - assert.equal(element.getCursorStops().length, 0); - }); - - test('getCursorStops', () => { - setupDiff(); - assert.equal(element.getCursorStops().length, 50); - }); - }); - - test('adds .hiddenscroll', () => { - Gerrit.hiddenscroll = true; - element.displayLine = true; - assert.include(element.shadowRoot - .querySelector('.diffContainer').className, 'hiddenscroll'); - }); - }); - - suite('logged in', () => { - let fakeLineEl; - setup(() => { - element = fixture('basic'); - element.loggedIn = true; - element.patchRange = {}; - - fakeLineEl = { - getAttribute: sandbox.stub().returns(42), - classList: { - contains: sandbox.stub().returns(true), - }, - }; - }); - - test('addDraftAtLine', () => { - sandbox.stub(element, '_selectLine'); - sandbox.stub(element, '_createComment'); - element.addDraftAtLine(fakeLineEl); - assert.isTrue(element._createComment - .calledWithExactly(fakeLineEl, 42)); - }); - - test('addDraftAtLine on an edit', () => { - element.patchRange.basePatchNum = element.EDIT_NAME; - sandbox.stub(element, '_selectLine'); - sandbox.stub(element, '_createComment'); - const alertSpy = sandbox.spy(); - element.addEventListener('show-alert', alertSpy); - element.addDraftAtLine(fakeLineEl); - assert.isTrue(alertSpy.called); - assert.isFalse(element._createComment.called); - }); - - test('addDraftAtLine on an edit base', () => { - element.patchRange.patchNum = element.EDIT_NAME; - element.patchRange.basePatchNum = element.PARENT_NAME; - sandbox.stub(element, '_selectLine'); - sandbox.stub(element, '_createComment'); - const alertSpy = sandbox.spy(); - element.addEventListener('show-alert', alertSpy); - element.addDraftAtLine(fakeLineEl); - assert.isTrue(alertSpy.called); - assert.isFalse(element._createComment.called); - }); - - suite('change in preferences', () => { - setup(() => { - element.diff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - diff_header: [], - intraline_status: 'OK', - change_type: 'MODIFIED', - content: [{skip: 66}], - }; - element.flushDebouncer('renderDiffTable'); - }); - - test('change in preferences re-renders diff', () => { - sandbox.stub(element, '_renderDiffTable'); - element.prefs = Object.assign( - {}, MINIMAL_PREFS, {time_format: 'HHMM_12'}); - element.flushDebouncer('renderDiffTable'); - assert.isTrue(element._renderDiffTable.called); - }); - - test('change in preferences does not re-renders diff with ' + - 'noRenderOnPrefsChange', () => { - sandbox.stub(element, '_renderDiffTable'); - element.noRenderOnPrefsChange = true; - element.prefs = Object.assign( - {}, MINIMAL_PREFS, {time_format: 'HHMM_12'}); - element.flushDebouncer('renderDiffTable'); - assert.isFalse(element._renderDiffTable.called); - }); - }); - }); - - suite('diff header', () => { - setup(() => { - element = fixture('basic'); - element.diff = { - meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, - meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', - lines: 560}, - diff_header: [], - intraline_status: 'OK', - change_type: 'MODIFIED', - content: [{skip: 66}], - }; - }); - - test('hidden', () => { - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg'); - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644'); - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', '--- a/test.jpg'); - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', '+++ b/test.jpg'); - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', 'test'); - assert.equal(element._diffHeaderItems.length, 1); - flushAsynchronousOperations(); - - assert.equal(element.$.diffHeader.textContent.trim(), 'test'); - }); - - test('binary files', () => { - element.diff.binary = true; - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg'); - assert.equal(element._diffHeaderItems.length, 0); - element.push('diff.diff_header', 'test'); - assert.equal(element._diffHeaderItems.length, 1); - element.push('diff.diff_header', 'Binary files differ'); - assert.equal(element._diffHeaderItems.length, 1); - }); - }); - - suite('safety and bypass', () => { - let renderStub; - - setup(() => { - element = fixture('basic'); - renderStub = sandbox.stub(element.$.diffBuilder, 'render', - () => { - element.$.diffBuilder.dispatchEvent( - new CustomEvent('render', {bubbles: true, composed: true})); - return Promise.resolve({}); - }); - const mock = document.createElement('mock-diff-response'); - sandbox.stub(element, 'getDiffLength').returns(10000); - element.diff = mock.diffResponse; - element.noRenderOnPrefsChange = true; - }); - - test('large render w/ context = 10', done => { - element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10}); - function rendered() { - assert.isTrue(renderStub.called); - assert.isFalse(element._showWarning); - done(); - element.removeEventListener('render', rendered); - } - element.addEventListener('render', rendered); - element._renderDiffTable(); - }); - - test('large render w/ whole file and bypass', done => { - element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1}); - element._safetyBypass = 10; - function rendered() { - assert.isTrue(renderStub.called); - assert.isFalse(element._showWarning); - done(); - element.removeEventListener('render', rendered); - } - element.addEventListener('render', rendered); - element._renderDiffTable(); - }); - - test('large render w/ whole file and no bypass', done => { - element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1}); - function rendered() { - assert.isFalse(renderStub.called); - assert.isTrue(element._showWarning); - done(); - element.removeEventListener('render', rendered); - } - element.addEventListener('render', rendered); - element._renderDiffTable(); - }); - }); - - suite('blame', () => { - setup(() => { - element = fixture('basic'); - }); - - test('unsetting', () => { - element.blame = []; - const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame'); - element.classList.add('showBlame'); - element.blame = null; - assert.isTrue(setBlameSpy.calledWithExactly(null)); - assert.isFalse(element.classList.contains('showBlame')); - }); - - test('setting', () => { - const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}]; - element.blame = mockBlame; - assert.isTrue(element.classList.contains('showBlame')); - }); - }); - - suite('trailing newline warnings', () => { - const NO_NEWLINE_BASE = 'No newline at end of base file.'; - const NO_NEWLINE_REVISION = 'No newline at end of revision file.'; - - const getWarning = element => - element.shadowRoot.querySelector('.newlineWarning').textContent; - - setup(() => { - element = fixture('basic'); - element.showNewlineWarningLeft = false; - element.showNewlineWarningRight = false; - }); - - test('shows combined warning if both sides set to warn', () => { - element.showNewlineWarningLeft = true; - element.showNewlineWarningRight = true; - assert.include(getWarning(element), - NO_NEWLINE_BASE + ' — ' + NO_NEWLINE_REVISION); - }); - - suite('showNewlineWarningLeft', () => { - test('show warning if true', () => { - element.showNewlineWarningLeft = true; - assert.include(getWarning(element), NO_NEWLINE_BASE); - }); - - test('hide warning if false', () => { - element.showNewlineWarningLeft = false; - assert.notInclude(getWarning(element), NO_NEWLINE_BASE); - }); - - test('hide warning if undefined', () => { - element.showNewlineWarningLeft = undefined; - assert.notInclude(getWarning(element), NO_NEWLINE_BASE); - }); - }); - - suite('showNewlineWarningRight', () => { - test('show warning if true', () => { - element.showNewlineWarningRight = true; - assert.include(getWarning(element), NO_NEWLINE_REVISION); - }); - - test('hide warning if false', () => { - element.showNewlineWarningRight = false; - assert.notInclude(getWarning(element), NO_NEWLINE_REVISION); - }); - - test('hide warning if undefined', () => { - element.showNewlineWarningRight = undefined; - assert.notInclude(getWarning(element), NO_NEWLINE_REVISION); - }); - }); - - test('_computeNewlineWarningClass', () => { - const hidden = 'newlineWarning hidden'; - const shown = 'newlineWarning'; - assert.equal(element._computeNewlineWarningClass(null, true), hidden); - assert.equal(element._computeNewlineWarningClass('foo', true), hidden); - assert.equal(element._computeNewlineWarningClass(null, false), hidden); - assert.equal(element._computeNewlineWarningClass('foo', false), shown); - }); - }); - - suite('key locations', () => { - let renderStub; - - setup(() => { - element = fixture('basic'); - element.prefs = {}; - renderStub = sandbox.stub(element.$.diffBuilder, 'render') - .returns(new Promise(() => {})); - }); - - test('lineOfInterest is a key location', () => { - element.lineOfInterest = {number: 789, leftSide: true}; - element._renderDiffTable(); - assert.isTrue(renderStub.called); - assert.deepEqual(renderStub.lastCall.args[0], { - left: {789: true}, - right: {}, - }); - }); - - test('line comments are key locations', () => { - const threadEl = document.createElement('div'); - threadEl.className = 'comment-thread'; - threadEl.setAttribute('comment-side', 'right'); - threadEl.setAttribute('line-num', 3); - Polymer.dom(element).appendChild(threadEl); - Polymer.dom.flush(); - - element._renderDiffTable(); - assert.isTrue(renderStub.called); - assert.deepEqual(renderStub.lastCall.args[0], { - left: {}, - right: {3: true}, - }); - }); - - test('file comments are key locations', () => { - const threadEl = document.createElement('div'); - threadEl.className = 'comment-thread'; - threadEl.setAttribute('comment-side', 'left'); - Polymer.dom(element).appendChild(threadEl); - Polymer.dom.flush(); - - element._renderDiffTable(); - assert.isTrue(renderStub.called); - assert.deepEqual(renderStub.lastCall.args[0], { - left: {FILE: true}, - right: {}, - }); - }); - }); - - suite('whitespace changes only message', () => { - const setupDiff = function(ignore_whitespace, diffContent) { - element = fixture('basic'); + element.patchRange = {basePatchNum: 'PARENT', patchNum: 1}; + element.isImageDiff = true; element.prefs = { - ignore_whitespace, auto_hide_diff_table_header: true, context: 10, cursor_blink_rate: 0, font_size: 12, + ignore_whitespace: 'IGNORE_NONE', intraline_difference: true, line_length: 100, line_wrapping: false, @@ -976,98 +300,788 @@ tab_size: 8, theme: 'DEFAULT', }; + }); + test('renders image diffs with same file name', done => { + const rendered = () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + // Left image rendered with the parent commit's version of the file. + const leftImage = element.$.diffTable.querySelector('td.left img'); + const leftLabel = + element.$.diffTable.querySelector('td.left label'); + const leftLabelContent = leftLabel.querySelector('.label'); + const leftLabelName = leftLabel.querySelector('.name'); + + const rightImage = + element.$.diffTable.querySelector('td.right img'); + const rightLabel = element.$.diffTable.querySelector( + 'td.right label'); + const rightLabelContent = rightLabel.querySelector('.label'); + const rightLabelName = rightLabel.querySelector('.name'); + + assert.isNotOk(rightLabelName); + assert.isNotOk(leftLabelName); + + let leftLoaded = false; + let rightLoaded = false; + + leftImage.addEventListener('load', () => { + assert.isOk(leftImage); + assert.equal(leftImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile1.body); + assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); + leftLoaded = true; + if (rightLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + + rightImage.addEventListener('load', () => { + assert.isOk(rightImage); + assert.equal(rightImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile2.body); + assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); + + rightLoaded = true; + if (leftLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + }; + + element.addEventListener('render', rendered); + + element.baseImage = mockFile1; + element.revisionImage = mockFile2; element.diff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, intraline_status: 'OK', change_type: 'MODIFIED', diff_header: [ - 'diff --git a/carrot.js b/carrot.js', + 'diff --git a/carrot.jpg b/carrot.jpg', 'index 2adc47d..f9c2f2c 100644', - '--- a/carrot.js', - '+++ b/carrot.jjs', - 'file differ', + '--- a/carrot.jpg', + '+++ b/carrot.jpg', + 'Binary files differ', ], - content: diffContent, + content: [{skip: 66}], binary: true, }; + }); + + test('renders image diffs with a different file name', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot2.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot2.jpg', + 'index 2adc47d..f9c2f2c 100644', + '--- a/carrot.jpg', + '+++ b/carrot2.jpg', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + + const rendered = () => { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + // Left image rendered with the parent commit's version of the file. + const leftImage = element.$.diffTable.querySelector('td.left img'); + const leftLabel = + element.$.diffTable.querySelector('td.left label'); + const leftLabelContent = leftLabel.querySelector('.label'); + const leftLabelName = leftLabel.querySelector('.name'); + + const rightImage = + element.$.diffTable.querySelector('td.right img'); + const rightLabel = element.$.diffTable.querySelector( + 'td.right label'); + const rightLabelContent = rightLabel.querySelector('.label'); + const rightLabelName = rightLabel.querySelector('.name'); + + assert.isOk(rightLabelName); + assert.isOk(leftLabelName); + assert.equal(leftLabelName.textContent, mockDiff.meta_a.name); + assert.equal(rightLabelName.textContent, mockDiff.meta_b.name); + + let leftLoaded = false; + let rightLoaded = false; + + leftImage.addEventListener('load', () => { + assert.isOk(leftImage); + assert.equal(leftImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile1.body); + assert.equal(leftLabelContent.textContent, '1×1 image/bmp'); + leftLoaded = true; + if (rightLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + + rightImage.addEventListener('load', () => { + assert.isOk(rightImage); + assert.equal(rightImage.getAttribute('src'), + 'data:image/bmp;base64, ' + mockFile2.body); + assert.equal(rightLabelContent.textContent, '1×1 image/bmp'); + + rightLoaded = true; + if (leftLoaded) { + element.removeEventListener('render', rendered); + done(); + } + }); + }; + + element.addEventListener('render', rendered); + + element.baseImage = mockFile1; + element.baseImage._name = mockDiff.meta_a.name; + element.revisionImage = mockFile2; + element.revisionImage._name = mockDiff.meta_b.name; + element.diff = mockDiff; + }); + + test('renders added image', done => { + const mockDiff = { + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'ADDED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index 0000000..f9c2f2c 100644', + '--- /dev/null', + '+++ b/carrot.jpg', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + + function rendered() { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + const leftImage = element.$.diffTable.querySelector('td.left img'); + const rightImage = element.$.diffTable.querySelector('td.right img'); + + assert.isNotOk(leftImage); + assert.isOk(rightImage); + done(); + element.removeEventListener('render', rendered); + } + element.addEventListener('render', rendered); + + element.revisionImage = mockFile2; + element.diff = mockDiff; + }); + + test('renders removed image', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + intraline_status: 'OK', + change_type: 'DELETED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index f9c2f2c..0000000 100644', + '--- a/carrot.jpg', + '+++ /dev/null', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + + function rendered() { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + + const leftImage = element.$.diffTable.querySelector('td.left img'); + const rightImage = element.$.diffTable.querySelector('td.right img'); + + assert.isOk(leftImage); + assert.isNotOk(rightImage); + done(); + element.removeEventListener('render', rendered); + } + element.addEventListener('render', rendered); + + element.baseImage = mockFile1; + element.diff = mockDiff; + }); + + test('does not render disallowed image type', done => { + const mockDiff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg-evil', + lines: 560}, + intraline_status: 'OK', + change_type: 'DELETED', + diff_header: [ + 'diff --git a/carrot.jpg b/carrot.jpg', + 'index f9c2f2c..0000000 100644', + '--- a/carrot.jpg', + '+++ /dev/null', + 'Binary files differ', + ], + content: [{skip: 66}], + binary: true, + }; + mockFile1.type = 'image/jpeg-evil'; + + function rendered() { + // Recognizes that it should be an image diff. + assert.isTrue(element.isImageDiff); + assert.instanceOf( + element.$.diffBuilder._builder, GrDiffBuilderImage); + const leftImage = element.$.diffTable.querySelector('td.left img'); + assert.isNotOk(leftImage); + done(); + element.removeEventListener('render', rendered); + } + element.addEventListener('render', rendered); + + element.baseImage = mockFile1; + element.diff = mockDiff; + }); + }); + + test('_handleTap lineNum', done => { + const addDraftStub = sandbox.stub(element, 'addDraftAtLine'); + const el = document.createElement('div'); + el.className = 'lineNum'; + el.addEventListener('click', e => { + element._handleTap(e); + assert.isTrue(addDraftStub.called); + assert.equal(addDraftStub.lastCall.args[0], el); + done(); + }); + el.click(); + }); + + test('_handleTap context', done => { + const showContextStub = + sandbox.stub(element.$.diffBuilder, 'showContext'); + const el = document.createElement('div'); + el.className = 'showContext'; + el.addEventListener('click', e => { + element._handleTap(e); + assert.isTrue(showContextStub.called); + done(); + }); + el.click(); + }); + + test('_handleTap content', done => { + const content = document.createElement('div'); + const lineEl = document.createElement('div'); + + const selectStub = sandbox.stub(element, '_selectLine'); + sandbox.stub(element.$.diffBuilder, 'getLineElByChild', () => lineEl); + + content.className = 'content'; + content.addEventListener('click', e => { + element._handleTap(e); + assert.isTrue(selectStub.called); + assert.equal(selectStub.lastCall.args[0], lineEl); + done(); + }); + content.click(); + }); + + suite('getCursorStops', () => { + const setupDiff = function() { + const mock = document.createElement('mock-diff-response'); + element.diff = mock.diffResponse; + element.prefs = { + context: 10, + tab_size: 8, + font_size: 12, + line_length: 100, + cursor_blink_rate: 0, + line_wrapping: false, + intraline_difference: true, + show_line_endings: true, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + auto_hide_diff_table_header: true, + theme: 'DEFAULT', + ignore_whitespace: 'IGNORE_NONE', + }; element._renderDiffTable(); flushAsynchronousOperations(); }; - test('show the message if ignore_whitespace is criteria matches', () => { - setupDiff('IGNORE_ALL', [{skip: 100}]); - assert.isTrue(element.showNoChangeMessage( - /* loading= */ false, - element.prefs, - element._diffLength - )); + test('getCursorStops returns [] when hidden and noAutoRender', () => { + element.noAutoRender = true; + setupDiff(); + element.hidden = true; + assert.equal(element.getCursorStops().length, 0); }); - test('do not show the message if still loading', () => { - setupDiff('IGNORE_ALL', [{skip: 100}]); - assert.isFalse(element.showNoChangeMessage( - /* loading= */ true, - element.prefs, - element._diffLength - )); - }); - - test('do not show the message if contains valid changes', () => { - const content = [{ - a: ['all work and no play make andybons a dull boy'], - b: ['elgoog elgoog elgoog'], - }, { - ab: [ - 'Non eram nescius, Brute, cum, quae summis ingeniis ', - 'exquisitaque doctrina philosophi Graeco sermone tractavissent', - ], - }]; - setupDiff('IGNORE_ALL', content); - assert.equal(element._diffLength, 3); - assert.isFalse(element.showNoChangeMessage( - /* loading= */ false, - element.prefs, - element._diffLength - )); - }); - - test('do not show message if ignore whitespace is disabled', () => { - const content = [{ - a: ['all work and no play make andybons a dull boy'], - b: ['elgoog elgoog elgoog'], - }, { - ab: [ - 'Non eram nescius, Brute, cum, quae summis ingeniis ', - 'exquisitaque doctrina philosophi Graeco sermone tractavissent', - ], - }]; - setupDiff('IGNORE_NONE', content); - assert.isFalse(element.showNoChangeMessage( - /* loading= */ false, - element.prefs, - element._diffLength - )); + test('getCursorStops', () => { + setupDiff(); + assert.equal(element.getCursorStops().length, 50); }); }); - test('getDiffLength', () => { - const diff = document.createElement('mock-diff-response').diffResponse; - assert.equal(element.getDiffLength(diff), 52); + test('adds .hiddenscroll', () => { + Gerrit.hiddenscroll = true; + element.displayLine = true; + assert.include(element.shadowRoot + .querySelector('.diffContainer').className, 'hiddenscroll'); }); + }); - test('`render` event has contentRendered field in detail', done => { + suite('logged in', () => { + let fakeLineEl; + setup(() => { element = fixture('basic'); - element.prefs = {}; - sandbox.stub(element.$.diffBuilder, 'render') - .returns(Promise.resolve()); - element.addEventListener('render', event => { - assert.isTrue(event.detail.contentRendered); - done(); + element.loggedIn = true; + element.patchRange = {}; + + fakeLineEl = { + getAttribute: sandbox.stub().returns(42), + classList: { + contains: sandbox.stub().returns(true), + }, + }; + }); + + test('addDraftAtLine', () => { + sandbox.stub(element, '_selectLine'); + sandbox.stub(element, '_createComment'); + element.addDraftAtLine(fakeLineEl); + assert.isTrue(element._createComment + .calledWithExactly(fakeLineEl, 42)); + }); + + test('addDraftAtLine on an edit', () => { + element.patchRange.basePatchNum = element.EDIT_NAME; + sandbox.stub(element, '_selectLine'); + sandbox.stub(element, '_createComment'); + const alertSpy = sandbox.spy(); + element.addEventListener('show-alert', alertSpy); + element.addDraftAtLine(fakeLineEl); + assert.isTrue(alertSpy.called); + assert.isFalse(element._createComment.called); + }); + + test('addDraftAtLine on an edit base', () => { + element.patchRange.patchNum = element.EDIT_NAME; + element.patchRange.basePatchNum = element.PARENT_NAME; + sandbox.stub(element, '_selectLine'); + sandbox.stub(element, '_createComment'); + const alertSpy = sandbox.spy(); + element.addEventListener('show-alert', alertSpy); + element.addDraftAtLine(fakeLineEl); + assert.isTrue(alertSpy.called); + assert.isFalse(element._createComment.called); + }); + + suite('change in preferences', () => { + setup(() => { + element.diff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + diff_header: [], + intraline_status: 'OK', + change_type: 'MODIFIED', + content: [{skip: 66}], + }; + element.flushDebouncer('renderDiffTable'); }); + + test('change in preferences re-renders diff', () => { + sandbox.stub(element, '_renderDiffTable'); + element.prefs = Object.assign( + {}, MINIMAL_PREFS, {time_format: 'HHMM_12'}); + element.flushDebouncer('renderDiffTable'); + assert.isTrue(element._renderDiffTable.called); + }); + + test('change in preferences does not re-renders diff with ' + + 'noRenderOnPrefsChange', () => { + sandbox.stub(element, '_renderDiffTable'); + element.noRenderOnPrefsChange = true; + element.prefs = Object.assign( + {}, MINIMAL_PREFS, {time_format: 'HHMM_12'}); + element.flushDebouncer('renderDiffTable'); + assert.isFalse(element._renderDiffTable.called); + }); + }); + }); + + suite('diff header', () => { + setup(() => { + element = fixture('basic'); + element.diff = { + meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66}, + meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg', + lines: 560}, + diff_header: [], + intraline_status: 'OK', + change_type: 'MODIFIED', + content: [{skip: 66}], + }; + }); + + test('hidden', () => { + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', 'index 2adc47d..f9c2f2c 100644'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', '--- a/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', '+++ b/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', 'test'); + assert.equal(element._diffHeaderItems.length, 1); + flushAsynchronousOperations(); + + assert.equal(element.$.diffHeader.textContent.trim(), 'test'); + }); + + test('binary files', () => { + element.diff.binary = true; + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', 'diff --git a/test.jpg b/test.jpg'); + assert.equal(element._diffHeaderItems.length, 0); + element.push('diff.diff_header', 'test'); + assert.equal(element._diffHeaderItems.length, 1); + element.push('diff.diff_header', 'Binary files differ'); + assert.equal(element._diffHeaderItems.length, 1); + }); + }); + + suite('safety and bypass', () => { + let renderStub; + + setup(() => { + element = fixture('basic'); + renderStub = sandbox.stub(element.$.diffBuilder, 'render', + () => { + element.$.diffBuilder.dispatchEvent( + new CustomEvent('render', {bubbles: true, composed: true})); + return Promise.resolve({}); + }); + const mock = document.createElement('mock-diff-response'); + sandbox.stub(element, 'getDiffLength').returns(10000); + element.diff = mock.diffResponse; + element.noRenderOnPrefsChange = true; + }); + + test('large render w/ context = 10', done => { + element.prefs = Object.assign({}, MINIMAL_PREFS, {context: 10}); + function rendered() { + assert.isTrue(renderStub.called); + assert.isFalse(element._showWarning); + done(); + element.removeEventListener('render', rendered); + } + element.addEventListener('render', rendered); + element._renderDiffTable(); + }); + + test('large render w/ whole file and bypass', done => { + element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1}); + element._safetyBypass = 10; + function rendered() { + assert.isTrue(renderStub.called); + assert.isFalse(element._showWarning); + done(); + element.removeEventListener('render', rendered); + } + element.addEventListener('render', rendered); + element._renderDiffTable(); + }); + + test('large render w/ whole file and no bypass', done => { + element.prefs = Object.assign({}, MINIMAL_PREFS, {context: -1}); + function rendered() { + assert.isFalse(renderStub.called); + assert.isTrue(element._showWarning); + done(); + element.removeEventListener('render', rendered); + } + element.addEventListener('render', rendered); element._renderDiffTable(); }); }); - a11ySuite('basic'); + suite('blame', () => { + setup(() => { + element = fixture('basic'); + }); + + test('unsetting', () => { + element.blame = []; + const setBlameSpy = sandbox.spy(element.$.diffBuilder, 'setBlame'); + element.classList.add('showBlame'); + element.blame = null; + assert.isTrue(setBlameSpy.calledWithExactly(null)); + assert.isFalse(element.classList.contains('showBlame')); + }); + + test('setting', () => { + const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}]; + element.blame = mockBlame; + assert.isTrue(element.classList.contains('showBlame')); + }); + }); + + suite('trailing newline warnings', () => { + const NO_NEWLINE_BASE = 'No newline at end of base file.'; + const NO_NEWLINE_REVISION = 'No newline at end of revision file.'; + + const getWarning = element => + element.shadowRoot.querySelector('.newlineWarning').textContent; + + setup(() => { + element = fixture('basic'); + element.showNewlineWarningLeft = false; + element.showNewlineWarningRight = false; + }); + + test('shows combined warning if both sides set to warn', () => { + element.showNewlineWarningLeft = true; + element.showNewlineWarningRight = true; + assert.include(getWarning(element), + NO_NEWLINE_BASE + ' — ' + NO_NEWLINE_REVISION); + }); + + suite('showNewlineWarningLeft', () => { + test('show warning if true', () => { + element.showNewlineWarningLeft = true; + assert.include(getWarning(element), NO_NEWLINE_BASE); + }); + + test('hide warning if false', () => { + element.showNewlineWarningLeft = false; + assert.notInclude(getWarning(element), NO_NEWLINE_BASE); + }); + + test('hide warning if undefined', () => { + element.showNewlineWarningLeft = undefined; + assert.notInclude(getWarning(element), NO_NEWLINE_BASE); + }); + }); + + suite('showNewlineWarningRight', () => { + test('show warning if true', () => { + element.showNewlineWarningRight = true; + assert.include(getWarning(element), NO_NEWLINE_REVISION); + }); + + test('hide warning if false', () => { + element.showNewlineWarningRight = false; + assert.notInclude(getWarning(element), NO_NEWLINE_REVISION); + }); + + test('hide warning if undefined', () => { + element.showNewlineWarningRight = undefined; + assert.notInclude(getWarning(element), NO_NEWLINE_REVISION); + }); + }); + + test('_computeNewlineWarningClass', () => { + const hidden = 'newlineWarning hidden'; + const shown = 'newlineWarning'; + assert.equal(element._computeNewlineWarningClass(null, true), hidden); + assert.equal(element._computeNewlineWarningClass('foo', true), hidden); + assert.equal(element._computeNewlineWarningClass(null, false), hidden); + assert.equal(element._computeNewlineWarningClass('foo', false), shown); + }); + }); + + suite('key locations', () => { + let renderStub; + + setup(() => { + element = fixture('basic'); + element.prefs = {}; + renderStub = sandbox.stub(element.$.diffBuilder, 'render') + .returns(new Promise(() => {})); + }); + + test('lineOfInterest is a key location', () => { + element.lineOfInterest = {number: 789, leftSide: true}; + element._renderDiffTable(); + assert.isTrue(renderStub.called); + assert.deepEqual(renderStub.lastCall.args[0], { + left: {789: true}, + right: {}, + }); + }); + + test('line comments are key locations', () => { + const threadEl = document.createElement('div'); + threadEl.className = 'comment-thread'; + threadEl.setAttribute('comment-side', 'right'); + threadEl.setAttribute('line-num', 3); + dom(element).appendChild(threadEl); + flush(); + + element._renderDiffTable(); + assert.isTrue(renderStub.called); + assert.deepEqual(renderStub.lastCall.args[0], { + left: {}, + right: {3: true}, + }); + }); + + test('file comments are key locations', () => { + const threadEl = document.createElement('div'); + threadEl.className = 'comment-thread'; + threadEl.setAttribute('comment-side', 'left'); + dom(element).appendChild(threadEl); + flush(); + + element._renderDiffTable(); + assert.isTrue(renderStub.called); + assert.deepEqual(renderStub.lastCall.args[0], { + left: {FILE: true}, + right: {}, + }); + }); + }); + + suite('whitespace changes only message', () => { + const setupDiff = function(ignore_whitespace, diffContent) { + element = fixture('basic'); + element.prefs = { + ignore_whitespace, + auto_hide_diff_table_header: true, + context: 10, + cursor_blink_rate: 0, + font_size: 12, + intraline_difference: true, + line_length: 100, + line_wrapping: false, + show_line_endings: true, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + tab_size: 8, + theme: 'DEFAULT', + }; + + element.diff = { + intraline_status: 'OK', + change_type: 'MODIFIED', + diff_header: [ + 'diff --git a/carrot.js b/carrot.js', + 'index 2adc47d..f9c2f2c 100644', + '--- a/carrot.js', + '+++ b/carrot.jjs', + 'file differ', + ], + content: diffContent, + binary: true, + }; + + element._renderDiffTable(); + flushAsynchronousOperations(); + }; + + test('show the message if ignore_whitespace is criteria matches', () => { + setupDiff('IGNORE_ALL', [{skip: 100}]); + assert.isTrue(element.showNoChangeMessage( + /* loading= */ false, + element.prefs, + element._diffLength + )); + }); + + test('do not show the message if still loading', () => { + setupDiff('IGNORE_ALL', [{skip: 100}]); + assert.isFalse(element.showNoChangeMessage( + /* loading= */ true, + element.prefs, + element._diffLength + )); + }); + + test('do not show the message if contains valid changes', () => { + const content = [{ + a: ['all work and no play make andybons a dull boy'], + b: ['elgoog elgoog elgoog'], + }, { + ab: [ + 'Non eram nescius, Brute, cum, quae summis ingeniis ', + 'exquisitaque doctrina philosophi Graeco sermone tractavissent', + ], + }]; + setupDiff('IGNORE_ALL', content); + assert.equal(element._diffLength, 3); + assert.isFalse(element.showNoChangeMessage( + /* loading= */ false, + element.prefs, + element._diffLength + )); + }); + + test('do not show message if ignore whitespace is disabled', () => { + const content = [{ + a: ['all work and no play make andybons a dull boy'], + b: ['elgoog elgoog elgoog'], + }, { + ab: [ + 'Non eram nescius, Brute, cum, quae summis ingeniis ', + 'exquisitaque doctrina philosophi Graeco sermone tractavissent', + ], + }]; + setupDiff('IGNORE_NONE', content); + assert.isFalse(element.showNoChangeMessage( + /* loading= */ false, + element.prefs, + element._diffLength + )); + }); + }); + + test('getDiffLength', () => { + const diff = document.createElement('mock-diff-response').diffResponse; + assert.equal(element.getDiffLength(diff), 52); + }); + + test('`render` event has contentRendered field in detail', done => { + element = fixture('basic'); + element.prefs = {}; + sandbox.stub(element.$.diffBuilder, 'render') + .returns(Promise.resolve()); + element.addEventListener('render', event => { + assert.isTrue(event.detail.contentRendered); + done(); + }); + element._renderDiffTable(); + }); +}); + +a11ySuite('basic'); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js index d24e0bc..f2b0599 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,278 +14,290 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Maximum length for patch set descriptions. - const PATCH_DESC_MAX_LENGTH = 500; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-dropdown-list/gr-dropdown-list.js'; +import '../../shared/gr-count-string-formatter/gr-count-string-formatter.js'; +import '../../shared/gr-select/gr-select.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-patch-range-select_html.js'; - /** - * @appliesMixin Gerrit.PatchSetMixin - */ - /** - * Fired when the patch range changes - * - * @event patch-range-change - * - * @property {string} patchNum - * @property {string} basePatchNum - * @extends Polymer.Element - */ - class GrPatchRangeSelect extends Polymer.mixinBehaviors( [ - Gerrit.PatchSetBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-patch-range-select'; } +// Maximum length for patch set descriptions. +const PATCH_DESC_MAX_LENGTH = 500; - static get properties() { - return { - availablePatches: Array, - _baseDropdownContent: { - type: Object, - computed: '_computeBaseDropdownContent(availablePatches, patchNum,' + - '_sortedRevisions, changeComments, revisionInfo)', - }, - _patchDropdownContent: { - type: Object, - computed: '_computePatchDropdownContent(availablePatches,' + - 'basePatchNum, _sortedRevisions, changeComments)', - }, - changeNum: String, - changeComments: Object, - /** @type {{ meta_a: !Array, meta_b: !Array}} */ - filesWeblinks: Object, - patchNum: String, - basePatchNum: String, - revisions: Object, - revisionInfo: Object, - _sortedRevisions: Array, - }; - } +/** + * @appliesMixin Gerrit.PatchSetMixin + */ +/** + * Fired when the patch range changes + * + * @event patch-range-change + * + * @property {string} patchNum + * @property {string} basePatchNum + * @extends Polymer.Element + */ +class GrPatchRangeSelect extends mixinBehaviors( [ + Gerrit.PatchSetBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - static get observers() { - return [ - '_updateSortedRevisions(revisions.*)', - ]; - } + static get is() { return 'gr-patch-range-select'; } - _getShaForPatch(patch) { - return patch.sha.substring(0, 10); - } - - _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions, - changeComments, revisionInfo) { - // Polymer 2: check for undefined - if ([ - availablePatches, - patchNum, - _sortedRevisions, - changeComments, - revisionInfo, - ].some(arg => arg === undefined)) { - return undefined; - } - - const parentCounts = revisionInfo.getParentCountMap(); - const currentParentCount = parentCounts.hasOwnProperty(patchNum) ? - parentCounts[patchNum] : 1; - const maxParents = revisionInfo.getMaxParents(); - const isMerge = currentParentCount > 1; - - const dropdownContent = []; - for (const basePatch of availablePatches) { - const basePatchNum = basePatch.num; - const entry = this._createDropdownEntry(basePatchNum, 'Patchset ', - _sortedRevisions, changeComments, this._getShaForPatch(basePatch)); - dropdownContent.push(Object.assign({}, entry, { - disabled: this._computeLeftDisabled( - basePatch.num, patchNum, _sortedRevisions), - })); - } - - dropdownContent.push({ - text: isMerge ? 'Auto Merge' : 'Base', - value: 'PARENT', - }); - - for (let idx = 0; isMerge && idx < maxParents; idx++) { - dropdownContent.push({ - disabled: idx >= currentParentCount, - triggerText: `Parent ${idx + 1}`, - text: `Parent ${idx + 1}`, - mobileText: `Parent ${idx + 1}`, - value: -(idx + 1), - }); - } - - return dropdownContent; - } - - _computeMobileText(patchNum, changeComments, revisions) { - return `${patchNum}` + - `${this._computePatchSetCommentsString(changeComments, patchNum)}` + - `${this._computePatchSetDescription(revisions, patchNum, true)}`; - } - - _computePatchDropdownContent(availablePatches, basePatchNum, - _sortedRevisions, changeComments) { - // Polymer 2: check for undefined - if ([ - availablePatches, - basePatchNum, - _sortedRevisions, - changeComments, - ].some(arg => arg === undefined)) { - return undefined; - } - - const dropdownContent = []; - for (const patch of availablePatches) { - const patchNum = patch.num; - const entry = this._createDropdownEntry( - patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions, - changeComments, this._getShaForPatch(patch)); - dropdownContent.push(Object.assign({}, entry, { - disabled: this._computeRightDisabled(basePatchNum, patchNum, - _sortedRevisions), - })); - } - return dropdownContent; - } - - _computeText(patchNum, prefix, changeComments, sha) { - return `${prefix}${patchNum}` + - `${this._computePatchSetCommentsString(changeComments, patchNum)}` + - (` | ${sha}`); - } - - _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments, - sha) { - const entry = { - triggerText: `${prefix}${patchNum}`, - text: this._computeText(patchNum, prefix, changeComments, sha), - mobileText: this._computeMobileText(patchNum, changeComments, - sortedRevisions), - bottomText: `${this._computePatchSetDescription( - sortedRevisions, patchNum)}`, - value: patchNum, - }; - const date = this._computePatchSetDate(sortedRevisions, patchNum); - if (date) { - entry['date'] = date; - } - return entry; - } - - _updateSortedRevisions(revisionsRecord) { - const revisions = revisionsRecord.base; - this._sortedRevisions = this.sortRevisions(Object.values(revisions)); - } - - /** - * The basePatchNum should always be <= patchNum -- because sortedRevisions - * is sorted in reverse order (higher patchset nums first), invalid base - * patch nums have an index greater than the index of patchNum. - * - * @param {number|string} basePatchNum The possible base patch num. - * @param {number|string} patchNum The current selected patch num. - * @param {!Array} sortedRevisions - */ - _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) { - return this.findSortedIndex(basePatchNum, sortedRevisions) <= - this.findSortedIndex(patchNum, sortedRevisions); - } - - /** - * The basePatchNum should always be <= patchNum -- because sortedRevisions - * is sorted in reverse order (higher patchset nums first), invalid patch - * nums have an index greater than the index of basePatchNum. - * - * In addition, if the current basePatchNum is 'PARENT', all patchNums are - * valid. - * - * If the curent basePatchNum is a parent index, then only patches that have - * at least that many parents are valid. - * - * @param {number|string} basePatchNum The current selected base patch num. - * @param {number|string} patchNum The possible patch num. - * @param {!Array} sortedRevisions - * @return {boolean} - */ - _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) { - if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; } - - if (this.isMergeParent(basePatchNum)) { - // Note: parent indices use 1-offset. - return this.revisionInfo.getParentCount(patchNum) < - this.getParentIndex(basePatchNum); - } - - return this.findSortedIndex(basePatchNum, sortedRevisions) <= - this.findSortedIndex(patchNum, sortedRevisions); - } - - _computePatchSetCommentsString(changeComments, patchNum) { - if (!changeComments) { return; } - - const commentCount = changeComments.computeCommentCount(patchNum); - const commentString = GrCountStringFormatter.computePluralString( - commentCount, 'comment'); - - const unresolvedCount = changeComments.computeUnresolvedNum(patchNum); - const unresolvedString = GrCountStringFormatter.computeString( - unresolvedCount, 'unresolved'); - - if (!commentString.length && !unresolvedString.length) { - return ''; - } - - return ` (${commentString}` + - // Add a comma + space if both comments and unresolved - (commentString && unresolvedString ? ', ' : '') + - `${unresolvedString})`; - } - - /** - * @param {!Array} revisions - * @param {number|string} patchNum - * @param {boolean=} opt_addFrontSpace - */ - _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) { - const rev = this.getRevisionByPatchNum(revisions, patchNum); - return (rev && rev.description) ? - (opt_addFrontSpace ? ' ' : '') + - rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; - } - - /** - * @param {!Array} revisions - * @param {number|string} patchNum - */ - _computePatchSetDate(revisions, patchNum) { - const rev = this.getRevisionByPatchNum(revisions, patchNum); - return rev ? rev.created : undefined; - } - - /** - * Catches value-change events from the patchset dropdowns and determines - * whether or not a patch change event should be fired. - */ - _handlePatchChange(e) { - const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum}; - const target = Polymer.dom(e).localTarget; - - if (target === this.$.patchNumDropdown) { - detail.patchNum = e.detail.value; - } else { - detail.basePatchNum = e.detail.value; - } - - this.dispatchEvent( - new CustomEvent('patch-range-change', {detail, bubbles: false})); - } + static get properties() { + return { + availablePatches: Array, + _baseDropdownContent: { + type: Object, + computed: '_computeBaseDropdownContent(availablePatches, patchNum,' + + '_sortedRevisions, changeComments, revisionInfo)', + }, + _patchDropdownContent: { + type: Object, + computed: '_computePatchDropdownContent(availablePatches,' + + 'basePatchNum, _sortedRevisions, changeComments)', + }, + changeNum: String, + changeComments: Object, + /** @type {{ meta_a: !Array, meta_b: !Array}} */ + filesWeblinks: Object, + patchNum: String, + basePatchNum: String, + revisions: Object, + revisionInfo: Object, + _sortedRevisions: Array, + }; } - customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect); -})(); + static get observers() { + return [ + '_updateSortedRevisions(revisions.*)', + ]; + } + + _getShaForPatch(patch) { + return patch.sha.substring(0, 10); + } + + _computeBaseDropdownContent(availablePatches, patchNum, _sortedRevisions, + changeComments, revisionInfo) { + // Polymer 2: check for undefined + if ([ + availablePatches, + patchNum, + _sortedRevisions, + changeComments, + revisionInfo, + ].some(arg => arg === undefined)) { + return undefined; + } + + const parentCounts = revisionInfo.getParentCountMap(); + const currentParentCount = parentCounts.hasOwnProperty(patchNum) ? + parentCounts[patchNum] : 1; + const maxParents = revisionInfo.getMaxParents(); + const isMerge = currentParentCount > 1; + + const dropdownContent = []; + for (const basePatch of availablePatches) { + const basePatchNum = basePatch.num; + const entry = this._createDropdownEntry(basePatchNum, 'Patchset ', + _sortedRevisions, changeComments, this._getShaForPatch(basePatch)); + dropdownContent.push(Object.assign({}, entry, { + disabled: this._computeLeftDisabled( + basePatch.num, patchNum, _sortedRevisions), + })); + } + + dropdownContent.push({ + text: isMerge ? 'Auto Merge' : 'Base', + value: 'PARENT', + }); + + for (let idx = 0; isMerge && idx < maxParents; idx++) { + dropdownContent.push({ + disabled: idx >= currentParentCount, + triggerText: `Parent ${idx + 1}`, + text: `Parent ${idx + 1}`, + mobileText: `Parent ${idx + 1}`, + value: -(idx + 1), + }); + } + + return dropdownContent; + } + + _computeMobileText(patchNum, changeComments, revisions) { + return `${patchNum}` + + `${this._computePatchSetCommentsString(changeComments, patchNum)}` + + `${this._computePatchSetDescription(revisions, patchNum, true)}`; + } + + _computePatchDropdownContent(availablePatches, basePatchNum, + _sortedRevisions, changeComments) { + // Polymer 2: check for undefined + if ([ + availablePatches, + basePatchNum, + _sortedRevisions, + changeComments, + ].some(arg => arg === undefined)) { + return undefined; + } + + const dropdownContent = []; + for (const patch of availablePatches) { + const patchNum = patch.num; + const entry = this._createDropdownEntry( + patchNum, patchNum === 'edit' ? '' : 'Patchset ', _sortedRevisions, + changeComments, this._getShaForPatch(patch)); + dropdownContent.push(Object.assign({}, entry, { + disabled: this._computeRightDisabled(basePatchNum, patchNum, + _sortedRevisions), + })); + } + return dropdownContent; + } + + _computeText(patchNum, prefix, changeComments, sha) { + return `${prefix}${patchNum}` + + `${this._computePatchSetCommentsString(changeComments, patchNum)}` + + (` | ${sha}`); + } + + _createDropdownEntry(patchNum, prefix, sortedRevisions, changeComments, + sha) { + const entry = { + triggerText: `${prefix}${patchNum}`, + text: this._computeText(patchNum, prefix, changeComments, sha), + mobileText: this._computeMobileText(patchNum, changeComments, + sortedRevisions), + bottomText: `${this._computePatchSetDescription( + sortedRevisions, patchNum)}`, + value: patchNum, + }; + const date = this._computePatchSetDate(sortedRevisions, patchNum); + if (date) { + entry['date'] = date; + } + return entry; + } + + _updateSortedRevisions(revisionsRecord) { + const revisions = revisionsRecord.base; + this._sortedRevisions = this.sortRevisions(Object.values(revisions)); + } + + /** + * The basePatchNum should always be <= patchNum -- because sortedRevisions + * is sorted in reverse order (higher patchset nums first), invalid base + * patch nums have an index greater than the index of patchNum. + * + * @param {number|string} basePatchNum The possible base patch num. + * @param {number|string} patchNum The current selected patch num. + * @param {!Array} sortedRevisions + */ + _computeLeftDisabled(basePatchNum, patchNum, sortedRevisions) { + return this.findSortedIndex(basePatchNum, sortedRevisions) <= + this.findSortedIndex(patchNum, sortedRevisions); + } + + /** + * The basePatchNum should always be <= patchNum -- because sortedRevisions + * is sorted in reverse order (higher patchset nums first), invalid patch + * nums have an index greater than the index of basePatchNum. + * + * In addition, if the current basePatchNum is 'PARENT', all patchNums are + * valid. + * + * If the curent basePatchNum is a parent index, then only patches that have + * at least that many parents are valid. + * + * @param {number|string} basePatchNum The current selected base patch num. + * @param {number|string} patchNum The possible patch num. + * @param {!Array} sortedRevisions + * @return {boolean} + */ + _computeRightDisabled(basePatchNum, patchNum, sortedRevisions) { + if (this.patchNumEquals(basePatchNum, 'PARENT')) { return false; } + + if (this.isMergeParent(basePatchNum)) { + // Note: parent indices use 1-offset. + return this.revisionInfo.getParentCount(patchNum) < + this.getParentIndex(basePatchNum); + } + + return this.findSortedIndex(basePatchNum, sortedRevisions) <= + this.findSortedIndex(patchNum, sortedRevisions); + } + + _computePatchSetCommentsString(changeComments, patchNum) { + if (!changeComments) { return; } + + const commentCount = changeComments.computeCommentCount(patchNum); + const commentString = GrCountStringFormatter.computePluralString( + commentCount, 'comment'); + + const unresolvedCount = changeComments.computeUnresolvedNum(patchNum); + const unresolvedString = GrCountStringFormatter.computeString( + unresolvedCount, 'unresolved'); + + if (!commentString.length && !unresolvedString.length) { + return ''; + } + + return ` (${commentString}` + + // Add a comma + space if both comments and unresolved + (commentString && unresolvedString ? ', ' : '') + + `${unresolvedString})`; + } + + /** + * @param {!Array} revisions + * @param {number|string} patchNum + * @param {boolean=} opt_addFrontSpace + */ + _computePatchSetDescription(revisions, patchNum, opt_addFrontSpace) { + const rev = this.getRevisionByPatchNum(revisions, patchNum); + return (rev && rev.description) ? + (opt_addFrontSpace ? ' ' : '') + + rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : ''; + } + + /** + * @param {!Array} revisions + * @param {number|string} patchNum + */ + _computePatchSetDate(revisions, patchNum) { + const rev = this.getRevisionByPatchNum(revisions, patchNum); + return rev ? rev.created : undefined; + } + + /** + * Catches value-change events from the patchset dropdowns and determines + * whether or not a patch change event should be fired. + */ + _handlePatchChange(e) { + const detail = {patchNum: this.patchNum, basePatchNum: this.basePatchNum}; + const target = dom(e).localTarget; + + if (target === this.$.patchNumDropdown) { + detail.patchNum = e.detail.value; + } else { + detail.basePatchNum = e.detail.value; + } + + this.dispatchEvent( + new CustomEvent('patch-range-change', {detail, bubbles: false})); + } +} + +customElements.define(GrPatchRangeSelect.is, GrPatchRangeSelect);
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js index ee1f536..5779a903 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-dropdown-list/gr-dropdown-list.html"> -<link rel="import" href="../../shared/gr-count-string-formatter/gr-count-string-formatter.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<dom-module id="gr-patch-range-select"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { align-items: center; @@ -59,34 +52,22 @@ } </style> <span class="patchRange"> - <gr-dropdown-list - id="basePatchDropdown" - value="[[basePatchNum]]" - on-value-change="_handlePatchChange" - items="[[_baseDropdownContent]]"> + <gr-dropdown-list id="basePatchDropdown" value="[[basePatchNum]]" on-value-change="_handlePatchChange" items="[[_baseDropdownContent]]"> </gr-dropdown-list> </span> <span is="dom-if" if="[[filesWeblinks.meta_a]]" class="filesWeblinks"> <template is="dom-repeat" items="[[filesWeblinks.meta_a]]" as="weblink"> - <a target="_blank" rel="noopener" - href$="[[weblink.url]]">[[weblink.name]]</a> + <a target="_blank" rel="noopener" href\$="[[weblink.url]]">[[weblink.name]]</a> </template> </span> - <span class="arrow">→</span> + <span class="arrow">→</span> <span class="patchRange"> - <gr-dropdown-list - id="patchNumDropdown" - value="[[patchNum]]" - on-value-change="_handlePatchChange" - items="[[_patchDropdownContent]]"> + <gr-dropdown-list id="patchNumDropdown" value="[[patchNum]]" on-value-change="_handlePatchChange" items="[[_patchDropdownContent]]"> </gr-dropdown-list> <span is="dom-if" if="[[filesWeblinks.meta_b]]" class="filesWeblinks"> <template is="dom-repeat" items="[[filesWeblinks.meta_b]]" as="weblink"> - <a target="_blank" - href$="[[weblink.url]]">[[weblink.name]]</a> + <a target="_blank" href\$="[[weblink.url]]">[[weblink.name]]</a> </template> </span> </span> - </template> - <script src="gr-patch-range-select.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html index 65dedef..3c07750 100644 --- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html +++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.html
@@ -19,21 +19,30 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-patch-range-select</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> -<link rel="import" href="../../diff/gr-comment-api/gr-comment-api.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> -<link rel="import" href="../../shared/revision-info/revision-info.html"> +<script type="module" src="../gr-comment-api/gr-comment-api.js"></script> +<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script> +<script type="module" src="../../shared/revision-info/revision-info.js"></script> -<link rel="import" href="gr-patch-range-select.html"> +<script type="module" src="./gr-patch-range-select.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-comment-api/gr-comment-api.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import '../../shared/revision-info/revision-info.js'; +import './gr-patch-range-select.js'; +import '../gr-comment-api/gr-comment-api-mock_test.js'; +void(0); +</script> <dom-module id="comment-api-mock"> <template> @@ -41,7 +50,7 @@ change-comments="[[_changeComments]]"></gr-patch-range-select> <gr-comment-api id="commentAPI"></gr-comment-api> </template> - <script src="../../diff/gr-comment-api/gr-comment-api-mock_test.js"></script> + <script type="module" src="../gr-comment-api/gr-comment-api-mock_test.js"></script> </dom-module> <test-fixture id="basic"> @@ -50,384 +59,391 @@ </template> </test-fixture> -<script> - suite('gr-patch-range-select tests', async () => { - await readyToTest(); - let element; - let sandbox; - let commentApiWrapper; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-comment-api/gr-comment-api.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import '../../shared/revision-info/revision-info.js'; +import './gr-patch-range-select.js'; +import '../gr-comment-api/gr-comment-api-mock_test.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-patch-range-select tests', () => { + let element; + let sandbox; + let commentApiWrapper; - function getInfo(revisions) { - const revisionObj = {}; - for (let i = 0; i < revisions.length; i++) { - revisionObj[i] = revisions[i]; - } - return new Gerrit.RevisionInfo({revisions: revisionObj}); + function getInfo(revisions) { + const revisionObj = {}; + for (let i = 0; i < revisions.length; i++) { + revisionObj[i] = revisions[i]; } + return new Gerrit.RevisionInfo({revisions: revisionObj}); + } - setup(() => { - sandbox = sinon.sandbox.create(); + setup(() => { + sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getDiffComments() { return Promise.resolve({}); }, - getDiffRobotComments() { return Promise.resolve({}); }, - getDiffDrafts() { return Promise.resolve({}); }, + stub('gr-rest-api-interface', { + getDiffComments() { return Promise.resolve({}); }, + getDiffRobotComments() { return Promise.resolve({}); }, + getDiffDrafts() { return Promise.resolve({}); }, + }); + + // Element must be wrapped in an element with direct access to the + // comment API. + commentApiWrapper = fixture('basic'); + element = commentApiWrapper.$.patchRange; + + // Stub methods on the changeComments object after changeComments has + // been initialized. + return commentApiWrapper.loadComments(); + }); + + teardown(() => sandbox.restore()); + + test('enabled/disabled options', () => { + const patchRange = { + basePatchNum: 'PARENT', + patchNum: '3', + }; + const sortedRevisions = [ + {_number: 3}, + {_number: element.EDIT_NAME, basePatchNum: 2}, + {_number: 2}, + {_number: 1}, + ]; + for (const patchNum of ['1', '2', '3']) { + assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, + patchNum, sortedRevisions)); + } + for (const basePatchNum of ['1', '2']) { + assert.isFalse(element._computeLeftDisabled(basePatchNum, + patchRange.patchNum, sortedRevisions)); + } + assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum)); + + patchRange.basePatchNum = element.EDIT_NAME; + assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum, + sortedRevisions)); + assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1', + sortedRevisions)); + assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2', + sortedRevisions)); + assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3', + sortedRevisions)); + assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, + element.EDIT_NAME, sortedRevisions)); + }); + + test('_computeBaseDropdownContent', () => { + const availablePatches = [ + {num: 'edit', sha: '1'}, + {num: 3, sha: '2'}, + {num: 2, sha: '3'}, + {num: 1, sha: '4'}, + ]; + const revisions = [ + { + commit: {parents: []}, + _number: 2, + description: 'description', + }, + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + ]; + element.revisionInfo = getInfo(revisions); + const patchNum = 1; + const sortedRevisions = [ + {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'}, + {_number: element.EDIT_NAME, basePatchNum: 2}, + {_number: 2, description: 'description'}, + {_number: 1}, + ]; + const expectedResult = [ + { + disabled: true, + triggerText: 'Patchset edit', + text: 'Patchset edit | 1', + mobileText: 'edit', + bottomText: '', + value: 'edit', + }, + { + disabled: true, + triggerText: 'Patchset 3', + text: 'Patchset 3 | 2', + mobileText: '3', + bottomText: '', + value: 3, + date: 'Mon, 01 Jan 2001 00:00:00 GMT', + }, + { + disabled: true, + triggerText: 'Patchset 2', + text: 'Patchset 2 | 3', + mobileText: '2 description', + bottomText: 'description', + value: 2, + }, + { + disabled: true, + triggerText: 'Patchset 1', + text: 'Patchset 1 | 4', + mobileText: '1', + bottomText: '', + value: 1, + }, + { + text: 'Base', + value: 'PARENT', + }, + ]; + assert.deepEqual(element._computeBaseDropdownContent(availablePatches, + patchNum, sortedRevisions, element.changeComments, + element.revisionInfo), + expectedResult); + }); + + test('_computeBaseDropdownContent called when patchNum updates', () => { + element.revisions = [ + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + ]; + element.revisionInfo = getInfo(element.revisions); + element.availablePatches = [ + {num: 1, sha: '1'}, + {num: 2, sha: '2'}, + {num: 3, sha: '3'}, + {num: 'edit', sha: '4'}, + ]; + element.patchNum = 2; + element.basePatchNum = 'PARENT'; + flushAsynchronousOperations(); + + sandbox.stub(element, '_computeBaseDropdownContent'); + + // Should be recomputed for each available patch + element.set('patchNum', 1); + assert.equal(element._computeBaseDropdownContent.callCount, 1); + }); + + test('_computeBaseDropdownContent called when changeComments update', + done => { + element.revisions = [ + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + ]; + element.revisionInfo = getInfo(element.revisions); + element.availablePatches = [ + {num: 'edit', sha: '1'}, + {num: 3, sha: '2'}, + {num: 2, sha: '3'}, + {num: 1, sha: '4'}, + ]; + element.patchNum = 2; + element.basePatchNum = 'PARENT'; + flushAsynchronousOperations(); + + // Should be recomputed for each available patch + sandbox.stub(element, '_computeBaseDropdownContent'); + assert.equal(element._computeBaseDropdownContent.callCount, 0); + commentApiWrapper.loadComments().then() + .then(() => { + assert.equal(element._computeBaseDropdownContent.callCount, 1); + done(); + }); }); - // Element must be wrapped in an element with direct access to the - // comment API. - commentApiWrapper = fixture('basic'); - element = commentApiWrapper.$.patchRange; + test('_computePatchDropdownContent called when basePatchNum updates', () => { + element.revisions = [ + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + ]; + element.revisionInfo = getInfo(element.revisions); + element.availablePatches = [ + {num: 1, sha: '1'}, + {num: 2, sha: '2'}, + {num: 3, sha: '3'}, + {num: 'edit', sha: '4'}, + ]; + element.patchNum = 2; + element.basePatchNum = 'PARENT'; + flushAsynchronousOperations(); - // Stub methods on the changeComments object after changeComments has - // been initialized. - return commentApiWrapper.loadComments(); - }); - - teardown(() => sandbox.restore()); - - test('enabled/disabled options', () => { - const patchRange = { - basePatchNum: 'PARENT', - patchNum: '3', - }; - const sortedRevisions = [ - {_number: 3}, - {_number: element.EDIT_NAME, basePatchNum: 2}, - {_number: 2}, - {_number: 1}, - ]; - for (const patchNum of ['1', '2', '3']) { - assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, - patchNum, sortedRevisions)); - } - for (const basePatchNum of ['1', '2']) { - assert.isFalse(element._computeLeftDisabled(basePatchNum, - patchRange.patchNum, sortedRevisions)); - } - assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum)); - - patchRange.basePatchNum = element.EDIT_NAME; - assert.isTrue(element._computeLeftDisabled('3', patchRange.patchNum, - sortedRevisions)); - assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '1', - sortedRevisions)); - assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, '2', - sortedRevisions)); - assert.isFalse(element._computeRightDisabled(patchRange.basePatchNum, '3', - sortedRevisions)); - assert.isTrue(element._computeRightDisabled(patchRange.basePatchNum, - element.EDIT_NAME, sortedRevisions)); - }); - - test('_computeBaseDropdownContent', () => { - const availablePatches = [ - {num: 'edit', sha: '1'}, - {num: 3, sha: '2'}, - {num: 2, sha: '3'}, - {num: 1, sha: '4'}, - ]; - const revisions = [ - { - commit: {parents: []}, - _number: 2, - description: 'description', - }, - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - ]; - element.revisionInfo = getInfo(revisions); - const patchNum = 1; - const sortedRevisions = [ - {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'}, - {_number: element.EDIT_NAME, basePatchNum: 2}, - {_number: 2, description: 'description'}, - {_number: 1}, - ]; - const expectedResult = [ - { - disabled: true, - triggerText: 'Patchset edit', - text: 'Patchset edit | 1', - mobileText: 'edit', - bottomText: '', - value: 'edit', - }, - { - disabled: true, - triggerText: 'Patchset 3', - text: 'Patchset 3 | 2', - mobileText: '3', - bottomText: '', - value: 3, - date: 'Mon, 01 Jan 2001 00:00:00 GMT', - }, - { - disabled: true, - triggerText: 'Patchset 2', - text: 'Patchset 2 | 3', - mobileText: '2 description', - bottomText: 'description', - value: 2, - }, - { - disabled: true, - triggerText: 'Patchset 1', - text: 'Patchset 1 | 4', - mobileText: '1', - bottomText: '', - value: 1, - }, - { - text: 'Base', - value: 'PARENT', - }, - ]; - assert.deepEqual(element._computeBaseDropdownContent(availablePatches, - patchNum, sortedRevisions, element.changeComments, - element.revisionInfo), - expectedResult); - }); - - test('_computeBaseDropdownContent called when patchNum updates', () => { - element.revisions = [ - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - ]; - element.revisionInfo = getInfo(element.revisions); - element.availablePatches = [ - {num: 1, sha: '1'}, - {num: 2, sha: '2'}, - {num: 3, sha: '3'}, - {num: 'edit', sha: '4'}, - ]; - element.patchNum = 2; - element.basePatchNum = 'PARENT'; - flushAsynchronousOperations(); - - sandbox.stub(element, '_computeBaseDropdownContent'); - - // Should be recomputed for each available patch - element.set('patchNum', 1); - assert.equal(element._computeBaseDropdownContent.callCount, 1); - }); - - test('_computeBaseDropdownContent called when changeComments update', - done => { - element.revisions = [ - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - ]; - element.revisionInfo = getInfo(element.revisions); - element.availablePatches = [ - {num: 'edit', sha: '1'}, - {num: 3, sha: '2'}, - {num: 2, sha: '3'}, - {num: 1, sha: '4'}, - ]; - element.patchNum = 2; - element.basePatchNum = 'PARENT'; - flushAsynchronousOperations(); - - // Should be recomputed for each available patch - sandbox.stub(element, '_computeBaseDropdownContent'); - assert.equal(element._computeBaseDropdownContent.callCount, 0); - commentApiWrapper.loadComments().then() - .then(() => { - assert.equal(element._computeBaseDropdownContent.callCount, 1); - done(); - }); - }); - - test('_computePatchDropdownContent called when basePatchNum updates', () => { - element.revisions = [ - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - ]; - element.revisionInfo = getInfo(element.revisions); - element.availablePatches = [ - {num: 1, sha: '1'}, - {num: 2, sha: '2'}, - {num: 3, sha: '3'}, - {num: 'edit', sha: '4'}, - ]; - element.patchNum = 2; - element.basePatchNum = 'PARENT'; - flushAsynchronousOperations(); - - // Should be recomputed for each available patch - sandbox.stub(element, '_computePatchDropdownContent'); - element.set('basePatchNum', 1); - assert.equal(element._computePatchDropdownContent.callCount, 1); - }); - - test('_computePatchDropdownContent called when comments update', done => { - element.revisions = [ - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - {commit: {parents: []}}, - ]; - element.revisionInfo = getInfo(element.revisions); - element.availablePatches = [ - {num: 1, sha: '1'}, - {num: 2, sha: '2'}, - {num: 3, sha: '3'}, - {num: 'edit', sha: '4'}, - ]; - element.patchNum = 2; - element.basePatchNum = 'PARENT'; - flushAsynchronousOperations(); - - // Should be recomputed for each available patch - sandbox.stub(element, '_computePatchDropdownContent'); - assert.equal(element._computePatchDropdownContent.callCount, 0); - commentApiWrapper.loadComments().then() - .then(() => { - done(); - }); - }); - - test('_computePatchDropdownContent', () => { - const availablePatches = [ - {num: 'edit', sha: '1'}, - {num: 3, sha: '2'}, - {num: 2, sha: '3'}, - {num: 1, sha: '4'}, - ]; - const basePatchNum = 1; - const sortedRevisions = [ - {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'}, - {_number: element.EDIT_NAME, basePatchNum: 2}, - {_number: 2, description: 'description'}, - {_number: 1}, - ]; - - const expectedResult = [ - { - disabled: false, - triggerText: 'edit', - text: 'edit | 1', - mobileText: 'edit', - bottomText: '', - value: 'edit', - }, - { - disabled: false, - triggerText: 'Patchset 3', - text: 'Patchset 3 | 2', - mobileText: '3', - bottomText: '', - value: 3, - date: 'Mon, 01 Jan 2001 00:00:00 GMT', - }, - { - disabled: false, - triggerText: 'Patchset 2', - text: 'Patchset 2 | 3', - mobileText: '2 description', - bottomText: 'description', - value: 2, - }, - { - disabled: true, - triggerText: 'Patchset 1', - text: 'Patchset 1 | 4', - mobileText: '1', - bottomText: '', - value: 1, - }, - ]; - - assert.deepEqual(element._computePatchDropdownContent(availablePatches, - basePatchNum, sortedRevisions, element.changeComments), - expectedResult); - }); - - test('filesWeblinks', () => { - element.filesWeblinks = { - meta_a: [ - { - name: 'foo', - url: 'f.oo', - }, - ], - meta_b: [ - { - name: 'bar', - url: 'ba.r', - }, - ], - }; - flushAsynchronousOperations(); - const domApi = Polymer.dom(element.root); - assert.equal( - domApi.querySelector('a[href="f.oo"]').textContent, 'foo'); - assert.equal( - domApi.querySelector('a[href="ba.r"]').textContent, 'bar'); - }); - - test('_computePatchSetCommentsString', () => { - // Test string with unresolved comments. - element.changeComments._comments = { - foo: [{ - id: '27dcee4d_f7b77cfa', - message: 'test', - patch_set: 1, - unresolved: true, - updated: '2017-10-11 20:48:40.000000000', - }], - bar: [{ - id: '27dcee4d_f7b77cfa', - message: 'test', - patch_set: 1, - updated: '2017-10-12 20:48:40.000000000', - }, - { - id: '27dcee4d_f7b77cfa', - message: 'test', - patch_set: 1, - updated: '2017-10-13 20:48:40.000000000', - }], - abc: [], - }; - - assert.equal(element._computePatchSetCommentsString( - element.changeComments, 1), ' (3 comments, 1 unresolved)'); - - // Test string with no unresolved comments. - delete element.changeComments._comments['foo']; - assert.equal(element._computePatchSetCommentsString( - element.changeComments, 1), ' (2 comments)'); - - // Test string with no comments. - delete element.changeComments._comments['bar']; - assert.equal(element._computePatchSetCommentsString( - element.changeComments, 1), ''); - }); - - test('patch-range-change fires', () => { - const handler = sandbox.stub(); - element.basePatchNum = 1; - element.patchNum = 3; - element.addEventListener('patch-range-change', handler); - - element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]); - assert.isTrue(handler.calledOnce); - assert.deepEqual(handler.lastCall.args[0].detail, - {basePatchNum: 2, patchNum: 3}); - - // BasePatchNum should not have changed, due to one-way data binding. - element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]); - assert.deepEqual(handler.lastCall.args[0].detail, - {basePatchNum: 1, patchNum: 'edit'}); - }); + // Should be recomputed for each available patch + sandbox.stub(element, '_computePatchDropdownContent'); + element.set('basePatchNum', 1); + assert.equal(element._computePatchDropdownContent.callCount, 1); }); + + test('_computePatchDropdownContent called when comments update', done => { + element.revisions = [ + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + {commit: {parents: []}}, + ]; + element.revisionInfo = getInfo(element.revisions); + element.availablePatches = [ + {num: 1, sha: '1'}, + {num: 2, sha: '2'}, + {num: 3, sha: '3'}, + {num: 'edit', sha: '4'}, + ]; + element.patchNum = 2; + element.basePatchNum = 'PARENT'; + flushAsynchronousOperations(); + + // Should be recomputed for each available patch + sandbox.stub(element, '_computePatchDropdownContent'); + assert.equal(element._computePatchDropdownContent.callCount, 0); + commentApiWrapper.loadComments().then() + .then(() => { + done(); + }); + }); + + test('_computePatchDropdownContent', () => { + const availablePatches = [ + {num: 'edit', sha: '1'}, + {num: 3, sha: '2'}, + {num: 2, sha: '3'}, + {num: 1, sha: '4'}, + ]; + const basePatchNum = 1; + const sortedRevisions = [ + {_number: 3, created: 'Mon, 01 Jan 2001 00:00:00 GMT'}, + {_number: element.EDIT_NAME, basePatchNum: 2}, + {_number: 2, description: 'description'}, + {_number: 1}, + ]; + + const expectedResult = [ + { + disabled: false, + triggerText: 'edit', + text: 'edit | 1', + mobileText: 'edit', + bottomText: '', + value: 'edit', + }, + { + disabled: false, + triggerText: 'Patchset 3', + text: 'Patchset 3 | 2', + mobileText: '3', + bottomText: '', + value: 3, + date: 'Mon, 01 Jan 2001 00:00:00 GMT', + }, + { + disabled: false, + triggerText: 'Patchset 2', + text: 'Patchset 2 | 3', + mobileText: '2 description', + bottomText: 'description', + value: 2, + }, + { + disabled: true, + triggerText: 'Patchset 1', + text: 'Patchset 1 | 4', + mobileText: '1', + bottomText: '', + value: 1, + }, + ]; + + assert.deepEqual(element._computePatchDropdownContent(availablePatches, + basePatchNum, sortedRevisions, element.changeComments), + expectedResult); + }); + + test('filesWeblinks', () => { + element.filesWeblinks = { + meta_a: [ + { + name: 'foo', + url: 'f.oo', + }, + ], + meta_b: [ + { + name: 'bar', + url: 'ba.r', + }, + ], + }; + flushAsynchronousOperations(); + const domApi = dom(element.root); + assert.equal( + domApi.querySelector('a[href="f.oo"]').textContent, 'foo'); + assert.equal( + domApi.querySelector('a[href="ba.r"]').textContent, 'bar'); + }); + + test('_computePatchSetCommentsString', () => { + // Test string with unresolved comments. + element.changeComments._comments = { + foo: [{ + id: '27dcee4d_f7b77cfa', + message: 'test', + patch_set: 1, + unresolved: true, + updated: '2017-10-11 20:48:40.000000000', + }], + bar: [{ + id: '27dcee4d_f7b77cfa', + message: 'test', + patch_set: 1, + updated: '2017-10-12 20:48:40.000000000', + }, + { + id: '27dcee4d_f7b77cfa', + message: 'test', + patch_set: 1, + updated: '2017-10-13 20:48:40.000000000', + }], + abc: [], + }; + + assert.equal(element._computePatchSetCommentsString( + element.changeComments, 1), ' (3 comments, 1 unresolved)'); + + // Test string with no unresolved comments. + delete element.changeComments._comments['foo']; + assert.equal(element._computePatchSetCommentsString( + element.changeComments, 1), ' (2 comments)'); + + // Test string with no comments. + delete element.changeComments._comments['bar']; + assert.equal(element._computePatchSetCommentsString( + element.changeComments, 1), ''); + }); + + test('patch-range-change fires', () => { + const handler = sandbox.stub(); + element.basePatchNum = 1; + element.patchNum = 3; + element.addEventListener('patch-range-change', handler); + + element.$.basePatchDropdown._handleValueChange(2, [{value: 2}]); + assert.isTrue(handler.calledOnce); + assert.deepEqual(handler.lastCall.args[0].detail, + {basePatchNum: 2, patchNum: 3}); + + // BasePatchNum should not have changed, due to one-way data binding. + element.$.patchNumDropdown._handleValueChange('edit', [{value: 'edit'}]); + assert.deepEqual(handler.lastCall.args[0].detail, + {basePatchNum: 1, patchNum: 'edit'}); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js index fd94b61..8f1b1c3 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
@@ -14,204 +14,210 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Polymer 1 adds # before array's key, while Polymer 2 doesn't - const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/; +import '../gr-diff-highlight/gr-annotation.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-ranged-comment-layer_html.js'; - const RANGE_HIGHLIGHT = 'style-scope gr-diff range'; - const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight'; +// Polymer 1 adds # before array's key, while Polymer 2 doesn't +const HOVER_PATH_PATTERN = /^(commentRanges\.\#?\d+)\.hovering$/; - /** @extends Polymer.Element */ - class GrRangedCommentLayer extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-ranged-comment-layer'; } - /** - * Fired when the range in a range comment was malformed and had to be - * normalized. - * - * It's `detail` has a `lineNum` and `side` parameter. - * - * @event normalize-range - */ +const RANGE_HIGHLIGHT = 'style-scope gr-diff range'; +const HOVER_HIGHLIGHT = 'style-scope gr-diff rangeHighlight'; - static get properties() { - return { - /** @type {!Array<!Gerrit.HoveredRange>} */ - commentRanges: Array, - _listeners: { - type: Array, - value() { return []; }, - }, - _rangesMap: { - type: Object, - value() { return {left: {}, right: {}}; }, - }, - }; +/** @extends Polymer.Element */ +class GrRangedCommentLayer extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-ranged-comment-layer'; } + /** + * Fired when the range in a range comment was malformed and had to be + * normalized. + * + * It's `detail` has a `lineNum` and `side` parameter. + * + * @event normalize-range + */ + + static get properties() { + return { + /** @type {!Array<!Gerrit.HoveredRange>} */ + commentRanges: Array, + _listeners: { + type: Array, + value() { return []; }, + }, + _rangesMap: { + type: Object, + value() { return {left: {}, right: {}}; }, + }, + }; + } + + static get observers() { + return [ + '_handleCommentRangesChange(commentRanges.*)', + ]; + } + + get styleModuleName() { + return 'gr-ranged-comment-styles'; + } + + /** + * Layer method to add annotations to a line. + * + * @param {!HTMLElement} el The DIV.contentText element to apply the + * annotation to. + * @param {!HTMLElement} lineNumberEl + * @param {!Object} line The line object. (GrDiffLine) + */ + annotate(el, lineNumberEl, line) { + let ranges = []; + if (line.type === GrDiffLine.Type.REMOVE || ( + line.type === GrDiffLine.Type.BOTH && + el.getAttribute('data-side') !== 'right')) { + ranges = ranges.concat(this._getRangesForLine(line, 'left')); + } + if (line.type === GrDiffLine.Type.ADD || ( + line.type === GrDiffLine.Type.BOTH && + el.getAttribute('data-side') !== 'left')) { + ranges = ranges.concat(this._getRangesForLine(line, 'right')); } - static get observers() { - return [ - '_handleCommentRangesChange(commentRanges.*)', - ]; + for (const range of ranges) { + GrAnnotation.annotateElement(el, range.start, + range.end - range.start, + range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT); } + } - get styleModuleName() { - return 'gr-ranged-comment-styles'; + /** + * Register a listener for layer updates. + * + * @param {function(number, number, string)} fn The update handler function. + * Should accept as arguments the line numbers for the start and end of + * the update and the side as a string. + */ + addListener(fn) { + this._listeners.push(fn); + } + + /** + * Notify Layer listeners of changes to annotations. + * + * @param {number} start The line where the update starts. + * @param {number} end The line where the update ends. + * @param {string} side The side of the update. ('left' or 'right') + */ + _notifyUpdateRange(start, end, side) { + for (const listener of this._listeners) { + listener(start, end, side); } + } - /** - * Layer method to add annotations to a line. - * - * @param {!HTMLElement} el The DIV.contentText element to apply the - * annotation to. - * @param {!HTMLElement} lineNumberEl - * @param {!Object} line The line object. (GrDiffLine) - */ - annotate(el, lineNumberEl, line) { - let ranges = []; - if (line.type === GrDiffLine.Type.REMOVE || ( - line.type === GrDiffLine.Type.BOTH && - el.getAttribute('data-side') !== 'right')) { - ranges = ranges.concat(this._getRangesForLine(line, 'left')); - } - if (line.type === GrDiffLine.Type.ADD || ( - line.type === GrDiffLine.Type.BOTH && - el.getAttribute('data-side') !== 'left')) { - ranges = ranges.concat(this._getRangesForLine(line, 'right')); - } + /** + * Handle change in the ranges by updating the ranges maps and by + * emitting appropriate update notifications. + * + * @param {Object} record The change record. + */ + _handleCommentRangesChange(record) { + if (!record) return; - for (const range of ranges) { - GrAnnotation.annotateElement(el, range.start, - range.end - range.start, - range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT); + // If the entire set of comments was changed. + if (record.path === 'commentRanges') { + this._rangesMap = {left: {}, right: {}}; + for (const {side, range, hovering} of record.value) { + this._updateRangesMap( + side, range, hovering, (forLine, start, end, hovering) => { + forLine.push({start, end, hovering}); + }); } } - /** - * Register a listener for layer updates. - * - * @param {function(number, number, string)} fn The update handler function. - * Should accept as arguments the line numbers for the start and end of - * the update and the side as a string. - */ - addListener(fn) { - this._listeners.push(fn); + // If the change only changed the `hovering` property of a comment. + const match = record.path.match(HOVER_PATH_PATTERN); + if (match) { + // The #number indicates the key of that item in the array + // not the index, especially in polymer 1. + const {side, range, hovering} = this.get(match[1]); + + this._updateRangesMap( + side, range, hovering, (forLine, start, end, hovering) => { + const index = forLine.findIndex(lineRange => + lineRange.start === start && lineRange.end === end); + forLine[index].hovering = hovering; + }); } - /** - * Notify Layer listeners of changes to annotations. - * - * @param {number} start The line where the update starts. - * @param {number} end The line where the update ends. - * @param {string} side The side of the update. ('left' or 'right') - */ - _notifyUpdateRange(start, end, side) { - for (const listener of this._listeners) { - listener(start, end, side); - } - } - - /** - * Handle change in the ranges by updating the ranges maps and by - * emitting appropriate update notifications. - * - * @param {Object} record The change record. - */ - _handleCommentRangesChange(record) { - if (!record) return; - - // If the entire set of comments was changed. - if (record.path === 'commentRanges') { - this._rangesMap = {left: {}, right: {}}; - for (const {side, range, hovering} of record.value) { + // If comments were spliced in or out. + if (record.path === 'commentRanges.splices') { + for (const indexSplice of record.value.indexSplices) { + const removed = indexSplice.removed; + for (const {side, range, hovering} of removed) { + this._updateRangesMap( + side, range, hovering, (forLine, start, end) => { + const index = forLine.findIndex(lineRange => + lineRange.start === start && lineRange.end === end); + forLine.splice(index, 1); + }); + } + const added = indexSplice.object.slice( + indexSplice.index, indexSplice.index + indexSplice.addedCount); + for (const {side, range, hovering} of added) { this._updateRangesMap( side, range, hovering, (forLine, start, end, hovering) => { forLine.push({start, end, hovering}); }); } } - - // If the change only changed the `hovering` property of a comment. - const match = record.path.match(HOVER_PATH_PATTERN); - if (match) { - // The #number indicates the key of that item in the array - // not the index, especially in polymer 1. - const {side, range, hovering} = this.get(match[1]); - - this._updateRangesMap( - side, range, hovering, (forLine, start, end, hovering) => { - const index = forLine.findIndex(lineRange => - lineRange.start === start && lineRange.end === end); - forLine[index].hovering = hovering; - }); - } - - // If comments were spliced in or out. - if (record.path === 'commentRanges.splices') { - for (const indexSplice of record.value.indexSplices) { - const removed = indexSplice.removed; - for (const {side, range, hovering} of removed) { - this._updateRangesMap( - side, range, hovering, (forLine, start, end) => { - const index = forLine.findIndex(lineRange => - lineRange.start === start && lineRange.end === end); - forLine.splice(index, 1); - }); - } - const added = indexSplice.object.slice( - indexSplice.index, indexSplice.index + indexSplice.addedCount); - for (const {side, range, hovering} of added) { - this._updateRangesMap( - side, range, hovering, (forLine, start, end, hovering) => { - forLine.push({start, end, hovering}); - }); - } - } - } - } - - _updateRangesMap(side, range, hovering, operation) { - 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); - } - this._notifyUpdateRange(range.start_line, range.end_line, side); - } - - _getRangesForLine(line, side) { - const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; - const ranges = this.get(['_rangesMap', side, lineNum]) || []; - return ranges - .map(range => { - // Make a copy, so that the normalization below does not mess with - // our map. - range = Object.assign({}, 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; - this.dispatchEvent(new CustomEvent('normalize-range', { - bubbles: true, - composed: true, - detail: {lineNum, side}, - })); - } - - return range; - }) - // Sort the ranges so that hovering highlights are on top. - .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0)); } } - customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer); -})(); + _updateRangesMap(side, range, hovering, operation) { + 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); + } + this._notifyUpdateRange(range.start_line, range.end_line, side); + } + + _getRangesForLine(line, side) { + const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber; + const ranges = this.get(['_rangesMap', side, lineNum]) || []; + return ranges + .map(range => { + // Make a copy, so that the normalization below does not mess with + // our map. + range = Object.assign({}, 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; + this.dispatchEvent(new CustomEvent('normalize-range', { + bubbles: true, + composed: true, + detail: {lineNum, side}, + })); + } + + return range; + }) + // Sort the ranges so that hovering highlights are on top. + .sort((a, b) => (a.hovering && !b.hovering ? 1 : 0)); + } +} + +customElements.define(GrRangedCommentLayer.is, GrRangedCommentLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js index 7625c8a..29757e5 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_html.js
@@ -1,25 +1,21 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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 +export const htmlTemplate = html` -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<script src="../gr-diff-highlight/gr-annotation.js"></script> - -<dom-module id="gr-ranged-comment-layer"> - <template> - </template> - <script src="gr-ranged-comment-layer.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html index 48883c1..d2d97de 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-ranged-comment-layer</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../gr-diff/gr-diff-line.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../gr-diff/gr-diff-line.js"></script> -<link rel="import" href="gr-ranged-comment-layer.html"> +<script type="module" src="./gr-ranged-comment-layer.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-diff/gr-diff-line.js'; +import './gr-ranged-comment-layer.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,310 +43,313 @@ </template> </test-fixture> -<script> - suite('gr-ranged-comment-layer', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-diff/gr-diff-line.js'; +import './gr-ranged-comment-layer.js'; +suite('gr-ranged-comment-layer', () => { + let element; + let sandbox; + + setup(() => { + const initialCommentRanges = [ + { + side: 'left', + range: { + end_character: 9, + end_line: 39, + start_character: 6, + start_line: 36, + }, + }, + { + side: 'right', + range: { + end_character: 22, + end_line: 12, + start_character: 10, + start_line: 10, + }, + }, + { + side: 'right', + range: { + end_character: 15, + end_line: 100, + start_character: 5, + start_line: 100, + }, + }, + { + side: 'right', + range: { + end_character: 2, + end_line: 55, + start_character: 32, + start_line: 55, + }, + }, + ]; + + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.commentRanges = initialCommentRanges; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('annotate', () => { let sandbox; + let el; + let line; + let annotateElementStub; + const lineNumberEl = document.createElement('td'); setup(() => { - const initialCommentRanges = [ - { - side: 'left', - range: { - end_character: 9, - end_line: 39, - start_character: 6, - start_line: 36, - }, - }, - { - side: 'right', - range: { - end_character: 22, - end_line: 12, - start_character: 10, - start_line: 10, - }, - }, - { - side: 'right', - range: { - end_character: 15, - end_line: 100, - start_character: 5, - start_line: 100, - }, - }, - { - side: 'right', - range: { - end_character: 2, - end_line: 55, - start_character: 32, - start_line: 55, - }, - }, - ]; - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.commentRanges = initialCommentRanges; + annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); + el = document.createElement('div'); + el.setAttribute('data-side', 'left'); + line = new GrDiffLine(GrDiffLine.Type.BOTH); + line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,'; }); teardown(() => { sandbox.restore(); }); - suite('annotate', () => { - let sandbox; - let el; - let line; - let annotateElementStub; - const lineNumberEl = document.createElement('td'); + test('type=Remove no-comment', () => { + line.type = GrDiffLine.Type.REMOVE; + line.beforeNumber = 40; - setup(() => { - sandbox = sinon.sandbox.create(); - annotateElementStub = sandbox.stub(GrAnnotation, 'annotateElement'); - el = document.createElement('div'); - el.setAttribute('data-side', 'left'); - line = new GrDiffLine(GrDiffLine.Type.BOTH); - line.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,'; - }); + element.annotate(el, lineNumberEl, line); - teardown(() => { - sandbox.restore(); - }); - - test('type=Remove no-comment', () => { - line.type = GrDiffLine.Type.REMOVE; - line.beforeNumber = 40; - - element.annotate(el, lineNumberEl, line); - - assert.isFalse(annotateElementStub.called); - }); - - test('type=Remove has-comment', () => { - line.type = GrDiffLine.Type.REMOVE; - line.beforeNumber = 36; - const expectedStart = 6; - const expectedLength = line.text.length - expectedStart; - - element.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementStub.called); - const lastCall = annotateElementStub.lastCall; - assert.equal(lastCall.args[0], el); - assert.equal(lastCall.args[1], expectedStart); - assert.equal(lastCall.args[2], expectedLength); - assert.equal(lastCall.args[3], 'style-scope gr-diff range'); - }); - - test('type=Remove has-comment hovering', () => { - line.type = GrDiffLine.Type.REMOVE; - line.beforeNumber = 36; - element.set(['commentRanges', 0, 'hovering'], true); - - const expectedStart = 6; - const expectedLength = line.text.length - expectedStart; - - element.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementStub.called); - const lastCall = annotateElementStub.lastCall; - assert.equal(lastCall.args[0], el); - assert.equal(lastCall.args[1], expectedStart); - assert.equal(lastCall.args[2], expectedLength); - assert.equal(lastCall.args[3], 'style-scope gr-diff rangeHighlight'); - }); - - test('type=Both has-comment', () => { - line.type = GrDiffLine.Type.BOTH; - line.beforeNumber = 36; - - const expectedStart = 6; - const expectedLength = line.text.length - expectedStart; - - element.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementStub.called); - const lastCall = annotateElementStub.lastCall; - assert.equal(lastCall.args[0], el); - assert.equal(lastCall.args[1], expectedStart); - assert.equal(lastCall.args[2], expectedLength); - assert.equal(lastCall.args[3], 'style-scope gr-diff range'); - }); - - test('type=Both has-comment off side', () => { - line.type = GrDiffLine.Type.BOTH; - line.beforeNumber = 36; - el.setAttribute('data-side', 'right'); - - element.annotate(el, lineNumberEl, line); - - assert.isFalse(annotateElementStub.called); - }); - - test('type=Add has-comment', () => { - line.type = GrDiffLine.Type.ADD; - line.afterNumber = 12; - el.setAttribute('data-side', 'right'); - - const expectedStart = 0; - const expectedLength = 22; - - element.annotate(el, lineNumberEl, line); - - assert.isTrue(annotateElementStub.called); - const lastCall = annotateElementStub.lastCall; - assert.equal(lastCall.args[0], el); - assert.equal(lastCall.args[1], expectedStart); - assert.equal(lastCall.args[2], expectedLength); - assert.equal(lastCall.args[3], 'style-scope gr-diff range'); - }); + assert.isFalse(annotateElementStub.called); }); - test('_handleCommentRangesChange overwrite', () => { - element.set('commentRanges', []); + test('type=Remove has-comment', () => { + line.type = GrDiffLine.Type.REMOVE; + line.beforeNumber = 36; + const expectedStart = 6; + const expectedLength = line.text.length - expectedStart; - assert.equal(Object.keys(element._rangesMap.left).length, 0); - assert.equal(Object.keys(element._rangesMap.right).length, 0); + element.annotate(el, lineNumberEl, line); + + assert.isTrue(annotateElementStub.called); + const lastCall = annotateElementStub.lastCall; + assert.equal(lastCall.args[0], el); + assert.equal(lastCall.args[1], expectedStart); + assert.equal(lastCall.args[2], expectedLength); + assert.equal(lastCall.args[3], 'style-scope gr-diff range'); }); - test('_handleCommentRangesChange hovering', () => { - const notifyStub = sinon.stub(); - element.addListener(notifyStub); - const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap'); + test('type=Remove has-comment hovering', () => { + line.type = GrDiffLine.Type.REMOVE; + line.beforeNumber = 36; + element.set(['commentRanges', 0, 'hovering'], true); - element.set(['commentRanges', 1, 'hovering'], true); + const expectedStart = 6; + const expectedLength = line.text.length - expectedStart; - assert.isTrue(notifyStub.called); - const lastCall = notifyStub.lastCall; - assert.equal(lastCall.args[0], 10); - assert.equal(lastCall.args[1], 12); - assert.equal(lastCall.args[2], 'right'); + element.annotate(el, lineNumberEl, line); - assert.isTrue(updateRangesMapSpy.called); + assert.isTrue(annotateElementStub.called); + const lastCall = annotateElementStub.lastCall; + assert.equal(lastCall.args[0], el); + assert.equal(lastCall.args[1], expectedStart); + assert.equal(lastCall.args[2], expectedLength); + assert.equal(lastCall.args[3], 'style-scope gr-diff rangeHighlight'); }); - test('_handleCommentRangesChange splice out', () => { - const notifyStub = sinon.stub(); - element.addListener(notifyStub); + test('type=Both has-comment', () => { + line.type = GrDiffLine.Type.BOTH; + line.beforeNumber = 36; - element.splice('commentRanges', 1, 1); + const expectedStart = 6; + const expectedLength = line.text.length - expectedStart; - assert.isTrue(notifyStub.called); - const lastCall = notifyStub.lastCall; - assert.equal(lastCall.args[0], 10); - assert.equal(lastCall.args[1], 12); - assert.equal(lastCall.args[2], 'right'); + element.annotate(el, lineNumberEl, line); + + assert.isTrue(annotateElementStub.called); + const lastCall = annotateElementStub.lastCall; + assert.equal(lastCall.args[0], el); + assert.equal(lastCall.args[1], expectedStart); + assert.equal(lastCall.args[2], expectedLength); + assert.equal(lastCall.args[3], 'style-scope gr-diff range'); }); - test('_handleCommentRangesChange splice in', () => { - const notifyStub = sinon.stub(); - element.addListener(notifyStub); + test('type=Both has-comment off side', () => { + line.type = GrDiffLine.Type.BOTH; + line.beforeNumber = 36; + el.setAttribute('data-side', 'right'); - element.splice('commentRanges', 1, 0, { - side: 'left', - range: { - end_character: 15, - end_line: 275, - start_character: 5, - start_line: 250, - }, - }); + element.annotate(el, lineNumberEl, line); - assert.isTrue(notifyStub.called); - const lastCall = notifyStub.lastCall; - assert.equal(lastCall.args[0], 250); - assert.equal(lastCall.args[1], 275); - assert.equal(lastCall.args[2], 'left'); + assert.isFalse(annotateElementStub.called); }); - test('_handleCommentRangesChange mixed actions', () => { - const notifyStub = sinon.stub(); - element.addListener(notifyStub); - const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap'); + test('type=Add has-comment', () => { + line.type = GrDiffLine.Type.ADD; + line.afterNumber = 12; + el.setAttribute('data-side', 'right'); - element.set(['commentRanges', 1, 'hovering'], true); - assert.isTrue(updateRangesMapSpy.callCount === 1); - element.splice('commentRanges', 1, 1); - assert.isTrue(updateRangesMapSpy.callCount === 2); - element.splice('commentRanges', 1, 1); - assert.isTrue(updateRangesMapSpy.callCount === 3); - element.splice('commentRanges', 1, 0, { - side: 'left', - range: { - end_character: 15, - end_line: 275, - start_character: 5, - start_line: 250, - }, - }); - assert.isTrue(updateRangesMapSpy.callCount === 4); - element.set(['commentRanges', 2, 'hovering'], true); - assert.isTrue(updateRangesMapSpy.callCount === 5); - }); + const expectedStart = 0; + const expectedLength = 22; - test('_computeCommentMap creates maps correctly', () => { - // There is only one ranged comment on the left, but it spans ll.36-39. - const leftKeys = []; - for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); } - assert.deepEqual(Object.keys(element._rangesMap.left).sort(), - leftKeys.sort()); + element.annotate(el, lineNumberEl, line); - assert.equal(element._rangesMap.left[36].length, 1); - assert.equal(element._rangesMap.left[36][0].start, 6); - assert.equal(element._rangesMap.left[36][0].end, -1); - - assert.equal(element._rangesMap.left[37].length, 1); - assert.equal(element._rangesMap.left[37][0].start, 0); - assert.equal(element._rangesMap.left[37][0].end, -1); - - assert.equal(element._rangesMap.left[38].length, 1); - assert.equal(element._rangesMap.left[38][0].start, 0); - assert.equal(element._rangesMap.left[38][0].end, -1); - - assert.equal(element._rangesMap.left[39].length, 1); - assert.equal(element._rangesMap.left[39][0].start, 0); - assert.equal(element._rangesMap.left[39][0].end, 9); - - // The right has two ranged comments, one spanning ll.10-12 and the other - // on line 100. - const rightKeys = []; - for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); } - rightKeys.push('55', '100'); - assert.deepEqual(Object.keys(element._rangesMap.right).sort(), - rightKeys.sort()); - - assert.equal(element._rangesMap.right[10].length, 1); - assert.equal(element._rangesMap.right[10][0].start, 10); - assert.equal(element._rangesMap.right[10][0].end, -1); - - assert.equal(element._rangesMap.right[11].length, 1); - assert.equal(element._rangesMap.right[11][0].start, 0); - assert.equal(element._rangesMap.right[11][0].end, -1); - - assert.equal(element._rangesMap.right[12].length, 1); - assert.equal(element._rangesMap.right[12][0].start, 0); - assert.equal(element._rangesMap.right[12][0].end, 22); - - assert.equal(element._rangesMap.right[100].length, 1); - assert.equal(element._rangesMap.right[100][0].start, 5); - assert.equal(element._rangesMap.right[100][0].end, 15); - }); - - test('_getRangesForLine normalizes invalid ranges', () => { - const line = { - afterNumber: 55, - text: '_getRangesForLine normalizes invalid ranges', - }; - const ranges = element._getRangesForLine(line, 'right'); - assert.equal(ranges.length, 1); - const range = ranges[0]; - assert.isTrue(range.start < range.end, 'start and end are normalized'); - assert.equal(range.end, line.text.length); + assert.isTrue(annotateElementStub.called); + const lastCall = annotateElementStub.lastCall; + assert.equal(lastCall.args[0], el); + assert.equal(lastCall.args[1], expectedStart); + assert.equal(lastCall.args[2], expectedLength); + assert.equal(lastCall.args[3], 'style-scope gr-diff range'); }); }); + + test('_handleCommentRangesChange overwrite', () => { + element.set('commentRanges', []); + + assert.equal(Object.keys(element._rangesMap.left).length, 0); + assert.equal(Object.keys(element._rangesMap.right).length, 0); + }); + + test('_handleCommentRangesChange hovering', () => { + const notifyStub = sinon.stub(); + element.addListener(notifyStub); + const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap'); + + element.set(['commentRanges', 1, 'hovering'], true); + + assert.isTrue(notifyStub.called); + const lastCall = notifyStub.lastCall; + assert.equal(lastCall.args[0], 10); + assert.equal(lastCall.args[1], 12); + assert.equal(lastCall.args[2], 'right'); + + assert.isTrue(updateRangesMapSpy.called); + }); + + test('_handleCommentRangesChange splice out', () => { + const notifyStub = sinon.stub(); + element.addListener(notifyStub); + + element.splice('commentRanges', 1, 1); + + assert.isTrue(notifyStub.called); + const lastCall = notifyStub.lastCall; + assert.equal(lastCall.args[0], 10); + assert.equal(lastCall.args[1], 12); + assert.equal(lastCall.args[2], 'right'); + }); + + test('_handleCommentRangesChange splice in', () => { + const notifyStub = sinon.stub(); + element.addListener(notifyStub); + + element.splice('commentRanges', 1, 0, { + side: 'left', + range: { + end_character: 15, + end_line: 275, + start_character: 5, + start_line: 250, + }, + }); + + assert.isTrue(notifyStub.called); + const lastCall = notifyStub.lastCall; + assert.equal(lastCall.args[0], 250); + assert.equal(lastCall.args[1], 275); + assert.equal(lastCall.args[2], 'left'); + }); + + test('_handleCommentRangesChange mixed actions', () => { + const notifyStub = sinon.stub(); + element.addListener(notifyStub); + const updateRangesMapSpy = sandbox.spy(element, '_updateRangesMap'); + + element.set(['commentRanges', 1, 'hovering'], true); + assert.isTrue(updateRangesMapSpy.callCount === 1); + element.splice('commentRanges', 1, 1); + assert.isTrue(updateRangesMapSpy.callCount === 2); + element.splice('commentRanges', 1, 1); + assert.isTrue(updateRangesMapSpy.callCount === 3); + element.splice('commentRanges', 1, 0, { + side: 'left', + range: { + end_character: 15, + end_line: 275, + start_character: 5, + start_line: 250, + }, + }); + assert.isTrue(updateRangesMapSpy.callCount === 4); + element.set(['commentRanges', 2, 'hovering'], true); + assert.isTrue(updateRangesMapSpy.callCount === 5); + }); + + test('_computeCommentMap creates maps correctly', () => { + // There is only one ranged comment on the left, but it spans ll.36-39. + const leftKeys = []; + for (let i = 36; i <= 39; i++) { leftKeys.push('' + i); } + assert.deepEqual(Object.keys(element._rangesMap.left).sort(), + leftKeys.sort()); + + assert.equal(element._rangesMap.left[36].length, 1); + assert.equal(element._rangesMap.left[36][0].start, 6); + assert.equal(element._rangesMap.left[36][0].end, -1); + + assert.equal(element._rangesMap.left[37].length, 1); + assert.equal(element._rangesMap.left[37][0].start, 0); + assert.equal(element._rangesMap.left[37][0].end, -1); + + assert.equal(element._rangesMap.left[38].length, 1); + assert.equal(element._rangesMap.left[38][0].start, 0); + assert.equal(element._rangesMap.left[38][0].end, -1); + + assert.equal(element._rangesMap.left[39].length, 1); + assert.equal(element._rangesMap.left[39][0].start, 0); + assert.equal(element._rangesMap.left[39][0].end, 9); + + // The right has two ranged comments, one spanning ll.10-12 and the other + // on line 100. + const rightKeys = []; + for (let i = 10; i <= 12; i++) { rightKeys.push('' + i); } + rightKeys.push('55', '100'); + assert.deepEqual(Object.keys(element._rangesMap.right).sort(), + rightKeys.sort()); + + assert.equal(element._rangesMap.right[10].length, 1); + assert.equal(element._rangesMap.right[10][0].start, 10); + assert.equal(element._rangesMap.right[10][0].end, -1); + + assert.equal(element._rangesMap.right[11].length, 1); + assert.equal(element._rangesMap.right[11][0].start, 0); + assert.equal(element._rangesMap.right[11][0].end, -1); + + assert.equal(element._rangesMap.right[12].length, 1); + assert.equal(element._rangesMap.right[12][0].start, 0); + assert.equal(element._rangesMap.right[12][0].end, 22); + + assert.equal(element._rangesMap.right[100].length, 1); + assert.equal(element._rangesMap.right[100][0].start, 5); + assert.equal(element._rangesMap.right[100][0].end, 15); + }); + + test('_getRangesForLine normalizes invalid ranges', () => { + const line = { + afterNumber: 55, + text: '_getRangesForLine normalizes invalid ranges', + }; + const ranges = element._getRangesForLine(line, 'right'); + assert.equal(ranges.length, 1); + const range = ranges[0]; + assert.isTrue(range.start < range.end, 'start and end are normalized'); + assert.equal(range.end, line.text.length); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js index cefd241..49ed980 100644 --- a/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js +++ b/polygerrit-ui/app/elements/diff/gr-ranged-comment-themes/gr-ranged-comment-theme.js
@@ -1,20 +1,22 @@ -<!-- -@license -Copyright (C) 2019 The Android Open Source Project +/** + * @license + * Copyright (C) 2019 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. + */ +const $_documentContainer = document.createElement('template'); -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. ---> -<dom-module id="gr-ranged-comment-theme"> +$_documentContainer.innerHTML = `<dom-module id="gr-ranged-comment-theme"> <template> <style> .range { @@ -27,4 +29,13 @@ } </style> </template> -</dom-module> +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js index 3d831c9..20d3081 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box.js
@@ -14,93 +14,104 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-tooltip/gr-tooltip.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-selection-action-box_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrSelectionActionBox extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-selection-action-box'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the comment creation action was taken (click). + * + * @event create-comment-requested */ - class GrSelectionActionBox extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-selection-action-box'; } - /** - * Fired when the comment creation action was taken (click). - * - * @event create-comment-requested - */ - static get properties() { - return { - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - positionBelow: Boolean, - }; - } - - /** @override */ - created() { - super.created(); - - // See https://crbug.com/gerrit/4767 - this.addEventListener('mousedown', - e => this._handleMouseDown(e)); - } - - placeAbove(el) { - Polymer.dom.flush(); - const rect = this._getTargetBoundingRect(el); - const boxRect = this.$.tooltip.getBoundingClientRect(); - const parentRect = this._getParentBoundingClientRect(); - this.style.top = - rect.top - parentRect.top - boxRect.height - 6 + 'px'; - this.style.left = - rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; - } - - placeBelow(el) { - Polymer.dom.flush(); - const rect = this._getTargetBoundingRect(el); - const boxRect = this.$.tooltip.getBoundingClientRect(); - const parentRect = this._getParentBoundingClientRect(); - this.style.top = - rect.top - parentRect.top + boxRect.height - 6 + 'px'; - this.style.left = - rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; - } - - _getParentBoundingClientRect() { - // With native shadow DOM, the parent is the shadow root, not the gr-diff - // element - const parent = this.parentElement || this.parentNode.host; - return parent.getBoundingClientRect(); - } - - _getTargetBoundingRect(el) { - let rect; - if (el instanceof Text) { - const range = document.createRange(); - range.selectNode(el); - rect = range.getBoundingClientRect(); - range.detach(); - } else { - rect = el.getBoundingClientRect(); - } - return rect; - } - - _handleMouseDown(e) { - if (e.button !== 0) { return; } // 0 = main button - e.preventDefault(); - e.stopPropagation(); - this.fire('create-comment-requested'); - } + static get properties() { + return { + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + positionBelow: Boolean, + }; } - customElements.define(GrSelectionActionBox.is, GrSelectionActionBox); -})(); + /** @override */ + created() { + super.created(); + + // See https://crbug.com/gerrit/4767 + this.addEventListener('mousedown', + e => this._handleMouseDown(e)); + } + + placeAbove(el) { + flush(); + const rect = this._getTargetBoundingRect(el); + const boxRect = this.$.tooltip.getBoundingClientRect(); + const parentRect = this._getParentBoundingClientRect(); + this.style.top = + rect.top - parentRect.top - boxRect.height - 6 + 'px'; + this.style.left = + rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; + } + + placeBelow(el) { + flush(); + const rect = this._getTargetBoundingRect(el); + const boxRect = this.$.tooltip.getBoundingClientRect(); + const parentRect = this._getParentBoundingClientRect(); + this.style.top = + rect.top - parentRect.top + boxRect.height - 6 + 'px'; + this.style.left = + rect.left - parentRect.left + (rect.width - boxRect.width) / 2 + 'px'; + } + + _getParentBoundingClientRect() { + // With native shadow DOM, the parent is the shadow root, not the gr-diff + // element + const parent = this.parentElement || this.parentNode.host; + return parent.getBoundingClientRect(); + } + + _getTargetBoundingRect(el) { + let rect; + if (el instanceof Text) { + const range = document.createRange(); + range.selectNode(el); + rect = range.getBoundingClientRect(); + range.detach(); + } else { + rect = el.getBoundingClientRect(); + } + return rect; + } + + _handleMouseDown(e) { + if (e.button !== 0) { return; } // 0 = main button + e.preventDefault(); + e.stopPropagation(); + this.fire('create-comment-requested'); + } +} + +customElements.define(GrSelectionActionBox.is, GrSelectionActionBox);
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js index aa4d2e1..670a755 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-tooltip/gr-tooltip.html"> - -<dom-module id="gr-selection-action-box"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { cursor: pointer; @@ -31,10 +25,5 @@ white-space: nowrap; } </style> - <gr-tooltip - id="tooltip" - text="Press c to comment" - position-below="[[positionBelow]]"></gr-tooltip> - </template> - <script src="gr-selection-action-box.js"></script> -</dom-module> + <gr-tooltip id="tooltip" text="Press c to comment" position-below="[[positionBelow]]"></gr-tooltip> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html index bb802f8..c0c711a 100644 --- a/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html +++ b/polygerrit-ui/app/elements/diff/gr-selection-action-box/gr-selection-action-box_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-selection-action-box</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-selection-action-box.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-selection-action-box.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-selection-action-box.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -38,98 +43,100 @@ </template> </test-fixture> -<script> - suite('gr-selection-action-box', async () => { - await readyToTest(); - let container; - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-selection-action-box.js'; +suite('gr-selection-action-box', () => { + let container; + let element; + let sandbox; + + setup(() => { + container = fixture('basic'); + element = container.querySelector('gr-selection-action-box'); + sandbox = sinon.sandbox.create(); + sandbox.stub(element, 'fire'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('ignores regular keys', () => { + MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc'); + assert.isFalse(element.fire.called); + }); + + suite('mousedown reacts only to main button', () => { + let e; setup(() => { - container = fixture('basic'); - element = container.querySelector('gr-selection-action-box'); - sandbox = sinon.sandbox.create(); - sandbox.stub(element, 'fire'); + e = { + button: 0, + preventDefault: sandbox.stub(), + stopPropagation: sandbox.stub(), + }; }); - teardown(() => { - sandbox.restore(); + test('event handled if main button', () => { + element._handleMouseDown(e); + assert.isTrue(e.preventDefault.called); + assert(element.fire.calledWithExactly('create-comment-requested')); }); - test('ignores regular keys', () => { - MockInteractions.pressAndReleaseKeyOn(document.body, 27, null, 'esc'); + test('event ignored if not main button', () => { + e.button = 1; + element._handleMouseDown(e); + assert.isFalse(e.preventDefault.called); assert.isFalse(element.fire.called); }); + }); - suite('mousedown reacts only to main button', () => { - let e; + suite('placeAbove', () => { + let target; - setup(() => { - e = { - button: 0, - preventDefault: sandbox.stub(), - stopPropagation: sandbox.stub(), - }; - }); - - test('event handled if main button', () => { - element._handleMouseDown(e); - assert.isTrue(e.preventDefault.called); - assert(element.fire.calledWithExactly('create-comment-requested')); - }); - - test('event ignored if not main button', () => { - e.button = 1; - element._handleMouseDown(e); - assert.isFalse(e.preventDefault.called); - assert.isFalse(element.fire.called); - }); + setup(() => { + target = container.querySelector('.target'); + sandbox.stub(container, 'getBoundingClientRect').returns( + {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6}); + sandbox.stub(element, '_getTargetBoundingRect').returns( + {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60}); + sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns( + {width: 10, height: 10}); }); - suite('placeAbove', () => { - let target; + test('placeAbove for Element argument', () => { + element.placeAbove(target); + assert.equal(element.style.top, '25px'); + assert.equal(element.style.left, '72px'); + }); - setup(() => { - target = container.querySelector('.target'); - sandbox.stub(container, 'getBoundingClientRect').returns( - {top: 1, bottom: 2, left: 3, right: 4, width: 50, height: 6}); - sandbox.stub(element, '_getTargetBoundingRect').returns( - {top: 42, bottom: 20, left: 30, right: 40, width: 100, height: 60}); - sandbox.stub(element.$.tooltip, 'getBoundingClientRect').returns( - {width: 10, height: 10}); - }); + test('placeAbove for Text Node argument', () => { + element.placeAbove(target.firstChild); + assert.equal(element.style.top, '25px'); + assert.equal(element.style.left, '72px'); + }); - test('placeAbove for Element argument', () => { - element.placeAbove(target); - assert.equal(element.style.top, '25px'); - assert.equal(element.style.left, '72px'); - }); + test('placeBelow for Element argument', () => { + element.placeBelow(target); + assert.equal(element.style.top, '45px'); + assert.equal(element.style.left, '72px'); + }); - test('placeAbove for Text Node argument', () => { - element.placeAbove(target.firstChild); - assert.equal(element.style.top, '25px'); - assert.equal(element.style.left, '72px'); - }); + test('placeBelow for Text Node argument', () => { + element.placeBelow(target.firstChild); + assert.equal(element.style.top, '45px'); + assert.equal(element.style.left, '72px'); + }); - test('placeBelow for Element argument', () => { - element.placeBelow(target); - assert.equal(element.style.top, '45px'); - assert.equal(element.style.left, '72px'); - }); - - test('placeBelow for Text Node argument', () => { - element.placeBelow(target.firstChild); - assert.equal(element.style.top, '45px'); - assert.equal(element.style.left, '72px'); - }); - - test('uses document.createRange', () => { - sandbox.spy(document, 'createRange'); - element._getTargetBoundingRect.restore(); - sandbox.spy(element, '_getTargetBoundingRect'); - element.placeAbove(target.firstChild); - assert.isTrue(document.createRange.called); - }); + test('uses document.createRange', () => { + sandbox.spy(document, 'createRange'); + element._getTargetBoundingRect.restore(); + sandbox.spy(element, '_getTargetBoundingRect'); + element.placeAbove(target.firstChild); + assert.isTrue(document.createRange.called); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js index b6e2884..33e894b 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -14,534 +14,543 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const LANGUAGE_MAP = { - 'application/dart': 'dart', - 'application/json': 'json', - 'application/x-powershell': 'powershell', - 'application/typescript': 'typescript', - 'application/xml': 'xml', - 'application/xquery': 'xquery', - 'application/x-erb': 'erb', - 'text/css': 'css', - 'text/html': 'html', - 'text/javascript': 'js', - 'text/jsx': 'jsx', - 'text/x-c': 'cpp', - 'text/x-c++src': 'cpp', - 'text/x-clojure': 'clojure', - 'text/x-cmake': 'cmake', - 'text/x-coffeescript': 'coffeescript', - 'text/x-common-lisp': 'lisp', - 'text/x-crystal': 'crystal', - 'text/x-csharp': 'csharp', - 'text/x-csrc': 'cpp', - 'text/x-d': 'd', - 'text/x-diff': 'diff', - 'text/x-django': 'django', - 'text/x-dockerfile': 'dockerfile', - 'text/x-ebnf': 'ebnf', - 'text/x-elm': 'elm', - 'text/x-erlang': 'erlang', - 'text/x-fortran': 'fortran', - 'text/x-fsharp': 'fsharp', - 'text/x-go': 'go', - 'text/x-groovy': 'groovy', - 'text/x-haml': 'haml', - 'text/x-handlebars': 'handlebars', - 'text/x-haskell': 'haskell', - 'text/x-haxe': 'haxe', - 'text/x-ini': 'ini', - 'text/x-java': 'java', - 'text/x-julia': 'julia', - 'text/x-kotlin': 'kotlin', - 'text/x-latex': 'latex', - 'text/x-less': 'less', - 'text/x-lua': 'lua', - 'text/x-mathematica': 'mathematica', - 'text/x-nginx-conf': 'nginx', - 'text/x-nsis': 'nsis', - 'text/x-objectivec': 'objectivec', - 'text/x-ocaml': 'ocaml', - 'text/x-perl': 'perl', - 'text/x-pgsql': 'pgsql', // postgresql - 'text/x-php': 'php', - 'text/x-properties': 'properties', - 'text/x-protobuf': 'protobuf', - 'text/x-puppet': 'puppet', - 'text/x-python': 'python', - 'text/x-q': 'q', - 'text/x-ruby': 'ruby', - 'text/x-rustsrc': 'rust', - 'text/x-scala': 'scala', - 'text/x-scss': 'scss', - 'text/x-scheme': 'scheme', - 'text/x-shell': 'shell', - 'text/x-soy': 'soy', - 'text/x-spreadsheet': 'excel', - 'text/x-sh': 'bash', - 'text/x-sql': 'sql', - 'text/x-swift': 'swift', - 'text/x-systemverilog': 'sv', - 'text/x-tcl': 'tcl', - 'text/x-torque': 'torque', - 'text/x-twig': 'twig', - 'text/x-vb': 'vb', - 'text/x-verilog': 'v', - 'text/x-vhdl': 'vhdl', - 'text/x-yaml': 'yaml', - 'text/vbscript': 'vbscript', - }; - const ASYNC_DELAY = 10; +import '../../shared/gr-lib-loader/gr-lib-loader.js'; +import '../../../scripts/util.js'; +import '../gr-diff/gr-diff-line.js'; +import '../gr-diff-highlight/gr-annotation.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-syntax-layer_html.js'; - const CLASS_WHITELIST = { - 'gr-diff gr-syntax gr-syntax-attr': true, - 'gr-diff gr-syntax gr-syntax-attribute': true, - 'gr-diff gr-syntax gr-syntax-built_in': true, - 'gr-diff gr-syntax gr-syntax-comment': true, - 'gr-diff gr-syntax gr-syntax-doctag': true, - 'gr-diff gr-syntax gr-syntax-function': true, - 'gr-diff gr-syntax gr-syntax-keyword': true, - 'gr-diff gr-syntax gr-syntax-link': true, - 'gr-diff gr-syntax gr-syntax-literal': true, - 'gr-diff gr-syntax gr-syntax-meta': true, - 'gr-diff gr-syntax gr-syntax-meta-keyword': true, - 'gr-diff gr-syntax gr-syntax-name': true, - 'gr-diff gr-syntax gr-syntax-number': true, - 'gr-diff gr-syntax gr-syntax-params': true, - 'gr-diff gr-syntax gr-syntax-regexp': true, - 'gr-diff gr-syntax gr-syntax-selector-attr': true, - 'gr-diff gr-syntax gr-syntax-selector-class': true, - 'gr-diff gr-syntax gr-syntax-selector-id': true, - 'gr-diff gr-syntax gr-syntax-selector-pseudo': true, - 'gr-diff gr-syntax gr-syntax-selector-tag': true, - 'gr-diff gr-syntax gr-syntax-string': true, - 'gr-diff gr-syntax gr-syntax-tag': true, - 'gr-diff gr-syntax gr-syntax-template-tag': true, - 'gr-diff gr-syntax gr-syntax-template-variable': true, - 'gr-diff gr-syntax gr-syntax-title': true, - 'gr-diff gr-syntax gr-syntax-type': true, - 'gr-diff gr-syntax gr-syntax-variable': true, - }; +const LANGUAGE_MAP = { + 'application/dart': 'dart', + 'application/json': 'json', + 'application/x-powershell': 'powershell', + 'application/typescript': 'typescript', + 'application/xml': 'xml', + 'application/xquery': 'xquery', + 'application/x-erb': 'erb', + 'text/css': 'css', + 'text/html': 'html', + 'text/javascript': 'js', + 'text/jsx': 'jsx', + 'text/x-c': 'cpp', + 'text/x-c++src': 'cpp', + 'text/x-clojure': 'clojure', + 'text/x-cmake': 'cmake', + 'text/x-coffeescript': 'coffeescript', + 'text/x-common-lisp': 'lisp', + 'text/x-crystal': 'crystal', + 'text/x-csharp': 'csharp', + 'text/x-csrc': 'cpp', + 'text/x-d': 'd', + 'text/x-diff': 'diff', + 'text/x-django': 'django', + 'text/x-dockerfile': 'dockerfile', + 'text/x-ebnf': 'ebnf', + 'text/x-elm': 'elm', + 'text/x-erlang': 'erlang', + 'text/x-fortran': 'fortran', + 'text/x-fsharp': 'fsharp', + 'text/x-go': 'go', + 'text/x-groovy': 'groovy', + 'text/x-haml': 'haml', + 'text/x-handlebars': 'handlebars', + 'text/x-haskell': 'haskell', + 'text/x-haxe': 'haxe', + 'text/x-ini': 'ini', + 'text/x-java': 'java', + 'text/x-julia': 'julia', + 'text/x-kotlin': 'kotlin', + 'text/x-latex': 'latex', + 'text/x-less': 'less', + 'text/x-lua': 'lua', + 'text/x-mathematica': 'mathematica', + 'text/x-nginx-conf': 'nginx', + 'text/x-nsis': 'nsis', + 'text/x-objectivec': 'objectivec', + 'text/x-ocaml': 'ocaml', + 'text/x-perl': 'perl', + 'text/x-pgsql': 'pgsql', // postgresql + 'text/x-php': 'php', + 'text/x-properties': 'properties', + 'text/x-protobuf': 'protobuf', + 'text/x-puppet': 'puppet', + 'text/x-python': 'python', + 'text/x-q': 'q', + 'text/x-ruby': 'ruby', + 'text/x-rustsrc': 'rust', + 'text/x-scala': 'scala', + 'text/x-scss': 'scss', + 'text/x-scheme': 'scheme', + 'text/x-shell': 'shell', + 'text/x-soy': 'soy', + 'text/x-spreadsheet': 'excel', + 'text/x-sh': 'bash', + 'text/x-sql': 'sql', + 'text/x-swift': 'swift', + 'text/x-systemverilog': 'sv', + 'text/x-tcl': 'tcl', + 'text/x-torque': 'torque', + 'text/x-twig': 'twig', + 'text/x-vb': 'vb', + 'text/x-verilog': 'v', + 'text/x-vhdl': 'vhdl', + 'text/x-yaml': 'yaml', + 'text/vbscript': 'vbscript', +}; +const ASYNC_DELAY = 10; - const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; - const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g; - const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; - const GO_BACKSLASH_LITERAL = '\'\\\\\''; - const GLOBAL_LT_PATTERN = /</g; +const CLASS_WHITELIST = { + 'gr-diff gr-syntax gr-syntax-attr': true, + 'gr-diff gr-syntax gr-syntax-attribute': true, + 'gr-diff gr-syntax gr-syntax-built_in': true, + 'gr-diff gr-syntax gr-syntax-comment': true, + 'gr-diff gr-syntax gr-syntax-doctag': true, + 'gr-diff gr-syntax gr-syntax-function': true, + 'gr-diff gr-syntax gr-syntax-keyword': true, + 'gr-diff gr-syntax gr-syntax-link': true, + 'gr-diff gr-syntax gr-syntax-literal': true, + 'gr-diff gr-syntax gr-syntax-meta': true, + 'gr-diff gr-syntax gr-syntax-meta-keyword': true, + 'gr-diff gr-syntax gr-syntax-name': true, + 'gr-diff gr-syntax gr-syntax-number': true, + 'gr-diff gr-syntax gr-syntax-params': true, + 'gr-diff gr-syntax gr-syntax-regexp': true, + 'gr-diff gr-syntax gr-syntax-selector-attr': true, + 'gr-diff gr-syntax gr-syntax-selector-class': true, + 'gr-diff gr-syntax gr-syntax-selector-id': true, + 'gr-diff gr-syntax gr-syntax-selector-pseudo': true, + 'gr-diff gr-syntax gr-syntax-selector-tag': true, + 'gr-diff gr-syntax gr-syntax-string': true, + 'gr-diff gr-syntax gr-syntax-tag': true, + 'gr-diff gr-syntax gr-syntax-template-tag': true, + 'gr-diff gr-syntax gr-syntax-template-variable': true, + 'gr-diff gr-syntax gr-syntax-title': true, + 'gr-diff gr-syntax gr-syntax-type': true, + 'gr-diff gr-syntax gr-syntax-variable': true, +}; - /** @extends Polymer.Element */ - class GrSyntaxLayer extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-syntax-layer'; } +const CPP_DIRECTIVE_WITH_LT_PATTERN = /^\s*#(if|define).*</; +const CPP_WCHAR_PATTERN = /L\'(\\)?.\'/g; +const JAVA_PARAM_ANNOT_PATTERN = /(@[^\s]+)\(([^)]+)\)/g; +const GO_BACKSLASH_LITERAL = '\'\\\\\''; +const GLOBAL_LT_PATTERN = /</g; - static get properties() { - return { - diff: { - type: Object, - observer: '_diffChanged', - }, - enabled: { - type: Boolean, - value: true, - }, - _baseRanges: { - type: Array, - value() { return []; }, - }, - _revisionRanges: { - type: Array, - value() { return []; }, - }, - _baseLanguage: String, - _revisionLanguage: String, - _listeners: { - type: Array, - value() { return []; }, - }, - /** @type {?number} */ - _processHandle: Number, - /** - * The promise last returned from `process()` while the asynchronous - * processing is running - `null` otherwise. Provides a `cancel()` - * method that rejects it with `{isCancelled: true}`. - * - * @type {?Object} - */ - _processPromise: { - type: Object, - value: null, - }, - _hljs: Object, - }; +/** @extends Polymer.Element */ +class GrSyntaxLayer extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-syntax-layer'; } + + static get properties() { + return { + diff: { + type: Object, + observer: '_diffChanged', + }, + enabled: { + type: Boolean, + value: true, + }, + _baseRanges: { + type: Array, + value() { return []; }, + }, + _revisionRanges: { + type: Array, + value() { return []; }, + }, + _baseLanguage: String, + _revisionLanguage: String, + _listeners: { + type: Array, + value() { return []; }, + }, + /** @type {?number} */ + _processHandle: Number, + /** + * The promise last returned from `process()` while the asynchronous + * processing is running - `null` otherwise. Provides a `cancel()` + * method that rejects it with `{isCancelled: true}`. + * + * @type {?Object} + */ + _processPromise: { + type: Object, + value: null, + }, + _hljs: Object, + }; + } + + addListener(fn) { + this.push('_listeners', fn); + } + + removeListener(fn) { + this._listeners = this._listeners.filter(f => f != fn); + } + + /** + * Annotation layer method to add syntax annotations to the given element + * for the given line. + * + * @param {!HTMLElement} el + * @param {!HTMLElement} lineNumberEl + * @param {!Object} line (GrDiffLine) + */ + annotate(el, lineNumberEl, line) { + if (!this.enabled) { return; } + + // Determine the side. + let side; + if (line.type === GrDiffLine.Type.REMOVE || ( + line.type === GrDiffLine.Type.BOTH && + el.getAttribute('data-side') !== 'right')) { + side = 'left'; + } else if (line.type === GrDiffLine.Type.ADD || ( + el.getAttribute('data-side') !== 'left')) { + side = 'right'; } - addListener(fn) { - this.push('_listeners', fn); + // Find the relevant syntax ranges, if any. + let ranges = []; + if (side === 'left' && this._baseRanges.length >= line.beforeNumber) { + ranges = this._baseRanges[line.beforeNumber - 1] || []; + } else if (side === 'right' && + this._revisionRanges.length >= line.afterNumber) { + ranges = this._revisionRanges[line.afterNumber - 1] || []; } - removeListener(fn) { - this._listeners = this._listeners.filter(f => f != fn); + // Apply the ranges to the element. + for (const range of ranges) { + GrAnnotation.annotateElement( + el, range.start, range.length, range.className); + } + } + + _getLanguage(diffFileMetaInfo) { + // The Gerrit API provides only content-type, but for other users of + // gr-diff it may be more convenient to specify the language directly. + return diffFileMetaInfo.language || + LANGUAGE_MAP[diffFileMetaInfo.content_type]; + } + + /** + * Start processing syntax for the loaded diff and notify layer listeners + * as syntax info comes online. + * + * @return {Promise} + */ + process() { + // Cancel any still running process() calls, because they append to the + // same _baseRanges and _revisionRanges fields. + this._cancel(); + + // Discard existing ranges. + this._baseRanges = []; + this._revisionRanges = []; + + if (!this.enabled || !this.diff.content.length) { + return Promise.resolve(); } - /** - * Annotation layer method to add syntax annotations to the given element - * for the given line. - * - * @param {!HTMLElement} el - * @param {!HTMLElement} lineNumberEl - * @param {!Object} line (GrDiffLine) - */ - annotate(el, lineNumberEl, line) { - if (!this.enabled) { return; } - - // Determine the side. - let side; - if (line.type === GrDiffLine.Type.REMOVE || ( - line.type === GrDiffLine.Type.BOTH && - el.getAttribute('data-side') !== 'right')) { - side = 'left'; - } else if (line.type === GrDiffLine.Type.ADD || ( - el.getAttribute('data-side') !== 'left')) { - side = 'right'; - } - - // Find the relevant syntax ranges, if any. - let ranges = []; - if (side === 'left' && this._baseRanges.length >= line.beforeNumber) { - ranges = this._baseRanges[line.beforeNumber - 1] || []; - } else if (side === 'right' && - this._revisionRanges.length >= line.afterNumber) { - ranges = this._revisionRanges[line.afterNumber - 1] || []; - } - - // Apply the ranges to the element. - for (const range of ranges) { - GrAnnotation.annotateElement( - el, range.start, range.length, range.className); - } + if (this.diff.meta_a) { + this._baseLanguage = this._getLanguage(this.diff.meta_a); + } + if (this.diff.meta_b) { + this._revisionLanguage = this._getLanguage(this.diff.meta_b); + } + if (!this._baseLanguage && !this._revisionLanguage) { + return Promise.resolve(); } - _getLanguage(diffFileMetaInfo) { - // The Gerrit API provides only content-type, but for other users of - // gr-diff it may be more convenient to specify the language directly. - return diffFileMetaInfo.language || - LANGUAGE_MAP[diffFileMetaInfo.content_type]; + const state = { + sectionIndex: 0, + lineIndex: 0, + baseContext: undefined, + revisionContext: undefined, + lineNums: {left: 1, right: 1}, + lastNotify: {left: 1, right: 1}, + }; + + const rangesCache = new Map(); + + this._processPromise = util.makeCancelable(this._loadHLJS() + .then(() => new Promise(resolve => { + const nextStep = () => { + this._processHandle = null; + this._processNextLine(state, rangesCache); + + // Move to the next line in the section. + state.lineIndex++; + + // If the section has been exhausted, move to the next one. + if (this._isSectionDone(state)) { + state.lineIndex = 0; + state.sectionIndex++; + } + + // If all sections have been exhausted, finish. + if (state.sectionIndex >= this.diff.content.length) { + resolve(); + this._notify(state); + return; + } + + if (state.lineIndex % 100 === 0) { + this._notify(state); + this._processHandle = this.async(nextStep, ASYNC_DELAY); + } else { + nextStep.call(this); + } + }; + + this._processHandle = this.async(nextStep, 1); + }))); + return this._processPromise + .finally(() => { this._processPromise = null; }); + } + + /** + * Cancel any asynchronous syntax processing jobs. + */ + _cancel() { + if (this._processHandle != null) { + this.cancelAsync(this._processHandle); + this._processHandle = null; } - - /** - * Start processing syntax for the loaded diff and notify layer listeners - * as syntax info comes online. - * - * @return {Promise} - */ - process() { - // Cancel any still running process() calls, because they append to the - // same _baseRanges and _revisionRanges fields. - this._cancel(); - - // Discard existing ranges. - this._baseRanges = []; - this._revisionRanges = []; - - if (!this.enabled || !this.diff.content.length) { - return Promise.resolve(); - } - - if (this.diff.meta_a) { - this._baseLanguage = this._getLanguage(this.diff.meta_a); - } - if (this.diff.meta_b) { - this._revisionLanguage = this._getLanguage(this.diff.meta_b); - } - if (!this._baseLanguage && !this._revisionLanguage) { - return Promise.resolve(); - } - - const state = { - sectionIndex: 0, - lineIndex: 0, - baseContext: undefined, - revisionContext: undefined, - lineNums: {left: 1, right: 1}, - lastNotify: {left: 1, right: 1}, - }; - - const rangesCache = new Map(); - - this._processPromise = util.makeCancelable(this._loadHLJS() - .then(() => new Promise(resolve => { - const nextStep = () => { - this._processHandle = null; - this._processNextLine(state, rangesCache); - - // Move to the next line in the section. - state.lineIndex++; - - // If the section has been exhausted, move to the next one. - if (this._isSectionDone(state)) { - state.lineIndex = 0; - state.sectionIndex++; - } - - // If all sections have been exhausted, finish. - if (state.sectionIndex >= this.diff.content.length) { - resolve(); - this._notify(state); - return; - } - - if (state.lineIndex % 100 === 0) { - this._notify(state); - this._processHandle = this.async(nextStep, ASYNC_DELAY); - } else { - nextStep.call(this); - } - }; - - this._processHandle = this.async(nextStep, 1); - }))); - return this._processPromise - .finally(() => { this._processPromise = null; }); + if (this._processPromise) { + this._processPromise.cancel(); } + } - /** - * Cancel any asynchronous syntax processing jobs. - */ - _cancel() { - if (this._processHandle != null) { - this.cancelAsync(this._processHandle); - this._processHandle = null; - } - if (this._processPromise) { - this._processPromise.cancel(); - } - } + _diffChanged() { + this._cancel(); + this._baseRanges = []; + this._revisionRanges = []; + } - _diffChanged() { - this._cancel(); - this._baseRanges = []; - this._revisionRanges = []; - } + /** + * Take a string of HTML with the (potentially nested) syntax markers + * Highlight.js emits and emit a list of text ranges and classes for the + * markers. + * + * @param {string} str The string of HTML. + * @param {Map<string, !Array<!Object>>} rangesCache A map for caching + * ranges for each string. A cache is read and written by this method. + * Since diff is mostly comparing same file on two sides, there is good rate + * of duplication at least for parts that are on left and right parts. + * @return {!Array<!Object>} The list of ranges. + */ + _rangesFromString(str, rangesCache) { + const cached = rangesCache.get(str); + if (cached) return cached; - /** - * Take a string of HTML with the (potentially nested) syntax markers - * Highlight.js emits and emit a list of text ranges and classes for the - * markers. - * - * @param {string} str The string of HTML. - * @param {Map<string, !Array<!Object>>} rangesCache A map for caching - * ranges for each string. A cache is read and written by this method. - * Since diff is mostly comparing same file on two sides, there is good rate - * of duplication at least for parts that are on left and right parts. - * @return {!Array<!Object>} The list of ranges. - */ - _rangesFromString(str, rangesCache) { - const cached = rangesCache.get(str); - if (cached) return cached; + const div = document.createElement('div'); + div.innerHTML = str; + const ranges = this._rangesFromElement(div, 0); + rangesCache.set(str, ranges); + return ranges; + } - const div = document.createElement('div'); - div.innerHTML = str; - const ranges = this._rangesFromElement(div, 0); - rangesCache.set(str, ranges); - return ranges; - } - - _rangesFromElement(elem, offset) { - let result = []; - for (const node of elem.childNodes) { - const nodeLength = GrAnnotation.getLength(node); - // Note: HLJS may emit a span with class undefined when it thinks there - // may be a syntax error. - if (node.tagName === 'SPAN' && node.className !== 'undefined') { - if (CLASS_WHITELIST.hasOwnProperty(node.className)) { - result.push({ - start: offset, - length: nodeLength, - className: node.className, - }); - } - if (node.children.length) { - result = result.concat(this._rangesFromElement(node, offset)); - } + _rangesFromElement(elem, offset) { + let result = []; + for (const node of elem.childNodes) { + const nodeLength = GrAnnotation.getLength(node); + // Note: HLJS may emit a span with class undefined when it thinks there + // may be a syntax error. + if (node.tagName === 'SPAN' && node.className !== 'undefined') { + if (CLASS_WHITELIST.hasOwnProperty(node.className)) { + result.push({ + start: offset, + length: nodeLength, + className: node.className, + }); } - offset += nodeLength; + if (node.children.length) { + result = result.concat(this._rangesFromElement(node, offset)); + } } - return result; + offset += nodeLength; } + return result; + } - /** - * For a given state, process the syntax for the next line (or pair of - * lines). - * - * @param {!Object} state The processing state for the layer. - */ - _processNextLine(state, rangesCache) { - let baseLine; - let revisionLine; + /** + * For a given state, process the syntax for the next line (or pair of + * lines). + * + * @param {!Object} state The processing state for the layer. + */ + _processNextLine(state, rangesCache) { + let baseLine; + let revisionLine; - const section = this.diff.content[state.sectionIndex]; - if (section.ab) { - baseLine = section.ab[state.lineIndex]; - revisionLine = section.ab[state.lineIndex]; + const section = this.diff.content[state.sectionIndex]; + if (section.ab) { + baseLine = section.ab[state.lineIndex]; + revisionLine = section.ab[state.lineIndex]; + state.lineNums.left++; + state.lineNums.right++; + } else { + if (section.a && section.a.length > state.lineIndex) { + baseLine = section.a[state.lineIndex]; state.lineNums.left++; + } + if (section.b && section.b.length > state.lineIndex) { + revisionLine = section.b[state.lineIndex]; state.lineNums.right++; - } else { - if (section.a && section.a.length > state.lineIndex) { - baseLine = section.a[state.lineIndex]; - state.lineNums.left++; - } - if (section.b && section.b.length > state.lineIndex) { - revisionLine = section.b[state.lineIndex]; - state.lineNums.right++; - } - } - - // To store the result of the syntax highlighter. - let result; - - if (this._baseLanguage && baseLine !== undefined && - this._hljs.getLanguage(this._baseLanguage)) { - baseLine = this._workaround(this._baseLanguage, baseLine); - result = this._hljs.highlight(this._baseLanguage, baseLine, true, - state.baseContext); - this.push('_baseRanges', - this._rangesFromString(result.value, rangesCache)); - state.baseContext = result.top; - } - - if (this._revisionLanguage && revisionLine !== undefined && - this._hljs.getLanguage(this._revisionLanguage)) { - revisionLine = this._workaround(this._revisionLanguage, revisionLine); - result = this._hljs.highlight(this._revisionLanguage, revisionLine, - true, state.revisionContext); - this.push('_revisionRanges', - this._rangesFromString(result.value, rangesCache)); - state.revisionContext = result.top; } } - /** - * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained - * cases before sending them into HLJS so that they parse correctly. - * - * Important notes: - * * These tests should be as constrained as possible to avoid interfering - * with code it shouldn't AND to avoid executing regexes as much as - * possible. - * * These tests should document the issue clearly enough that the test can - * be condidently removed when the issue is solved in HLJS. - * * These tests should rewrite the line of code to have the same number of - * characters. This method rewrites the string that gets parsed, but NOT - * the string that gets displayed and highlighted. Thus, the positions - * must be consistent. - * - * @param {!string} language The name of the HLJS language plugin in use. - * @param {!string} line The line of code to potentially rewrite. - * @return {string} A potentially-rewritten line of code. - */ - _workaround(language, line) { - if (language === 'cpp') { - /** - * Prevent confusing < and << operators for the start of a meta string - * by converting them to a different operator. - * {@see Issue 4864} - * {@see https://github.com/isagalaev/highlight.js/issues/1341} - */ - if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) { - line = line.replace(GLOBAL_LT_PATTERN, '|'); - } + // To store the result of the syntax highlighter. + let result; - /** - * Rewrite CPP wchar_t characters literals to wchar_t string literals - * because HLJS only understands the string form. - * {@see Issue 5242} - * {#see https://github.com/isagalaev/highlight.js/issues/1412} - */ - if (CPP_WCHAR_PATTERN.test(line)) { - line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."'); - } + if (this._baseLanguage && baseLine !== undefined && + this._hljs.getLanguage(this._baseLanguage)) { + baseLine = this._workaround(this._baseLanguage, baseLine); + result = this._hljs.highlight(this._baseLanguage, baseLine, true, + state.baseContext); + this.push('_baseRanges', + this._rangesFromString(result.value, rangesCache)); + state.baseContext = result.top; + } - return line; + if (this._revisionLanguage && revisionLine !== undefined && + this._hljs.getLanguage(this._revisionLanguage)) { + revisionLine = this._workaround(this._revisionLanguage, revisionLine); + result = this._hljs.highlight(this._revisionLanguage, revisionLine, + true, state.revisionContext); + this.push('_revisionRanges', + this._rangesFromString(result.value, rangesCache)); + state.revisionContext = result.top; + } + } + + /** + * Ad hoc fixes for HLJS parsing bugs. Rewrite lines of code in constrained + * cases before sending them into HLJS so that they parse correctly. + * + * Important notes: + * * These tests should be as constrained as possible to avoid interfering + * with code it shouldn't AND to avoid executing regexes as much as + * possible. + * * These tests should document the issue clearly enough that the test can + * be condidently removed when the issue is solved in HLJS. + * * These tests should rewrite the line of code to have the same number of + * characters. This method rewrites the string that gets parsed, but NOT + * the string that gets displayed and highlighted. Thus, the positions + * must be consistent. + * + * @param {!string} language The name of the HLJS language plugin in use. + * @param {!string} line The line of code to potentially rewrite. + * @return {string} A potentially-rewritten line of code. + */ + _workaround(language, line) { + if (language === 'cpp') { + /** + * Prevent confusing < and << operators for the start of a meta string + * by converting them to a different operator. + * {@see Issue 4864} + * {@see https://github.com/isagalaev/highlight.js/issues/1341} + */ + if (CPP_DIRECTIVE_WITH_LT_PATTERN.test(line)) { + line = line.replace(GLOBAL_LT_PATTERN, '|'); } /** - * Prevent confusing the closing paren of a parameterized Java annotation - * being applied to a formal argument as the closing paren of the argument - * list. Rewrite the parens as spaces. - * {@see Issue 4776} - * {@see https://github.com/isagalaev/highlight.js/issues/1324} + * Rewrite CPP wchar_t characters literals to wchar_t string literals + * because HLJS only understands the string form. + * {@see Issue 5242} + * {#see https://github.com/isagalaev/highlight.js/issues/1412} */ - if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) { - return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 '); - } - - /** - * HLJS misunderstands backslash character literals in Go. - * {@see Issue 5007} - * {#see https://github.com/isagalaev/highlight.js/issues/1411} - */ - if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) { - return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"'); + if (CPP_WCHAR_PATTERN.test(line)) { + line = line.replace(CPP_WCHAR_PATTERN, 'L"$1."'); } return line; } /** - * Tells whether the state has exhausted its current section. - * - * @param {!Object} state - * @return {boolean} + * Prevent confusing the closing paren of a parameterized Java annotation + * being applied to a formal argument as the closing paren of the argument + * list. Rewrite the parens as spaces. + * {@see Issue 4776} + * {@see https://github.com/isagalaev/highlight.js/issues/1324} */ - _isSectionDone(state) { - const section = this.diff.content[state.sectionIndex]; - if (section.ab) { - return state.lineIndex >= section.ab.length; - } else { - return (!section.a || state.lineIndex >= section.a.length) && - (!section.b || state.lineIndex >= section.b.length); - } + if (language === 'java' && JAVA_PARAM_ANNOT_PATTERN.test(line)) { + return line.replace(JAVA_PARAM_ANNOT_PATTERN, '$1 $2 '); } /** - * For a given state, notify layer listeners of any processed line ranges - * that have not yet been notified. - * - * @param {!Object} state + * HLJS misunderstands backslash character literals in Go. + * {@see Issue 5007} + * {#see https://github.com/isagalaev/highlight.js/issues/1411} */ - _notify(state) { - if (state.lineNums.left - state.lastNotify.left) { - this._notifyRange( - state.lastNotify.left, - state.lineNums.left, - 'left'); - state.lastNotify.left = state.lineNums.left; - } - if (state.lineNums.right - state.lastNotify.right) { - this._notifyRange( - state.lastNotify.right, - state.lineNums.right, - 'right'); - state.lastNotify.right = state.lineNums.right; - } + if (language === 'go' && line.includes(GO_BACKSLASH_LITERAL)) { + return line.replace(GO_BACKSLASH_LITERAL, '"\\\\"'); } - _notifyRange(start, end, side) { - for (const fn of this._listeners) { - fn(start, end, side); - } - } + return line; + } - _loadHLJS() { - return this.$.libLoader.getHLJS().then(hljs => { - this._hljs = hljs; - }); + /** + * Tells whether the state has exhausted its current section. + * + * @param {!Object} state + * @return {boolean} + */ + _isSectionDone(state) { + const section = this.diff.content[state.sectionIndex]; + if (section.ab) { + return state.lineIndex >= section.ab.length; + } else { + return (!section.a || state.lineIndex >= section.a.length) && + (!section.b || state.lineIndex >= section.b.length); } } - customElements.define(GrSyntaxLayer.is, GrSyntaxLayer); -})(); + /** + * For a given state, notify layer listeners of any processed line ranges + * that have not yet been notified. + * + * @param {!Object} state + */ + _notify(state) { + if (state.lineNums.left - state.lastNotify.left) { + this._notifyRange( + state.lastNotify.left, + state.lineNums.left, + 'left'); + state.lastNotify.left = state.lineNums.left; + } + if (state.lineNums.right - state.lastNotify.right) { + this._notifyRange( + state.lastNotify.right, + state.lineNums.right, + 'right'); + state.lastNotify.right = state.lineNums.right; + } + } + + _notifyRange(start, end, side) { + for (const fn of this._listeners) { + fn(start, end, side); + } + } + + _loadHLJS() { + return this.$.libLoader.getHLJS().then(hljs => { + this._hljs = hljs; + }); + } +} + +customElements.define(GrSyntaxLayer.is, GrSyntaxLayer);
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js index f9e1279..183cbd1c 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_html.js
@@ -1,28 +1,21 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html"> -<script src="../../../scripts/util.js"></script> -<script src="../gr-diff/gr-diff-line.js"></script> -<script src="../gr-diff-highlight/gr-annotation.js"></script> - -<dom-module id="gr-syntax-layer"> - <template> +export const htmlTemplate = html` <gr-lib-loader id="libLoader"></gr-lib-loader> - </template> - <script src="gr-syntax-layer.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html index 62a3f1e..aa49f71 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-syntax-layer</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html"> -<link rel="import" href="gr-syntax-layer.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../shared/gr-rest-api-interface/mock-diff-response_test.js"></script> +<script type="module" src="./gr-syntax-layer.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-syntax-layer.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,469 +42,472 @@ </template> </test-fixture> -<script> - suite('gr-syntax-layer tests', async () => { - await readyToTest(); - let sandbox; - let diff; - let element; - const lineNumberEl = document.createElement('td'); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-rest-api-interface/mock-diff-response_test.js'; +import './gr-syntax-layer.js'; +suite('gr-syntax-layer tests', () => { + let sandbox; + let diff; + let element; + const lineNumberEl = document.createElement('td'); - function getMockHLJS() { - const html = '<span class="gr-diff gr-syntax gr-syntax-string">' + - 'ipsum</span>'; - return { - configure() {}, - highlight(lang, line, ignore, state) { - return { - value: line.replace(/ipsum/, html), - top: state === undefined ? 1 : state + 1, - }; - }, - // Return something truthy because this method is used to check if the - // language is supported. - getLanguage(s) { - return {}; - }, - }; - } + function getMockHLJS() { + const html = '<span class="gr-diff gr-syntax gr-syntax-string">' + + 'ipsum</span>'; + return { + configure() {}, + highlight(lang, line, ignore, state) { + return { + value: line.replace(/ipsum/, html), + top: state === undefined ? 1 : state + 1, + }; + }, + // Return something truthy because this method is used to check if the + // language is supported. + getLanguage(s) { + return {}; + }, + }; + } - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - const mock = document.createElement('mock-diff-response'); - diff = mock.diffResponse; - element.diff = diff; - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + const mock = document.createElement('mock-diff-response'); + diff = mock.diffResponse; + element.diff = diff; + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('annotate without range does nothing', () => { - const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - const el = document.createElement('div'); - el.textContent = 'Etiam dui, blandit wisi.'; - const line = new GrDiffLine(GrDiffLine.Type.REMOVE); - line.beforeNumber = 12; + test('annotate without range does nothing', () => { + const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const el = document.createElement('div'); + el.textContent = 'Etiam dui, blandit wisi.'; + const line = new GrDiffLine(GrDiffLine.Type.REMOVE); + line.beforeNumber = 12; - element.annotate(el, lineNumberEl, line); + element.annotate(el, lineNumberEl, line); - assert.isFalse(annotationSpy.called); - }); + assert.isFalse(annotationSpy.called); + }); - test('annotate with range applies it', () => { - const str = 'Etiam dui, blandit wisi.'; - const start = 6; - const length = 3; - const className = 'foobar'; + test('annotate with range applies it', () => { + const str = 'Etiam dui, blandit wisi.'; + const start = 6; + const length = 3; + const className = 'foobar'; - const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - const el = document.createElement('div'); - el.textContent = str; - const line = new GrDiffLine(GrDiffLine.Type.REMOVE); - line.beforeNumber = 12; - element._baseRanges[11] = [{ - start, - length, - className, - }]; + const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const el = document.createElement('div'); + el.textContent = str; + const line = new GrDiffLine(GrDiffLine.Type.REMOVE); + line.beforeNumber = 12; + element._baseRanges[11] = [{ + start, + length, + className, + }]; - element.annotate(el, lineNumberEl, line); + element.annotate(el, lineNumberEl, line); - assert.isTrue(annotationSpy.called); - assert.equal(annotationSpy.lastCall.args[0], el); - assert.equal(annotationSpy.lastCall.args[1], start); - assert.equal(annotationSpy.lastCall.args[2], length); - assert.equal(annotationSpy.lastCall.args[3], className); - assert.isOk(el.querySelector('hl.' + className)); - }); + assert.isTrue(annotationSpy.called); + assert.equal(annotationSpy.lastCall.args[0], el); + assert.equal(annotationSpy.lastCall.args[1], start); + assert.equal(annotationSpy.lastCall.args[2], length); + assert.equal(annotationSpy.lastCall.args[3], className); + assert.isOk(el.querySelector('hl.' + className)); + }); - test('annotate with range but disabled does nothing', () => { - const str = 'Etiam dui, blandit wisi.'; - const start = 6; - const length = 3; - const className = 'foobar'; + test('annotate with range but disabled does nothing', () => { + const str = 'Etiam dui, blandit wisi.'; + const start = 6; + const length = 3; + const className = 'foobar'; - const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - const el = document.createElement('div'); - el.textContent = str; - const line = new GrDiffLine(GrDiffLine.Type.REMOVE); - line.beforeNumber = 12; - element._baseRanges[11] = [{ - start, - length, - className, - }]; - element.enabled = false; + const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const el = document.createElement('div'); + el.textContent = str; + const line = new GrDiffLine(GrDiffLine.Type.REMOVE); + line.beforeNumber = 12; + element._baseRanges[11] = [{ + start, + length, + className, + }]; + element.enabled = false; - element.annotate(el, lineNumberEl, line); + element.annotate(el, lineNumberEl, line); - assert.isFalse(annotationSpy.called); - }); + assert.isFalse(annotationSpy.called); + }); - test('process on empty diff does nothing', done => { - element.diff = { - meta_a: {content_type: 'application/json'}, - meta_b: {content_type: 'application/json'}, - content: [], - }; - const processNextSpy = sandbox.spy(element, '_processNextLine'); + test('process on empty diff does nothing', done => { + element.diff = { + meta_a: {content_type: 'application/json'}, + meta_b: {content_type: 'application/json'}, + content: [], + }; + const processNextSpy = sandbox.spy(element, '_processNextLine'); - const processPromise = element.process(); + const processPromise = element.process(); - processPromise.then(() => { - assert.isFalse(processNextSpy.called); - assert.equal(element._baseRanges.length, 0); - assert.equal(element._revisionRanges.length, 0); - done(); - }); - }); - - test('process for unsupported languages does nothing', done => { - element.diff = { - meta_a: {content_type: 'text/x+objective-cobol-plus-plus'}, - meta_b: {content_type: 'application/not-a-real-language'}, - content: [], - }; - const processNextSpy = sandbox.spy(element, '_processNextLine'); - - const processPromise = element.process(); - - processPromise.then(() => { - assert.isFalse(processNextSpy.called); - assert.equal(element._baseRanges.length, 0); - assert.equal(element._revisionRanges.length, 0); - done(); - }); - }); - - test('process while disabled does nothing', done => { - const processNextSpy = sandbox.spy(element, '_processNextLine'); - element.enabled = false; - const loadHLJSSpy = sandbox.spy(element, '_loadHLJS'); - - const processPromise = element.process(); - - processPromise.then(() => { - assert.isFalse(processNextSpy.called); - assert.equal(element._baseRanges.length, 0); - assert.equal(element._revisionRanges.length, 0); - assert.isFalse(loadHLJSSpy.called); - done(); - }); - }); - - test('process highlight ipsum', done => { - element.diff.meta_a.content_type = 'application/json'; - element.diff.meta_b.content_type = 'application/json'; - - const mockHLJS = getMockHLJS(); - const highlightSpy = sinon.spy(mockHLJS, 'highlight'); - sandbox.stub(element.$.libLoader, 'getHLJS', - () => Promise.resolve(mockHLJS)); - const processNextSpy = sandbox.spy(element, '_processNextLine'); - const processPromise = element.process(); - - processPromise.then(() => { - const linesA = diff.meta_a.lines; - const linesB = diff.meta_b.lines; - - assert.isTrue(processNextSpy.called); - assert.equal(element._baseRanges.length, linesA); - assert.equal(element._revisionRanges.length, linesB); - - assert.equal(highlightSpy.callCount, linesA + linesB); - - // The first line of both sides have a range. - let ranges = [element._baseRanges[0], element._revisionRanges[0]]; - for (const range of ranges) { - assert.equal(range.length, 1); - assert.equal(range[0].className, - 'gr-diff gr-syntax gr-syntax-string'); - assert.equal(range[0].start, 'lorem '.length); - assert.equal(range[0].length, 'ipsum'.length); - } - - // There are no ranges from ll.1-12 on the left and ll.1-11 on the - // right. - ranges = element._baseRanges.slice(1, 12) - .concat(element._revisionRanges.slice(1, 11)); - - for (const range of ranges) { - assert.equal(range.length, 0); - } - - // There should be another pair of ranges on l.13 for the left and - // l.12 for the right. - ranges = [element._baseRanges[13], element._revisionRanges[12]]; - - for (const range of ranges) { - assert.equal(range.length, 1); - assert.equal(range[0].className, - 'gr-diff gr-syntax gr-syntax-string'); - assert.equal(range[0].start, 32); - assert.equal(range[0].length, 'ipsum'.length); - } - - // The next group should have a similar instance on either side. - - let range = element._baseRanges[15]; - assert.equal(range.length, 1); - assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string'); - assert.equal(range[0].start, 34); - assert.equal(range[0].length, 'ipsum'.length); - - range = element._revisionRanges[14]; - assert.equal(range.length, 1); - assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string'); - assert.equal(range[0].start, 35); - assert.equal(range[0].length, 'ipsum'.length); - - done(); - }); - }); - - test('_diffChanged calls cancel', () => { - const cancelSpy = sandbox.spy(element, '_diffChanged'); - element.diff = {content: []}; - assert.isTrue(cancelSpy.called); - }); - - test('_rangesFromElement no ranges', () => { - const elem = document.createElement('span'); - elem.textContent = 'Etiam dui, blandit wisi.'; - const offset = 100; - - const result = element._rangesFromElement(elem, offset); - - assert.equal(result.length, 0); - }); - - test('_rangesFromElement single range', () => { - const str0 = 'Etiam '; - const str1 = 'dui, blandit'; - const str2 = ' wisi.'; - const className = 'gr-diff gr-syntax gr-syntax-string'; - const offset = 100; - - const elem = document.createElement('span'); - elem.appendChild(document.createTextNode(str0)); - const span = document.createElement('span'); - span.textContent = str1; - span.className = className; - elem.appendChild(span); - elem.appendChild(document.createTextNode(str2)); - - const result = element._rangesFromElement(elem, offset); - - assert.equal(result.length, 1); - assert.equal(result[0].start, str0.length + offset); - assert.equal(result[0].length, str1.length); - assert.equal(result[0].className, className); - }); - - test('_rangesFromElement non-whitelist', () => { - const str0 = 'Etiam '; - const str1 = 'dui, blandit'; - const str2 = ' wisi.'; - const className = 'not-in-the-whitelist'; - const offset = 100; - - const elem = document.createElement('span'); - elem.appendChild(document.createTextNode(str0)); - const span = document.createElement('span'); - span.textContent = str1; - span.className = className; - elem.appendChild(span); - elem.appendChild(document.createTextNode(str2)); - - const result = element._rangesFromElement(elem, offset); - - assert.equal(result.length, 0); - }); - - test('_rangesFromElement milti range', () => { - const str0 = 'Etiam '; - const str1 = 'dui,'; - const str2 = ' blandit'; - const str3 = ' wisi.'; - const className = 'gr-diff gr-syntax gr-syntax-string'; - const offset = 100; - - const elem = document.createElement('span'); - elem.appendChild(document.createTextNode(str0)); - let span = document.createElement('span'); - span.textContent = str1; - span.className = className; - elem.appendChild(span); - elem.appendChild(document.createTextNode(str2)); - span = document.createElement('span'); - span.textContent = str3; - span.className = className; - elem.appendChild(span); - - const result = element._rangesFromElement(elem, offset); - - assert.equal(result.length, 2); - - assert.equal(result[0].start, str0.length + offset); - assert.equal(result[0].length, str1.length); - assert.equal(result[0].className, className); - - assert.equal(result[1].start, - str0.length + str1.length + str2.length + offset); - assert.equal(result[1].length, str3.length); - assert.equal(result[1].className, className); - }); - - test('_rangesFromElement nested range', () => { - const str0 = 'Etiam '; - const str1 = 'dui,'; - const str2 = ' blandit'; - const str3 = ' wisi.'; - const className = 'gr-diff gr-syntax gr-syntax-string'; - const offset = 100; - - const elem = document.createElement('span'); - elem.appendChild(document.createTextNode(str0)); - const span1 = document.createElement('span'); - span1.textContent = str1; - span1.className = className; - elem.appendChild(span1); - const span2 = document.createElement('span'); - span2.textContent = str2; - span2.className = className; - span1.appendChild(span2); - elem.appendChild(document.createTextNode(str3)); - - const result = element._rangesFromElement(elem, offset); - - assert.equal(result.length, 2); - - assert.equal(result[0].start, str0.length + offset); - assert.equal(result[0].length, str1.length + str2.length); - assert.equal(result[0].className, className); - - assert.equal(result[1].start, str0.length + str1.length + offset); - assert.equal(result[1].length, str2.length); - assert.equal(result[1].className, className); - }); - - test('_rangesFromString whitelist allows recursion', () => { - const str = [ - '<span class="non-whtelisted-class">', - '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>', - '</span>'].join(''); - const result = element._rangesFromString(str, new Map()); - assert.notEqual(result.length, 0); - }); - - test('_rangesFromString cache same syntax markers', () => { - sandbox.spy(element, '_rangesFromElement'); - const str = - '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>'; - const cacheMap = new Map(); - element._rangesFromString(str, cacheMap); - element._rangesFromString(str, cacheMap); - assert.isTrue(element._rangesFromElement.calledOnce); - }); - - test('_isSectionDone', () => { - let state = {sectionIndex: 0, lineIndex: 0}; - assert.isFalse(element._isSectionDone(state)); - - state = {sectionIndex: 0, lineIndex: 2}; - assert.isFalse(element._isSectionDone(state)); - - state = {sectionIndex: 0, lineIndex: 4}; - assert.isTrue(element._isSectionDone(state)); - - state = {sectionIndex: 1, lineIndex: 2}; - assert.isFalse(element._isSectionDone(state)); - - state = {sectionIndex: 1, lineIndex: 3}; - assert.isTrue(element._isSectionDone(state)); - - state = {sectionIndex: 3, lineIndex: 0}; - assert.isFalse(element._isSectionDone(state)); - - state = {sectionIndex: 3, lineIndex: 3}; - assert.isFalse(element._isSectionDone(state)); - - state = {sectionIndex: 3, lineIndex: 4}; - assert.isTrue(element._isSectionDone(state)); - }); - - test('workaround CPP LT directive', () => { - // Does nothing to regular line. - let line = 'int main(int argc, char** argv) { return 0; }'; - assert.equal(element._workaround('cpp', line), line); - - // Does nothing to include directive. - line = '#include <stdio>'; - assert.equal(element._workaround('cpp', line), line); - - // Converts left-shift operator in #define. - line = '#define GiB (1ull << 30)'; - let expected = '#define GiB (1ull || 30)'; - assert.equal(element._workaround('cpp', line), expected); - - // Converts less-than operator in #if. - line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)'; - expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)'; - assert.equal(element._workaround('cpp', line), expected); - }); - - test('workaround Java param-annotation', () => { - // Does nothing to regular line. - let line = 'public static void foo(int bar) { }'; - assert.equal(element._workaround('java', line), line); - - // Does nothing to regular annotation. - line = 'public static void foo(@Nullable int bar) { }'; - assert.equal(element._workaround('java', line), line); - - // Converts parameterized annotation. - line = 'public static void foo(@SuppressWarnings("unused") int bar) { }'; - const expected = 'public static void foo(@SuppressWarnings "unused" ' + - ' int bar) { }'; - assert.equal(element._workaround('java', line), expected); - }); - - test('workaround CPP whcar_t character literals', () => { - // Does nothing to regular line. - let line = 'int main(int argc, char** argv) { return 0; }'; - assert.equal(element._workaround('cpp', line), line); - - // Does nothing to wchar_t string. - line = 'wchar_t* sz = L"abc 123";'; - assert.equal(element._workaround('cpp', line), line); - - // Converts wchar_t character literal to string. - line = 'wchar_t myChar = L\'#\''; - let expected = 'wchar_t myChar = L"."'; - assert.equal(element._workaround('cpp', line), expected); - - // Converts wchar_t character literal with escape sequence to string. - line = 'wchar_t myChar = L\'\\"\''; - expected = 'wchar_t myChar = L"\\."'; - assert.equal(element._workaround('cpp', line), expected); - }); - - test('workaround go backslash character literals', () => { - // Does nothing to regular line. - let line = 'func foo(in []byte) (lit []byte, n int, err error) {'; - assert.equal(element._workaround('go', line), line); - - // Does nothing to string with backslash literal - line = 'c := "\\\\"'; - assert.equal(element._workaround('go', line), line); - - // Converts backslash literal character to a string. - line = 'c := \'\\\\\''; - const expected = 'c := "\\\\"'; - assert.equal(element._workaround('go', line), expected); + processPromise.then(() => { + assert.isFalse(processNextSpy.called); + assert.equal(element._baseRanges.length, 0); + assert.equal(element._revisionRanges.length, 0); + done(); }); }); + + test('process for unsupported languages does nothing', done => { + element.diff = { + meta_a: {content_type: 'text/x+objective-cobol-plus-plus'}, + meta_b: {content_type: 'application/not-a-real-language'}, + content: [], + }; + const processNextSpy = sandbox.spy(element, '_processNextLine'); + + const processPromise = element.process(); + + processPromise.then(() => { + assert.isFalse(processNextSpy.called); + assert.equal(element._baseRanges.length, 0); + assert.equal(element._revisionRanges.length, 0); + done(); + }); + }); + + test('process while disabled does nothing', done => { + const processNextSpy = sandbox.spy(element, '_processNextLine'); + element.enabled = false; + const loadHLJSSpy = sandbox.spy(element, '_loadHLJS'); + + const processPromise = element.process(); + + processPromise.then(() => { + assert.isFalse(processNextSpy.called); + assert.equal(element._baseRanges.length, 0); + assert.equal(element._revisionRanges.length, 0); + assert.isFalse(loadHLJSSpy.called); + done(); + }); + }); + + test('process highlight ipsum', done => { + element.diff.meta_a.content_type = 'application/json'; + element.diff.meta_b.content_type = 'application/json'; + + const mockHLJS = getMockHLJS(); + const highlightSpy = sinon.spy(mockHLJS, 'highlight'); + sandbox.stub(element.$.libLoader, 'getHLJS', + () => Promise.resolve(mockHLJS)); + const processNextSpy = sandbox.spy(element, '_processNextLine'); + const processPromise = element.process(); + + processPromise.then(() => { + const linesA = diff.meta_a.lines; + const linesB = diff.meta_b.lines; + + assert.isTrue(processNextSpy.called); + assert.equal(element._baseRanges.length, linesA); + assert.equal(element._revisionRanges.length, linesB); + + assert.equal(highlightSpy.callCount, linesA + linesB); + + // The first line of both sides have a range. + let ranges = [element._baseRanges[0], element._revisionRanges[0]]; + for (const range of ranges) { + assert.equal(range.length, 1); + assert.equal(range[0].className, + 'gr-diff gr-syntax gr-syntax-string'); + assert.equal(range[0].start, 'lorem '.length); + assert.equal(range[0].length, 'ipsum'.length); + } + + // There are no ranges from ll.1-12 on the left and ll.1-11 on the + // right. + ranges = element._baseRanges.slice(1, 12) + .concat(element._revisionRanges.slice(1, 11)); + + for (const range of ranges) { + assert.equal(range.length, 0); + } + + // There should be another pair of ranges on l.13 for the left and + // l.12 for the right. + ranges = [element._baseRanges[13], element._revisionRanges[12]]; + + for (const range of ranges) { + assert.equal(range.length, 1); + assert.equal(range[0].className, + 'gr-diff gr-syntax gr-syntax-string'); + assert.equal(range[0].start, 32); + assert.equal(range[0].length, 'ipsum'.length); + } + + // The next group should have a similar instance on either side. + + let range = element._baseRanges[15]; + assert.equal(range.length, 1); + assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string'); + assert.equal(range[0].start, 34); + assert.equal(range[0].length, 'ipsum'.length); + + range = element._revisionRanges[14]; + assert.equal(range.length, 1); + assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string'); + assert.equal(range[0].start, 35); + assert.equal(range[0].length, 'ipsum'.length); + + done(); + }); + }); + + test('_diffChanged calls cancel', () => { + const cancelSpy = sandbox.spy(element, '_diffChanged'); + element.diff = {content: []}; + assert.isTrue(cancelSpy.called); + }); + + test('_rangesFromElement no ranges', () => { + const elem = document.createElement('span'); + elem.textContent = 'Etiam dui, blandit wisi.'; + const offset = 100; + + const result = element._rangesFromElement(elem, offset); + + assert.equal(result.length, 0); + }); + + test('_rangesFromElement single range', () => { + const str0 = 'Etiam '; + const str1 = 'dui, blandit'; + const str2 = ' wisi.'; + const className = 'gr-diff gr-syntax gr-syntax-string'; + const offset = 100; + + const elem = document.createElement('span'); + elem.appendChild(document.createTextNode(str0)); + const span = document.createElement('span'); + span.textContent = str1; + span.className = className; + elem.appendChild(span); + elem.appendChild(document.createTextNode(str2)); + + const result = element._rangesFromElement(elem, offset); + + assert.equal(result.length, 1); + assert.equal(result[0].start, str0.length + offset); + assert.equal(result[0].length, str1.length); + assert.equal(result[0].className, className); + }); + + test('_rangesFromElement non-whitelist', () => { + const str0 = 'Etiam '; + const str1 = 'dui, blandit'; + const str2 = ' wisi.'; + const className = 'not-in-the-whitelist'; + const offset = 100; + + const elem = document.createElement('span'); + elem.appendChild(document.createTextNode(str0)); + const span = document.createElement('span'); + span.textContent = str1; + span.className = className; + elem.appendChild(span); + elem.appendChild(document.createTextNode(str2)); + + const result = element._rangesFromElement(elem, offset); + + assert.equal(result.length, 0); + }); + + test('_rangesFromElement milti range', () => { + const str0 = 'Etiam '; + const str1 = 'dui,'; + const str2 = ' blandit'; + const str3 = ' wisi.'; + const className = 'gr-diff gr-syntax gr-syntax-string'; + const offset = 100; + + const elem = document.createElement('span'); + elem.appendChild(document.createTextNode(str0)); + let span = document.createElement('span'); + span.textContent = str1; + span.className = className; + elem.appendChild(span); + elem.appendChild(document.createTextNode(str2)); + span = document.createElement('span'); + span.textContent = str3; + span.className = className; + elem.appendChild(span); + + const result = element._rangesFromElement(elem, offset); + + assert.equal(result.length, 2); + + assert.equal(result[0].start, str0.length + offset); + assert.equal(result[0].length, str1.length); + assert.equal(result[0].className, className); + + assert.equal(result[1].start, + str0.length + str1.length + str2.length + offset); + assert.equal(result[1].length, str3.length); + assert.equal(result[1].className, className); + }); + + test('_rangesFromElement nested range', () => { + const str0 = 'Etiam '; + const str1 = 'dui,'; + const str2 = ' blandit'; + const str3 = ' wisi.'; + const className = 'gr-diff gr-syntax gr-syntax-string'; + const offset = 100; + + const elem = document.createElement('span'); + elem.appendChild(document.createTextNode(str0)); + const span1 = document.createElement('span'); + span1.textContent = str1; + span1.className = className; + elem.appendChild(span1); + const span2 = document.createElement('span'); + span2.textContent = str2; + span2.className = className; + span1.appendChild(span2); + elem.appendChild(document.createTextNode(str3)); + + const result = element._rangesFromElement(elem, offset); + + assert.equal(result.length, 2); + + assert.equal(result[0].start, str0.length + offset); + assert.equal(result[0].length, str1.length + str2.length); + assert.equal(result[0].className, className); + + assert.equal(result[1].start, str0.length + str1.length + offset); + assert.equal(result[1].length, str2.length); + assert.equal(result[1].className, className); + }); + + test('_rangesFromString whitelist allows recursion', () => { + const str = [ + '<span class="non-whtelisted-class">', + '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>', + '</span>'].join(''); + const result = element._rangesFromString(str, new Map()); + assert.notEqual(result.length, 0); + }); + + test('_rangesFromString cache same syntax markers', () => { + sandbox.spy(element, '_rangesFromElement'); + const str = + '<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>'; + const cacheMap = new Map(); + element._rangesFromString(str, cacheMap); + element._rangesFromString(str, cacheMap); + assert.isTrue(element._rangesFromElement.calledOnce); + }); + + test('_isSectionDone', () => { + let state = {sectionIndex: 0, lineIndex: 0}; + assert.isFalse(element._isSectionDone(state)); + + state = {sectionIndex: 0, lineIndex: 2}; + assert.isFalse(element._isSectionDone(state)); + + state = {sectionIndex: 0, lineIndex: 4}; + assert.isTrue(element._isSectionDone(state)); + + state = {sectionIndex: 1, lineIndex: 2}; + assert.isFalse(element._isSectionDone(state)); + + state = {sectionIndex: 1, lineIndex: 3}; + assert.isTrue(element._isSectionDone(state)); + + state = {sectionIndex: 3, lineIndex: 0}; + assert.isFalse(element._isSectionDone(state)); + + state = {sectionIndex: 3, lineIndex: 3}; + assert.isFalse(element._isSectionDone(state)); + + state = {sectionIndex: 3, lineIndex: 4}; + assert.isTrue(element._isSectionDone(state)); + }); + + test('workaround CPP LT directive', () => { + // Does nothing to regular line. + let line = 'int main(int argc, char** argv) { return 0; }'; + assert.equal(element._workaround('cpp', line), line); + + // Does nothing to include directive. + line = '#include <stdio>'; + assert.equal(element._workaround('cpp', line), line); + + // Converts left-shift operator in #define. + line = '#define GiB (1ull << 30)'; + let expected = '#define GiB (1ull || 30)'; + assert.equal(element._workaround('cpp', line), expected); + + // Converts less-than operator in #if. + line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)'; + expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)'; + assert.equal(element._workaround('cpp', line), expected); + }); + + test('workaround Java param-annotation', () => { + // Does nothing to regular line. + let line = 'public static void foo(int bar) { }'; + assert.equal(element._workaround('java', line), line); + + // Does nothing to regular annotation. + line = 'public static void foo(@Nullable int bar) { }'; + assert.equal(element._workaround('java', line), line); + + // Converts parameterized annotation. + line = 'public static void foo(@SuppressWarnings("unused") int bar) { }'; + const expected = 'public static void foo(@SuppressWarnings "unused" ' + + ' int bar) { }'; + assert.equal(element._workaround('java', line), expected); + }); + + test('workaround CPP whcar_t character literals', () => { + // Does nothing to regular line. + let line = 'int main(int argc, char** argv) { return 0; }'; + assert.equal(element._workaround('cpp', line), line); + + // Does nothing to wchar_t string. + line = 'wchar_t* sz = L"abc 123";'; + assert.equal(element._workaround('cpp', line), line); + + // Converts wchar_t character literal to string. + line = 'wchar_t myChar = L\'#\''; + let expected = 'wchar_t myChar = L"."'; + assert.equal(element._workaround('cpp', line), expected); + + // Converts wchar_t character literal with escape sequence to string. + line = 'wchar_t myChar = L\'\\"\''; + expected = 'wchar_t myChar = L"\\."'; + assert.equal(element._workaround('cpp', line), expected); + }); + + test('workaround go backslash character literals', () => { + // Does nothing to regular line. + let line = 'func foo(in []byte) (lit []byte, n int, err error) {'; + assert.equal(element._workaround('go', line), line); + + // Does nothing to string with backslash literal + line = 'c := "\\\\"'; + assert.equal(element._workaround('go', line), line); + + // Converts backslash literal character to a string. + line = 'c := \'\\\\\''; + const expected = 'c := "\\\\"'; + assert.equal(element._workaround('go', line), expected); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js index e5ae06d..76a01de 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-syntax-theme.js
@@ -1,20 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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. + */ +const $_documentContainer = document.createElement('template'); -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. ---> -<dom-module id="gr-syntax-theme"> +$_documentContainer.innerHTML = `<dom-module id="gr-syntax-theme"> <template> <style> /** @@ -107,4 +109,13 @@ } </style> </template> -</dom-module> +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js index 022a985..0a57883 100644 --- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js +++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search.js
@@ -14,78 +14,90 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.ListViewMixin - * @extends Polymer.Element - */ - class GrDocumentationSearch extends Polymer.mixinBehaviors( [ - Gerrit.ListViewBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-documentation-search'; } +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.js'; +import '../../../styles/gr-table-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-list-view/gr-list-view.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-documentation-search_html.js'; - static get properties() { - return { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, +/** + * @appliesMixin Gerrit.ListViewMixin + * @extends Polymer.Element + */ +class GrDocumentationSearch extends mixinBehaviors( [ + Gerrit.ListViewBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _path: { - type: String, - readOnly: true, - value: '/Documentation', - }, - _documentationSearches: Array, + static get is() { return 'gr-documentation-search'; } - _loading: { - type: Boolean, - value: true, - }, - _filter: { - type: String, - value: '', - }, - }; - } + static get properties() { + return { + /** + * URL params passed from the router. + */ + params: { + type: Object, + observer: '_paramsChanged', + }, - /** @override */ - attached() { - super.attached(); - this.dispatchEvent( - new CustomEvent('title-change', {title: 'Documentation Search'})); - } + _path: { + type: String, + readOnly: true, + value: '/Documentation', + }, + _documentationSearches: Array, - _paramsChanged(params) { - this._loading = true; - this._filter = this.getFilterValue(params); - - return this._getDocumentationSearches(this._filter); - } - - _getDocumentationSearches(filter) { - this._documentationSearches = []; - return this.$.restAPI.getDocumentationSearches(filter) - .then(searches => { - // Late response. - if (filter !== this._filter || !searches) { return; } - this._documentationSearches = searches; - this._loading = false; - }); - } - - _computeSearchUrl(url) { - if (!url) { return ''; } - return this.getBaseUrl() + '/' + url; - } + _loading: { + type: Boolean, + value: true, + }, + _filter: { + type: String, + value: '', + }, + }; } - customElements.define(GrDocumentationSearch.is, GrDocumentationSearch); -})(); + /** @override */ + attached() { + super.attached(); + this.dispatchEvent( + new CustomEvent('title-change', {title: 'Documentation Search'})); + } + + _paramsChanged(params) { + this._loading = true; + this._filter = this.getFilterValue(params); + + return this._getDocumentationSearches(this._filter); + } + + _getDocumentationSearches(filter) { + this._documentationSearches = []; + return this.$.restAPI.getDocumentationSearches(filter) + .then(searches => { + // Late response. + if (filter !== this._filter || !searches) { return; } + this._documentationSearches = searches; + this._loading = false; + }); + } + + _computeSearchUrl(url) { + if (!url) { return ''; } + return this.getBaseUrl() + '/' + url; + } +} + +customElements.define(GrDocumentationSearch.is, GrDocumentationSearch);
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js index 5ae679e..ced351a 100644 --- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js +++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_html.js
@@ -1,56 +1,43 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html"> -<link rel="import" href="../../../styles/gr-table-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-list-view/gr-list-view.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-documentation-search"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <style include="gr-table-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <gr-list-view - filter="[[_filter]]" - items=false - offset=0 - loading="[[_loading]]" - path="[[_path]]"> + <gr-list-view filter="[[_filter]]" items="false" offset="0" loading="[[_loading]]" path="[[_path]]"> <table id="list" class="genericList"> - <tr class="headerRow"> + <tbody><tr class="headerRow"> <th class="name topHeader">Name</th> <th class="name topHeader"></th> <th class="name topHeader"></th> </tr> - <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]"> + <tr id="loading" class\$="loadingMsg [[computeLoadingClass(_loading)]]"> <td>Loading...</td> </tr> - <tbody class$="[[computeLoadingClass(_loading)]]"> + </tbody><tbody class\$="[[computeLoadingClass(_loading)]]"> <template is="dom-repeat" items="[[_documentationSearches]]"> <tr class="table"> <td class="name"> - <a href$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a> + <a href\$="[[_computeSearchUrl(item.url)]]">[[item.title]]</a> </td> <td></td> <td></td> @@ -60,6 +47,4 @@ </table> </gr-list-view> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-documentation-search.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html index e9bf78d..9c3a08d 100644 --- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html +++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-documentation-search</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-documentation-search.html"> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-documentation-search.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-documentation-search.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,90 +41,92 @@ </template> </test-fixture> -<script> - let counter; - const documentationGenerator = () => { - return { - title: `Gerrit Code Review - REST API Developers Notes${++counter}`, - url: 'Documentation/dev-rest-api.html', - }; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-documentation-search.js'; +let counter; +const documentationGenerator = () => { + return { + title: `Gerrit Code Review - REST API Developers Notes${++counter}`, + url: 'Documentation/dev-rest-api.html', }; +}; - suite('gr-documentation-search tests', async () => { - await readyToTest(); - let element; - let documentationSearches; - let sandbox; - let value; +suite('gr-documentation-search tests', () => { + let element; + let documentationSearches; + let sandbox; + let value; - setup(() => { - sandbox = sinon.sandbox.create(); - sandbox.stub(page, 'show'); - element = fixture('basic'); - counter = 0; + setup(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(page, 'show'); + element = fixture('basic'); + counter = 0; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('list with searches for documentation', () => { + setup(done => { + documentationSearches = _.times(26, documentationGenerator); + stub('gr-rest-api-interface', { + getDocumentationSearches() { + return Promise.resolve(documentationSearches); + }, + }); + element._paramsChanged(value).then(() => { flush(done); }); }); - teardown(() => { - sandbox.restore(); - }); - - suite('list with searches for documentation', () => { - setup(done => { - documentationSearches = _.times(26, documentationGenerator); - stub('gr-rest-api-interface', { - getDocumentationSearches() { - return Promise.resolve(documentationSearches); - }, - }); - element._paramsChanged(value).then(() => { flush(done); }); - }); - - test('test for test repo in the list', done => { - flush(() => { - assert.equal(element._documentationSearches[0].title, - 'Gerrit Code Review - REST API Developers Notes1'); - assert.equal(element._documentationSearches[0].url, - 'Documentation/dev-rest-api.html'); - done(); - }); - }); - }); - - suite('filter', () => { - setup(() => { - documentationSearches = _.times(25, documentationGenerator); - _.times(1, documentationSearches); - }); - - test('_paramsChanged', done => { - sandbox.stub( - element.$.restAPI, - 'getDocumentationSearches', - () => Promise.resolve(documentationSearches)); - const value = { - filter: 'test', - }; - element._paramsChanged(value).then(() => { - assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall - .calledWithExactly('test')); - done(); - }); - }); - }); - - suite('loading', () => { - test('correct contents are displayed', () => { - assert.isTrue(element._loading); - assert.equal(element.computeLoadingClass(element._loading), 'loading'); - assert.equal(getComputedStyle(element.$.loading).display, 'block'); - - element._loading = false; - element._repos = _.times(25, documentationGenerator); - - flushAsynchronousOperations(); - assert.equal(element.computeLoadingClass(element._loading), ''); - assert.equal(getComputedStyle(element.$.loading).display, 'none'); + test('test for test repo in the list', done => { + flush(() => { + assert.equal(element._documentationSearches[0].title, + 'Gerrit Code Review - REST API Developers Notes1'); + assert.equal(element._documentationSearches[0].url, + 'Documentation/dev-rest-api.html'); + done(); }); }); }); + + suite('filter', () => { + setup(() => { + documentationSearches = _.times(25, documentationGenerator); + _.times(1, documentationSearches); + }); + + test('_paramsChanged', done => { + sandbox.stub( + element.$.restAPI, + 'getDocumentationSearches', + () => Promise.resolve(documentationSearches)); + const value = { + filter: 'test', + }; + element._paramsChanged(value).then(() => { + assert.isTrue(element.$.restAPI.getDocumentationSearches.lastCall + .calledWithExactly('test')); + done(); + }); + }); + }); + + suite('loading', () => { + test('correct contents are displayed', () => { + assert.isTrue(element._loading); + assert.equal(element.computeLoadingClass(element._loading), 'loading'); + assert.equal(getComputedStyle(element.$.loading).display, 'block'); + + element._loading = false; + element._repos = _.times(25, documentationGenerator); + + flushAsynchronousOperations(); + assert.equal(element.computeLoadingClass(element._loading), ''); + assert.equal(getComputedStyle(element.$.loading).display, 'none'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js index 73dbaf8..09f4abf 100644 --- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js +++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor.js
@@ -14,32 +14,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrDefaultEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-default-editor'; } - /** - * Fired when the content of the editor changes. - * - * @event content-change - */ +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-default-editor_html.js'; - static get properties() { - return { - fileContent: String, - }; - } +/** @extends Polymer.Element */ +class GrDefaultEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _handleTextareaInput(e) { - this.dispatchEvent(new CustomEvent( - 'content-change', - {detail: {value: e.target.value}, bubbles: true, composed: true})); - } + static get is() { return 'gr-default-editor'; } + /** + * Fired when the content of the editor changes. + * + * @event content-change + */ + + static get properties() { + return { + fileContent: String, + }; } - customElements.define(GrDefaultEditor.is, GrDefaultEditor); -})(); + _handleTextareaInput(e) { + this.dispatchEvent(new CustomEvent( + 'content-change', + {detail: {value: e.target.value}, bubbles: true, composed: true})); + } +} + +customElements.define(GrDefaultEditor.is, GrDefaultEditor);
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js index 19a4e63..e7fc6fd 100644 --- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js +++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-default-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> textarea { border: none; @@ -36,10 +33,5 @@ outline: none; } </style> - <textarea - id="textarea" - value="[[fileContent]]" - on-input="_handleTextareaInput"></textarea> - </template> - <script src="gr-default-editor.js"></script> -</dom-module> + <textarea id="textarea" value="[[fileContent]]" on-input="_handleTextareaInput"></textarea> +`;
diff --git a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html index 228c70e..043f656 100644 --- a/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html +++ b/polygerrit-ui/app/elements/edit/gr-default-editor/gr-default-editor_test.html
@@ -18,16 +18,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-default-editor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-default-editor.html"> +<script type="module" src="./gr-default-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-default-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,26 +40,28 @@ </template> </test-fixture> -<script> - suite('gr-default-editor tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-default-editor.js'; +suite('gr-default-editor tests', () => { + let element; - setup(() => { - element = fixture('basic'); - element.fileContent = ''; - }); - - test('fires content-change event', done => { - const contentChangedHandler = e => { - assert.equal(e.detail.value, 'test'); - done(); - }; - const textarea = element.$.textarea; - element.addEventListener('content-change', contentChangedHandler); - textarea.value = 'test'; - textarea.dispatchEvent(new CustomEvent('input', - {target: textarea, bubbles: true, composed: true})); - }); + setup(() => { + element = fixture('basic'); + element.fileContent = ''; }); + + test('fires content-change event', done => { + const contentChangedHandler = e => { + assert.equal(e.detail.value, 'test'); + done(); + }; + const textarea = element.$.textarea; + element.addEventListener('content-change', contentChangedHandler); + textarea.value = 'test'; + textarea.dispatchEvent(new CustomEvent('input', + {target: textarea, bubbles: true, composed: true})); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-constants.js b/polygerrit-ui/app/elements/edit/gr-edit-constants.js index 5895124..2a929f2 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-constants.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-constants.js
@@ -1,33 +1,31 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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. + */ +(function(window) { + 'use strict'; -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 + const GrEditConstants = window.GrEditConstants || {}; -http://www.apache.org/licenses/LICENSE-2.0 + // Order corresponds to order in the UI. + GrEditConstants.Actions = { + OPEN: {label: 'Add/Open', id: 'open'}, + DELETE: {label: 'Delete', id: 'delete'}, + RENAME: {label: 'Rename', id: 'rename'}, + RESTORE: {label: 'Restore', id: 'restore'}, + }; -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. ---> -<script> - (function(window) { - 'use strict'; - - const GrEditConstants = window.GrEditConstants || {}; - - // Order corresponds to order in the UI. - GrEditConstants.Actions = { - OPEN: {label: 'Add/Open', id: 'open'}, - DELETE: {label: 'Delete', id: 'delete'}, - RENAME: {label: 'Rename', id: 'rename'}, - RESTORE: {label: 'Restore', id: 'restore'}, - }; - - window.GrEditConstants = GrEditConstants; - })(window); -</script> + window.GrEditConstants = GrEditConstants; +})(window);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js index e655f7b..e17fe03 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls.js
@@ -14,231 +14,250 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.PatchSetMixin - * @extends Polymer.Element - */ - class GrEditControls extends Polymer.mixinBehaviors( [ - Gerrit.PatchSetBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-edit-controls'; } +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-dialog/gr-dialog.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-edit-constants.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-edit-controls_html.js'; - static get properties() { - return { - change: Object, - patchNum: String, +/** + * @appliesMixin Gerrit.PatchSetMixin + * @extends Polymer.Element + */ +class GrEditControls extends mixinBehaviors( [ + Gerrit.PatchSetBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** - * TODO(kaspern): by default, the RESTORE action should be hidden in the - * file-list as it is a per-file action only. Remove this default value - * when the Actions dictionary is moved to a shared constants file and - * use the hiddenActions property in the parent component. - */ - hiddenActions: { - type: Array, - value() { return [GrEditConstants.Actions.RESTORE.id]; }, + static get is() { return 'gr-edit-controls'; } + + static get properties() { + return { + change: Object, + patchNum: String, + + /** + * TODO(kaspern): by default, the RESTORE action should be hidden in the + * file-list as it is a per-file action only. Remove this default value + * when the Actions dictionary is moved to a shared constants file and + * use the hiddenActions property in the parent component. + */ + hiddenActions: { + type: Array, + value() { return [GrEditConstants.Actions.RESTORE.id]; }, + }, + + _actions: { + type: Array, + value() { return Object.values(GrEditConstants.Actions); }, + }, + _path: { + type: String, + value: '', + }, + _newPath: { + type: String, + value: '', + }, + _query: { + type: Function, + value() { + return this._queryFiles.bind(this); }, + }, + }; + } - _actions: { - type: Array, - value() { return Object.values(GrEditConstants.Actions); }, - }, - _path: { - type: String, - value: '', - }, - _newPath: { - type: String, - value: '', - }, - _query: { - type: Function, - value() { - return this._queryFiles.bind(this); - }, - }, - }; - } - - _handleTap(e) { - e.preventDefault(); - const action = Polymer.dom(e).localTarget.id; - switch (action) { - case GrEditConstants.Actions.OPEN.id: - this.openOpenDialog(); - return; - case GrEditConstants.Actions.DELETE.id: - this.openDeleteDialog(); - return; - case GrEditConstants.Actions.RENAME.id: - this.openRenameDialog(); - return; - case GrEditConstants.Actions.RESTORE.id: - this.openRestoreDialog(); - return; - } - } - - /** - * @param {string=} opt_path - */ - openOpenDialog(opt_path) { - if (opt_path) { this._path = opt_path; } - return this._showDialog(this.$.openDialog); - } - - /** - * @param {string=} opt_path - */ - openDeleteDialog(opt_path) { - if (opt_path) { this._path = opt_path; } - return this._showDialog(this.$.deleteDialog); - } - - /** - * @param {string=} opt_path - */ - openRenameDialog(opt_path) { - if (opt_path) { this._path = opt_path; } - return this._showDialog(this.$.renameDialog); - } - - /** - * @param {string=} opt_path - */ - openRestoreDialog(opt_path) { - if (opt_path) { this._path = opt_path; } - return this._showDialog(this.$.restoreDialog); - } - - /** - * Given a path string, checks that it is a valid file path. - * - * @param {string} path - * @return {boolean} - */ - _isValidPath(path) { - // Double negation needed for strict boolean return type. - return !!path.length && !path.endsWith('/'); - } - - _computeRenameDisabled(path, newPath) { - return this._isValidPath(path) && this._isValidPath(newPath); - } - - /** - * Given a dom event, gets the dialog that lies along this event path. - * - * @param {!Event} e - * @return {!Element|undefined} - */ - _getDialogFromEvent(e) { - return Polymer.dom(e).path.find(element => { - if (!element.classList) { return false; } - return element.classList.contains('dialog'); - }); - } - - _showDialog(dialog) { - // Some dialogs may not fire their on-close event when closed in certain - // ways (e.g. by clicking outside the dialog body). This call prevents - // multiple dialogs from being shown in the same overlay. - this._hideAllDialogs(); - - return this.$.overlay.open().then(() => { - dialog.classList.toggle('invisible', false); - const autocomplete = dialog.querySelector('gr-autocomplete'); - if (autocomplete) { autocomplete.focus(); } - this.async(() => { this.$.overlay.center(); }, 1); - }); - } - - _hideAllDialogs() { - const dialogs = Polymer.dom(this.root).querySelectorAll('.dialog'); - for (const dialog of dialogs) { this._closeDialog(dialog); } - } - - /** - * @param {Element|undefined} dialog - * @param {boolean=} clearInputs - */ - _closeDialog(dialog, clearInputs) { - if (!dialog) { return; } - - if (clearInputs) { - // Dialog may have autocompletes and plain inputs -- as these have - // different properties representing their bound text, it is easier to - // just make two separate queries. - dialog.querySelectorAll('gr-autocomplete') - .forEach(input => { input.text = ''; }); - - dialog.querySelectorAll('iron-input') - .forEach(input => { input.bindValue = ''; }); - } - - dialog.classList.toggle('invisible', true); - return this.$.overlay.close(); - } - - _handleDialogCancel(e) { - this._closeDialog(this._getDialogFromEvent(e)); - } - - _handleOpenConfirm(e) { - const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path, - this.patchNum); - Gerrit.Nav.navigateToRelativeUrl(url); - this._closeDialog(this._getDialogFromEvent(e), true); - } - - _handleDeleteConfirm(e) { - // Get the dialog before the api call as the event will change during bubbling - // which will make Polymer.dom(e).path an emtpy array in polymer 2 - const dialog = this._getDialogFromEvent(e); - this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path) - .then(res => { - if (!res.ok) { return; } - this._closeDialog(dialog, true); - Gerrit.Nav.navigateToChange(this.change); - }); - } - - _handleRestoreConfirm(e) { - const dialog = this._getDialogFromEvent(e); - this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path) - .then(res => { - if (!res.ok) { return; } - this._closeDialog(dialog, true); - Gerrit.Nav.navigateToChange(this.change); - }); - } - - _handleRenameConfirm(e) { - const dialog = this._getDialogFromEvent(e); - return this.$.restAPI.renameFileInChangeEdit(this.change._number, - this._path, this._newPath).then(res => { - if (!res.ok) { return; } - this._closeDialog(dialog, true); - Gerrit.Nav.navigateToChange(this.change); - }); - } - - _queryFiles(input) { - return this.$.restAPI.queryChangeFiles(this.change._number, - this.patchNum, input).then(res => res.map(file => { - return {name: file}; - })); - } - - _computeIsInvisible(id, hiddenActions) { - return hiddenActions.includes(id) ? 'invisible' : ''; + _handleTap(e) { + e.preventDefault(); + const action = dom(e).localTarget.id; + switch (action) { + case GrEditConstants.Actions.OPEN.id: + this.openOpenDialog(); + return; + case GrEditConstants.Actions.DELETE.id: + this.openDeleteDialog(); + return; + case GrEditConstants.Actions.RENAME.id: + this.openRenameDialog(); + return; + case GrEditConstants.Actions.RESTORE.id: + this.openRestoreDialog(); + return; } } - customElements.define(GrEditControls.is, GrEditControls); -})(); + /** + * @param {string=} opt_path + */ + openOpenDialog(opt_path) { + if (opt_path) { this._path = opt_path; } + return this._showDialog(this.$.openDialog); + } + + /** + * @param {string=} opt_path + */ + openDeleteDialog(opt_path) { + if (opt_path) { this._path = opt_path; } + return this._showDialog(this.$.deleteDialog); + } + + /** + * @param {string=} opt_path + */ + openRenameDialog(opt_path) { + if (opt_path) { this._path = opt_path; } + return this._showDialog(this.$.renameDialog); + } + + /** + * @param {string=} opt_path + */ + openRestoreDialog(opt_path) { + if (opt_path) { this._path = opt_path; } + return this._showDialog(this.$.restoreDialog); + } + + /** + * Given a path string, checks that it is a valid file path. + * + * @param {string} path + * @return {boolean} + */ + _isValidPath(path) { + // Double negation needed for strict boolean return type. + return !!path.length && !path.endsWith('/'); + } + + _computeRenameDisabled(path, newPath) { + return this._isValidPath(path) && this._isValidPath(newPath); + } + + /** + * Given a dom event, gets the dialog that lies along this event path. + * + * @param {!Event} e + * @return {!Element|undefined} + */ + _getDialogFromEvent(e) { + return dom(e).path.find(element => { + if (!element.classList) { return false; } + return element.classList.contains('dialog'); + }); + } + + _showDialog(dialog) { + // Some dialogs may not fire their on-close event when closed in certain + // ways (e.g. by clicking outside the dialog body). This call prevents + // multiple dialogs from being shown in the same overlay. + this._hideAllDialogs(); + + return this.$.overlay.open().then(() => { + dialog.classList.toggle('invisible', false); + const autocomplete = dialog.querySelector('gr-autocomplete'); + if (autocomplete) { autocomplete.focus(); } + this.async(() => { this.$.overlay.center(); }, 1); + }); + } + + _hideAllDialogs() { + const dialogs = dom(this.root).querySelectorAll('.dialog'); + for (const dialog of dialogs) { this._closeDialog(dialog); } + } + + /** + * @param {Element|undefined} dialog + * @param {boolean=} clearInputs + */ + _closeDialog(dialog, clearInputs) { + if (!dialog) { return; } + + if (clearInputs) { + // Dialog may have autocompletes and plain inputs -- as these have + // different properties representing their bound text, it is easier to + // just make two separate queries. + dialog.querySelectorAll('gr-autocomplete') + .forEach(input => { input.text = ''; }); + + dialog.querySelectorAll('iron-input') + .forEach(input => { input.bindValue = ''; }); + } + + dialog.classList.toggle('invisible', true); + return this.$.overlay.close(); + } + + _handleDialogCancel(e) { + this._closeDialog(this._getDialogFromEvent(e)); + } + + _handleOpenConfirm(e) { + const url = Gerrit.Nav.getEditUrlForDiff(this.change, this._path, + this.patchNum); + Gerrit.Nav.navigateToRelativeUrl(url); + this._closeDialog(this._getDialogFromEvent(e), true); + } + + _handleDeleteConfirm(e) { + // Get the dialog before the api call as the event will change during bubbling + // which will make Polymer.dom(e).path an emtpy array in polymer 2 + const dialog = this._getDialogFromEvent(e); + this.$.restAPI.deleteFileInChangeEdit(this.change._number, this._path) + .then(res => { + if (!res.ok) { return; } + this._closeDialog(dialog, true); + Gerrit.Nav.navigateToChange(this.change); + }); + } + + _handleRestoreConfirm(e) { + const dialog = this._getDialogFromEvent(e); + this.$.restAPI.restoreFileInChangeEdit(this.change._number, this._path) + .then(res => { + if (!res.ok) { return; } + this._closeDialog(dialog, true); + Gerrit.Nav.navigateToChange(this.change); + }); + } + + _handleRenameConfirm(e) { + const dialog = this._getDialogFromEvent(e); + return this.$.restAPI.renameFileInChangeEdit(this.change._number, + this._path, this._newPath).then(res => { + if (!res.ok) { return; } + this._closeDialog(dialog, true); + Gerrit.Nav.navigateToChange(this.change); + }); + } + + _queryFiles(input) { + return this.$.restAPI.queryChangeFiles(this.change._number, + this.patchNum, input).then(res => res.map(file => { + return {name: file}; + })); + } + + _computeIsInvisible(id, hiddenActions) { + return hiddenActions.includes(id) ? 'invisible' : ''; + } +} + +customElements.define(GrEditControls.is, GrEditControls);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js index cb950da..2d09069 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_html.js
@@ -1,38 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-edit-constants.html"> - -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-edit-controls"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { align-items: center; @@ -70,94 +54,40 @@ } </style> <template is="dom-repeat" items="[[_actions]]" as="action"> - <gr-button - id$="[[action.id]]" - class$="[[_computeIsInvisible(action.id, hiddenActions)]]" - link - on-click="_handleTap">[[action.label]]</gr-button> + <gr-button id\$="[[action.id]]" class\$="[[_computeIsInvisible(action.id, hiddenActions)]]" link="" on-click="_handleTap">[[action.label]]</gr-button> </template> - <gr-overlay id="overlay" with-backdrop> - <gr-dialog - id="openDialog" - class="invisible dialog" - disabled$="[[!_isValidPath(_path)]]" - confirm-label="Confirm" - confirm-on-enter - on-confirm="_handleOpenConfirm" - on-cancel="_handleDialogCancel"> + <gr-overlay id="overlay" with-backdrop=""> + <gr-dialog id="openDialog" class="invisible dialog" disabled\$="[[!_isValidPath(_path)]]" confirm-label="Confirm" confirm-on-enter="" on-confirm="_handleOpenConfirm" on-cancel="_handleDialogCancel"> <div class="header" slot="header"> Add a new file or open an existing file </div> <div class="main" slot="main"> - <gr-autocomplete - placeholder="Enter an existing or new full file path." - query="[[_query]]" - text="{{_path}}"></gr-autocomplete> + <gr-autocomplete placeholder="Enter an existing or new full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete> </div> </gr-dialog> - <gr-dialog - id="deleteDialog" - class="invisible dialog" - disabled$="[[!_isValidPath(_path)]]" - confirm-label="Delete" - confirm-on-enter - on-confirm="_handleDeleteConfirm" - on-cancel="_handleDialogCancel"> + <gr-dialog id="deleteDialog" class="invisible dialog" disabled\$="[[!_isValidPath(_path)]]" confirm-label="Delete" confirm-on-enter="" on-confirm="_handleDeleteConfirm" on-cancel="_handleDialogCancel"> <div class="header" slot="header">Delete a file from the repo</div> <div class="main" slot="main"> - <gr-autocomplete - placeholder="Enter an existing full file path." - query="[[_query]]" - text="{{_path}}"></gr-autocomplete> + <gr-autocomplete placeholder="Enter an existing full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete> </div> </gr-dialog> - <gr-dialog - id="renameDialog" - class="invisible dialog" - disabled$="[[!_computeRenameDisabled(_path, _newPath)]]" - confirm-label="Rename" - confirm-on-enter - on-confirm="_handleRenameConfirm" - on-cancel="_handleDialogCancel"> + <gr-dialog id="renameDialog" class="invisible dialog" disabled\$="[[!_computeRenameDisabled(_path, _newPath)]]" confirm-label="Rename" confirm-on-enter="" on-confirm="_handleRenameConfirm" on-cancel="_handleDialogCancel"> <div class="header" slot="header">Rename a file in the repo</div> <div class="main" slot="main"> - <gr-autocomplete - placeholder="Enter an existing full file path." - query="[[_query]]" - text="{{_path}}"></gr-autocomplete> - <iron-input - class="newPathIronInput" - bind-value="{{_newPath}}" - placeholder="Enter the new path."> - <input - class="newPathInput" - is="iron-input" - bind-value="{{_newPath}}" - placeholder="Enter the new path."> + <gr-autocomplete placeholder="Enter an existing full file path." query="[[_query]]" text="{{_path}}"></gr-autocomplete> + <iron-input class="newPathIronInput" bind-value="{{_newPath}}" placeholder="Enter the new path."> + <input class="newPathInput" is="iron-input" bind-value="{{_newPath}}" placeholder="Enter the new path."> </iron-input> </div> </gr-dialog> - <gr-dialog - id="restoreDialog" - class="invisible dialog" - confirm-label="Restore" - confirm-on-enter - on-confirm="_handleRestoreConfirm" - on-cancel="_handleDialogCancel"> + <gr-dialog id="restoreDialog" class="invisible dialog" confirm-label="Restore" confirm-on-enter="" on-confirm="_handleRestoreConfirm" on-cancel="_handleDialogCancel"> <div class="header" slot="header">Restore this file?</div> <div class="main" slot="main"> - <iron-input - disabled - bind-value="{{_path}}"> - <input - is="iron-input" - disabled - bind-value="{{_path}}"> + <iron-input disabled="" bind-value="{{_path}}"> + <input is="iron-input" disabled="" bind-value="{{_path}}"> </iron-input> </div> </gr-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-edit-controls.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html index 80de093..034a7a7 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html +++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.html
@@ -18,16 +18,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-edit-controls</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-edit-controls.html"> +<script type="module" src="./gr-edit-controls.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-edit-controls.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,351 +40,355 @@ </template> </test-fixture> -<script> - suite('gr-edit-controls tests', async () => { - await readyToTest(); - let element; - let sandbox; - let showDialogSpy; - let closeDialogSpy; - let queryStub; - +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-edit-controls.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +suite('gr-edit-controls tests', () => { + let element; + let sandbox; + let showDialogSpy; + let closeDialogSpy; + let queryStub; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.change = {_number: '42'}; + showDialogSpy = sandbox.spy(element, '_showDialog'); + closeDialogSpy = sandbox.spy(element, '_closeDialog'); + sandbox.stub(element, '_hideAllDialogs'); + queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles') + .returns(Promise.resolve([])); + flushAsynchronousOperations(); + }); + + teardown(() => { sandbox.restore(); }); + + test('all actions exist', () => { + assert.equal(dom(element.root).querySelectorAll('gr-button').length, + element._actions.length); + }); + + suite('edit button CUJ', () => { + let navStubs; + let openAutoCcmplete; + setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.change = {_number: '42'}; - showDialogSpy = sandbox.spy(element, '_showDialog'); - closeDialogSpy = sandbox.spy(element, '_closeDialog'); - sandbox.stub(element, '_hideAllDialogs'); - queryStub = sandbox.stub(element.$.restAPI, 'queryChangeFiles') - .returns(Promise.resolve([])); - flushAsynchronousOperations(); + navStubs = [ + sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'), + sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'), + ]; + openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete'); }); - - teardown(() => { sandbox.restore(); }); - - test('all actions exist', () => { - assert.equal(Polymer.dom(element.root).querySelectorAll('gr-button').length, - element._actions.length); + + test('_isValidPath', () => { + assert.isFalse(element._isValidPath('')); + assert.isFalse(element._isValidPath('test/')); + assert.isFalse(element._isValidPath('/')); + assert.isTrue(element._isValidPath('test/path.cpp')); + assert.isTrue(element._isValidPath('test.js')); }); - - suite('edit button CUJ', () => { - let navStubs; - let openAutoCcmplete; - - setup(() => { - navStubs = [ - sandbox.stub(Gerrit.Nav, 'getEditUrlForDiff'), - sandbox.stub(Gerrit.Nav, 'navigateToRelativeUrl'), - ]; - openAutoCcmplete = element.$.openDialog.querySelector('gr-autocomplete'); - }); - - test('_isValidPath', () => { - assert.isFalse(element._isValidPath('')); - assert.isFalse(element._isValidPath('test/')); - assert.isFalse(element._isValidPath('/')); - assert.isTrue(element._isValidPath('test/path.cpp')); - assert.isTrue(element._isValidPath('test.js')); - }); - - test('open', () => { - MockInteractions.tap(element.shadowRoot.querySelector('#open')); - element.patchNum = 1; - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element._hideAllDialogs.called); - assert.isTrue(element.$.openDialog.disabled); - assert.isFalse(queryStub.called); - openAutoCcmplete.noDebounce = true; - openAutoCcmplete.text = 'src/test.cpp'; - assert.isTrue(queryStub.called); - assert.isFalse(element.$.openDialog.disabled); - MockInteractions.tap(element.$.openDialog.shadowRoot - .querySelector('gr-button[primary]')); - for (const stub of navStubs) { assert.isTrue(stub.called); } - assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args, - [element.change, 'src/test.cpp', element.patchNum]); - assert.isTrue(closeDialogSpy.called); - }); - }); - - test('cancel', () => { - MockInteractions.tap(element.shadowRoot.querySelector('#open')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.openDialog.disabled); - openAutoCcmplete.noDebounce = true; - openAutoCcmplete.text = 'src/test.cpp'; - assert.isFalse(element.$.openDialog.disabled); - MockInteractions.tap(element.$.openDialog.shadowRoot - .querySelector('gr-button')); - for (const stub of navStubs) { assert.isFalse(stub.called); } - assert.isTrue(closeDialogSpy.called); - assert.equal(element._path, 'src/test.cpp'); - }); + + test('open', () => { + MockInteractions.tap(element.shadowRoot.querySelector('#open')); + element.patchNum = 1; + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element._hideAllDialogs.called); + assert.isTrue(element.$.openDialog.disabled); + assert.isFalse(queryStub.called); + openAutoCcmplete.noDebounce = true; + openAutoCcmplete.text = 'src/test.cpp'; + assert.isTrue(queryStub.called); + assert.isFalse(element.$.openDialog.disabled); + MockInteractions.tap(element.$.openDialog.shadowRoot + .querySelector('gr-button[primary]')); + for (const stub of navStubs) { assert.isTrue(stub.called); } + assert.deepEqual(Gerrit.Nav.getEditUrlForDiff.lastCall.args, + [element.change, 'src/test.cpp', element.patchNum]); + assert.isTrue(closeDialogSpy.called); }); }); - - suite('delete button CUJ', () => { - let navStub; - let deleteStub; - let deleteAutocomplete; - - setup(() => { - navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit'); - deleteAutocomplete = - element.$.deleteDialog.querySelector('gr-autocomplete'); + + test('cancel', () => { + MockInteractions.tap(element.shadowRoot.querySelector('#open')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.openDialog.disabled); + openAutoCcmplete.noDebounce = true; + openAutoCcmplete.text = 'src/test.cpp'; + assert.isFalse(element.$.openDialog.disabled); + MockInteractions.tap(element.$.openDialog.shadowRoot + .querySelector('gr-button')); + for (const stub of navStubs) { assert.isFalse(stub.called); } + assert.isTrue(closeDialogSpy.called); + assert.equal(element._path, 'src/test.cpp'); }); - - test('delete', () => { - deleteStub.returns(Promise.resolve({ok: true})); - MockInteractions.tap(element.shadowRoot.querySelector('#delete')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.deleteDialog.disabled); - assert.isFalse(queryStub.called); - deleteAutocomplete.noDebounce = true; - deleteAutocomplete.text = 'src/test.cpp'; - assert.isTrue(queryStub.called); - assert.isFalse(element.$.deleteDialog.disabled); - MockInteractions.tap(element.$.deleteDialog.shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.isTrue(deleteStub.called); - - return deleteStub.lastCall.returnValue.then(() => { - assert.equal(element._path, ''); - assert.isTrue(navStub.called); - assert.isTrue(closeDialogSpy.called); - }); - }); - }); - - test('delete fails', () => { - deleteStub.returns(Promise.resolve({ok: false})); - MockInteractions.tap(element.shadowRoot.querySelector('#delete')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.deleteDialog.disabled); - assert.isFalse(queryStub.called); - deleteAutocomplete.noDebounce = true; - deleteAutocomplete.text = 'src/test.cpp'; - assert.isTrue(queryStub.called); - assert.isFalse(element.$.deleteDialog.disabled); - MockInteractions.tap(element.$.deleteDialog.shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.isTrue(deleteStub.called); - - return deleteStub.lastCall.returnValue.then(() => { - assert.isFalse(navStub.called); - assert.isFalse(closeDialogSpy.called); - }); - }); - }); - - test('cancel', () => { - MockInteractions.tap(element.shadowRoot.querySelector('#delete')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.deleteDialog.disabled); - element.$.deleteDialog.querySelector('gr-autocomplete').text = - 'src/test.cpp'; - assert.isFalse(element.$.deleteDialog.disabled); - MockInteractions.tap(element.$.deleteDialog.shadowRoot - .querySelector('gr-button')); - assert.isFalse(navStub.called); - assert.isTrue(closeDialogSpy.called); - assert.equal(element._path, 'src/test.cpp'); - }); - }); - }); - - suite('rename button CUJ', () => { - let navStub; - let renameStub; - let renameAutocomplete; - const inputSelector = Polymer.Element ? - '.newPathIronInput' : - '.newPathInput'; - - setup(() => { - navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit'); - renameAutocomplete = - element.$.renameDialog.querySelector('gr-autocomplete'); - }); - - test('rename', () => { - renameStub.returns(Promise.resolve({ok: true})); - MockInteractions.tap(element.shadowRoot.querySelector('#rename')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.renameDialog.disabled); - assert.isFalse(queryStub.called); - renameAutocomplete.noDebounce = true; - renameAutocomplete.text = 'src/test.cpp'; - assert.isTrue(queryStub.called); - assert.isTrue(element.$.renameDialog.disabled); - - element.$.renameDialog.querySelector(inputSelector).bindValue = - 'src/test.newPath'; - - assert.isFalse(element.$.renameDialog.disabled); - MockInteractions.tap(element.$.renameDialog.shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.isTrue(renameStub.called); - - return renameStub.lastCall.returnValue.then(() => { - assert.equal(element._path, ''); - assert.isTrue(navStub.called); - assert.isTrue(closeDialogSpy.called); - }); - }); - }); - - test('rename fails', () => { - renameStub.returns(Promise.resolve({ok: false})); - MockInteractions.tap(element.shadowRoot.querySelector('#rename')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.renameDialog.disabled); - assert.isFalse(queryStub.called); - renameAutocomplete.noDebounce = true; - renameAutocomplete.text = 'src/test.cpp'; - assert.isTrue(queryStub.called); - assert.isTrue(element.$.renameDialog.disabled); - - element.$.renameDialog.querySelector(inputSelector).bindValue = - 'src/test.newPath'; - - assert.isFalse(element.$.renameDialog.disabled); - MockInteractions.tap(element.$.renameDialog.shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.isTrue(renameStub.called); - - return renameStub.lastCall.returnValue.then(() => { - assert.isFalse(navStub.called); - assert.isFalse(closeDialogSpy.called); - }); - }); - }); - - test('cancel', () => { - MockInteractions.tap(element.shadowRoot.querySelector('#rename')); - return showDialogSpy.lastCall.returnValue.then(() => { - assert.isTrue(element.$.renameDialog.disabled); - element.$.renameDialog.querySelector('gr-autocomplete').text = - 'src/test.cpp'; - element.$.renameDialog.querySelector(inputSelector).bindValue = - 'src/test.newPath'; - assert.isFalse(element.$.renameDialog.disabled); - MockInteractions.tap(element.$.renameDialog.shadowRoot - .querySelector('gr-button')); - assert.isFalse(navStub.called); - assert.isTrue(closeDialogSpy.called); - assert.equal(element._path, 'src/test.cpp'); - assert.equal(element._newPath, 'src/test.newPath'); - }); - }); - }); - - suite('restore button CUJ', () => { - let navStub; - let restoreStub; - - setup(() => { - navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit'); - }); - - test('restore hidden by default', () => { - assert.isTrue(element.shadowRoot - .querySelector('#restore').classList.contains('invisible')); - }); - - test('restore', () => { - restoreStub.returns(Promise.resolve({ok: true})); - element._path = 'src/test.cpp'; - MockInteractions.tap(element.shadowRoot.querySelector('#restore')); - return showDialogSpy.lastCall.returnValue.then(() => { - MockInteractions.tap(element.$.restoreDialog.shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.isTrue(restoreStub.called); - assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp'); - return restoreStub.lastCall.returnValue.then(() => { - assert.equal(element._path, ''); - assert.isTrue(navStub.called); - assert.isTrue(closeDialogSpy.called); - }); - }); - }); - - test('restore fails', () => { - restoreStub.returns(Promise.resolve({ok: false})); - element._path = 'src/test.cpp'; - MockInteractions.tap(element.shadowRoot.querySelector('#restore')); - return showDialogSpy.lastCall.returnValue.then(() => { - MockInteractions.tap(element.$.restoreDialog.shadowRoot - .querySelector('gr-button[primary]')); - flushAsynchronousOperations(); - - assert.isTrue(restoreStub.called); - assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp'); - return restoreStub.lastCall.returnValue.then(() => { - assert.isFalse(navStub.called); - assert.isFalse(closeDialogSpy.called); - }); - }); - }); - - test('cancel', () => { - element._path = 'src/test.cpp'; - MockInteractions.tap(element.shadowRoot.querySelector('#restore')); - return showDialogSpy.lastCall.returnValue.then(() => { - MockInteractions.tap(element.$.restoreDialog.shadowRoot - .querySelector('gr-button')); - assert.isFalse(navStub.called); - assert.isTrue(closeDialogSpy.called); - assert.equal(element._path, 'src/test.cpp'); - }); - }); - }); - - test('openOpenDialog', done => { - element.openOpenDialog('test/path.cpp') - .then(() => { - assert.isFalse(element.$.openDialog.hasAttribute('hidden')); - assert.equal( - element.$.openDialog.querySelector('gr-autocomplete').text, - 'test/path.cpp'); - done(); - }); - }); - - test('_getDialogFromEvent', () => { - const spy = sandbox.spy(element, '_getDialogFromEvent'); - element.addEventListener('tap', element._getDialogFromEvent); - - MockInteractions.tap(element.$.openDialog); - flushAsynchronousOperations(); - assert.equal(spy.lastCall.returnValue.id, 'openDialog'); - - MockInteractions.tap(element.$.deleteDialog); - flushAsynchronousOperations(); - assert.equal(spy.lastCall.returnValue.id, 'deleteDialog'); - - MockInteractions.tap( - element.$.deleteDialog.querySelector('gr-autocomplete')); - flushAsynchronousOperations(); - assert.equal(spy.lastCall.returnValue.id, 'deleteDialog'); - - MockInteractions.tap(element); - flushAsynchronousOperations(); - assert.notOk(spy.lastCall.returnValue); }); }); + + suite('delete button CUJ', () => { + let navStub; + let deleteStub; + let deleteAutocomplete; + + setup(() => { + navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + deleteStub = sandbox.stub(element.$.restAPI, 'deleteFileInChangeEdit'); + deleteAutocomplete = + element.$.deleteDialog.querySelector('gr-autocomplete'); + }); + + test('delete', () => { + deleteStub.returns(Promise.resolve({ok: true})); + MockInteractions.tap(element.shadowRoot.querySelector('#delete')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.deleteDialog.disabled); + assert.isFalse(queryStub.called); + deleteAutocomplete.noDebounce = true; + deleteAutocomplete.text = 'src/test.cpp'; + assert.isTrue(queryStub.called); + assert.isFalse(element.$.deleteDialog.disabled); + MockInteractions.tap(element.$.deleteDialog.shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.isTrue(deleteStub.called); + + return deleteStub.lastCall.returnValue.then(() => { + assert.equal(element._path, ''); + assert.isTrue(navStub.called); + assert.isTrue(closeDialogSpy.called); + }); + }); + }); + + test('delete fails', () => { + deleteStub.returns(Promise.resolve({ok: false})); + MockInteractions.tap(element.shadowRoot.querySelector('#delete')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.deleteDialog.disabled); + assert.isFalse(queryStub.called); + deleteAutocomplete.noDebounce = true; + deleteAutocomplete.text = 'src/test.cpp'; + assert.isTrue(queryStub.called); + assert.isFalse(element.$.deleteDialog.disabled); + MockInteractions.tap(element.$.deleteDialog.shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.isTrue(deleteStub.called); + + return deleteStub.lastCall.returnValue.then(() => { + assert.isFalse(navStub.called); + assert.isFalse(closeDialogSpy.called); + }); + }); + }); + + test('cancel', () => { + MockInteractions.tap(element.shadowRoot.querySelector('#delete')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.deleteDialog.disabled); + element.$.deleteDialog.querySelector('gr-autocomplete').text = + 'src/test.cpp'; + assert.isFalse(element.$.deleteDialog.disabled); + MockInteractions.tap(element.$.deleteDialog.shadowRoot + .querySelector('gr-button')); + assert.isFalse(navStub.called); + assert.isTrue(closeDialogSpy.called); + assert.equal(element._path, 'src/test.cpp'); + }); + }); + }); + + suite('rename button CUJ', () => { + let navStub; + let renameStub; + let renameAutocomplete; + const inputSelector = PolymerElement ? + '.newPathIronInput' : + '.newPathInput'; + + setup(() => { + navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + renameStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit'); + renameAutocomplete = + element.$.renameDialog.querySelector('gr-autocomplete'); + }); + + test('rename', () => { + renameStub.returns(Promise.resolve({ok: true})); + MockInteractions.tap(element.shadowRoot.querySelector('#rename')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.renameDialog.disabled); + assert.isFalse(queryStub.called); + renameAutocomplete.noDebounce = true; + renameAutocomplete.text = 'src/test.cpp'; + assert.isTrue(queryStub.called); + assert.isTrue(element.$.renameDialog.disabled); + + element.$.renameDialog.querySelector(inputSelector).bindValue = + 'src/test.newPath'; + + assert.isFalse(element.$.renameDialog.disabled); + MockInteractions.tap(element.$.renameDialog.shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.isTrue(renameStub.called); + + return renameStub.lastCall.returnValue.then(() => { + assert.equal(element._path, ''); + assert.isTrue(navStub.called); + assert.isTrue(closeDialogSpy.called); + }); + }); + }); + + test('rename fails', () => { + renameStub.returns(Promise.resolve({ok: false})); + MockInteractions.tap(element.shadowRoot.querySelector('#rename')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.renameDialog.disabled); + assert.isFalse(queryStub.called); + renameAutocomplete.noDebounce = true; + renameAutocomplete.text = 'src/test.cpp'; + assert.isTrue(queryStub.called); + assert.isTrue(element.$.renameDialog.disabled); + + element.$.renameDialog.querySelector(inputSelector).bindValue = + 'src/test.newPath'; + + assert.isFalse(element.$.renameDialog.disabled); + MockInteractions.tap(element.$.renameDialog.shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.isTrue(renameStub.called); + + return renameStub.lastCall.returnValue.then(() => { + assert.isFalse(navStub.called); + assert.isFalse(closeDialogSpy.called); + }); + }); + }); + + test('cancel', () => { + MockInteractions.tap(element.shadowRoot.querySelector('#rename')); + return showDialogSpy.lastCall.returnValue.then(() => { + assert.isTrue(element.$.renameDialog.disabled); + element.$.renameDialog.querySelector('gr-autocomplete').text = + 'src/test.cpp'; + element.$.renameDialog.querySelector(inputSelector).bindValue = + 'src/test.newPath'; + assert.isFalse(element.$.renameDialog.disabled); + MockInteractions.tap(element.$.renameDialog.shadowRoot + .querySelector('gr-button')); + assert.isFalse(navStub.called); + assert.isTrue(closeDialogSpy.called); + assert.equal(element._path, 'src/test.cpp'); + assert.equal(element._newPath, 'src/test.newPath'); + }); + }); + }); + + suite('restore button CUJ', () => { + let navStub; + let restoreStub; + + setup(() => { + navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + restoreStub = sandbox.stub(element.$.restAPI, 'restoreFileInChangeEdit'); + }); + + test('restore hidden by default', () => { + assert.isTrue(element.shadowRoot + .querySelector('#restore').classList.contains('invisible')); + }); + + test('restore', () => { + restoreStub.returns(Promise.resolve({ok: true})); + element._path = 'src/test.cpp'; + MockInteractions.tap(element.shadowRoot.querySelector('#restore')); + return showDialogSpy.lastCall.returnValue.then(() => { + MockInteractions.tap(element.$.restoreDialog.shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.isTrue(restoreStub.called); + assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp'); + return restoreStub.lastCall.returnValue.then(() => { + assert.equal(element._path, ''); + assert.isTrue(navStub.called); + assert.isTrue(closeDialogSpy.called); + }); + }); + }); + + test('restore fails', () => { + restoreStub.returns(Promise.resolve({ok: false})); + element._path = 'src/test.cpp'; + MockInteractions.tap(element.shadowRoot.querySelector('#restore')); + return showDialogSpy.lastCall.returnValue.then(() => { + MockInteractions.tap(element.$.restoreDialog.shadowRoot + .querySelector('gr-button[primary]')); + flushAsynchronousOperations(); + + assert.isTrue(restoreStub.called); + assert.equal(restoreStub.lastCall.args[1], 'src/test.cpp'); + return restoreStub.lastCall.returnValue.then(() => { + assert.isFalse(navStub.called); + assert.isFalse(closeDialogSpy.called); + }); + }); + }); + + test('cancel', () => { + element._path = 'src/test.cpp'; + MockInteractions.tap(element.shadowRoot.querySelector('#restore')); + return showDialogSpy.lastCall.returnValue.then(() => { + MockInteractions.tap(element.$.restoreDialog.shadowRoot + .querySelector('gr-button')); + assert.isFalse(navStub.called); + assert.isTrue(closeDialogSpy.called); + assert.equal(element._path, 'src/test.cpp'); + }); + }); + }); + + test('openOpenDialog', done => { + element.openOpenDialog('test/path.cpp') + .then(() => { + assert.isFalse(element.$.openDialog.hasAttribute('hidden')); + assert.equal( + element.$.openDialog.querySelector('gr-autocomplete').text, + 'test/path.cpp'); + done(); + }); + }); + + test('_getDialogFromEvent', () => { + const spy = sandbox.spy(element, '_getDialogFromEvent'); + element.addEventListener('tap', element._getDialogFromEvent); + + MockInteractions.tap(element.$.openDialog); + flushAsynchronousOperations(); + assert.equal(spy.lastCall.returnValue.id, 'openDialog'); + + MockInteractions.tap(element.$.deleteDialog); + flushAsynchronousOperations(); + assert.equal(spy.lastCall.returnValue.id, 'deleteDialog'); + + MockInteractions.tap( + element.$.deleteDialog.querySelector('gr-autocomplete')); + flushAsynchronousOperations(); + assert.equal(spy.lastCall.returnValue.id, 'deleteDialog'); + + MockInteractions.tap(element); + flushAsynchronousOperations(); + assert.notOk(spy.lastCall.returnValue); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js index d59fcf7..10bff3c 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls.js
@@ -14,56 +14,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrEditFileControls extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-edit-file-controls'; } - /** - * Fired when an action in the overflow menu is tapped. - * - * @event file-action-tap - */ +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-dropdown/gr-dropdown.js'; +import '../gr-edit-constants.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-edit-file-controls_html.js'; - static get properties() { - return { - filePath: String, - _allFileActions: { - type: Array, - value: () => Object.values(GrEditConstants.Actions), - }, - _fileActions: { - type: Array, - computed: '_computeFileActions(_allFileActions)', - }, - }; - } +/** @extends Polymer.Element */ +class GrEditFileControls extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _handleActionTap(e) { - e.preventDefault(); - e.stopPropagation(); - this._dispatchFileAction(e.detail.id, this.filePath); - } + static get is() { return 'gr-edit-file-controls'; } + /** + * Fired when an action in the overflow menu is tapped. + * + * @event file-action-tap + */ - _dispatchFileAction(action, path) { - this.dispatchEvent(new CustomEvent( - 'file-action-tap', - {detail: {action, path}, bubbles: true, composed: true})); - } - - _computeFileActions(actions) { - // TODO(kaspern): conditionally disable some actions based on file status. - return actions.map(action => { - return { - name: action.label, - id: action.id, - }; - }); - } + static get properties() { + return { + filePath: String, + _allFileActions: { + type: Array, + value: () => Object.values(GrEditConstants.Actions), + }, + _fileActions: { + type: Array, + computed: '_computeFileActions(_allFileActions)', + }, + }; } - customElements.define(GrEditFileControls.is, GrEditFileControls); -})(); + _handleActionTap(e) { + e.preventDefault(); + e.stopPropagation(); + this._dispatchFileAction(e.detail.id, this.filePath); + } + + _dispatchFileAction(action, path) { + this.dispatchEvent(new CustomEvent( + 'file-action-tap', + {detail: {action, path}, bubbles: true, composed: true})); + } + + _computeFileActions(actions) { + // TODO(kaspern): conditionally disable some actions based on file status. + return actions.map(action => { + return { + name: action.label, + id: action.id, + }; + }); + } +} + +customElements.define(GrEditFileControls.is, GrEditFileControls);
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js index f6c7803..7a7ba5d 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js +++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> -<link rel="import" href="../gr-edit-constants.html"> - -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-edit-file-controls"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { align-items: center; @@ -49,13 +41,5 @@ } } </style> - <gr-dropdown - id="actions" - items="[[_fileActions]]" - down-arrow - vertical-offset="20" - on-tap-item="_handleActionTap" - link>Actions</gr-dropdown> - </template> - <script src="gr-edit-file-controls.js"></script> -</dom-module> + <gr-dropdown id="actions" items="[[_fileActions]]" down-arrow="" vertical-offset="20" on-tap-item="_handleActionTap" link="">Actions</gr-dropdown> +`;
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html index 392a105..d694226 100644 --- a/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html +++ b/polygerrit-ui/app/elements/edit/gr-edit-file-controls/gr-edit-file-controls_test.html
@@ -18,17 +18,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-edit-file-controls</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="../gr-edit-constants.html"> -<link rel="import" href="gr-edit-file-controls.html"> +<script type="module" src="../gr-edit-constants.js"></script> +<script type="module" src="./gr-edit-file-controls.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-edit-constants.js'; +import './gr-edit-file-controls.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,76 +42,79 @@ </template> </test-fixture> -<script> - suite('gr-edit-file-controls tests', async () => { - await readyToTest(); - let element; - let sandbox; - let fileActionHandler; - - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - fileActionHandler = sandbox.stub(); - element.addEventListener('file-action-tap', fileActionHandler); - }); - - teardown(() => { sandbox.restore(); }); - - test('open tap emits event', () => { - const actions = element.$.actions; - element.filePath = 'foo'; - actions._open(); - flushAsynchronousOperations(); - - MockInteractions.tap(actions.shadowRoot - .querySelector('li [data-id="open"]')); - assert.isTrue(fileActionHandler.called); - assert.deepEqual(fileActionHandler.lastCall.args[0].detail, - {action: GrEditConstants.Actions.OPEN.id, path: 'foo'}); - }); - - test('delete tap emits event', () => { - const actions = element.$.actions; - element.filePath = 'foo'; - actions._open(); - flushAsynchronousOperations(); - - MockInteractions.tap(actions.shadowRoot - .querySelector('li [data-id="delete"]')); - assert.isTrue(fileActionHandler.called); - assert.deepEqual(fileActionHandler.lastCall.args[0].detail, - {action: GrEditConstants.Actions.DELETE.id, path: 'foo'}); - }); - - test('restore tap emits event', () => { - const actions = element.$.actions; - element.filePath = 'foo'; - actions._open(); - flushAsynchronousOperations(); - - MockInteractions.tap(actions.shadowRoot - .querySelector('li [data-id="restore"]')); - assert.isTrue(fileActionHandler.called); - assert.deepEqual(fileActionHandler.lastCall.args[0].detail, - {action: GrEditConstants.Actions.RESTORE.id, path: 'foo'}); - }); - - test('rename tap emits event', () => { - const actions = element.$.actions; - element.filePath = 'foo'; - actions._open(); - flushAsynchronousOperations(); - - MockInteractions.tap(actions.shadowRoot - .querySelector('li [data-id="rename"]')); - assert.isTrue(fileActionHandler.called); - assert.deepEqual(fileActionHandler.lastCall.args[0].detail, - {action: GrEditConstants.Actions.RENAME.id, path: 'foo'}); - }); - - test('computed properties', () => { - assert.equal(element._allFileActions.length, 4); - }); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-edit-constants.js'; +import './gr-edit-file-controls.js'; +suite('gr-edit-file-controls tests', () => { + let element; + let sandbox; + let fileActionHandler; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + fileActionHandler = sandbox.stub(); + element.addEventListener('file-action-tap', fileActionHandler); }); + + teardown(() => { sandbox.restore(); }); + + test('open tap emits event', () => { + const actions = element.$.actions; + element.filePath = 'foo'; + actions._open(); + flushAsynchronousOperations(); + + MockInteractions.tap(actions.shadowRoot + .querySelector('li [data-id="open"]')); + assert.isTrue(fileActionHandler.called); + assert.deepEqual(fileActionHandler.lastCall.args[0].detail, + {action: GrEditConstants.Actions.OPEN.id, path: 'foo'}); + }); + + test('delete tap emits event', () => { + const actions = element.$.actions; + element.filePath = 'foo'; + actions._open(); + flushAsynchronousOperations(); + + MockInteractions.tap(actions.shadowRoot + .querySelector('li [data-id="delete"]')); + assert.isTrue(fileActionHandler.called); + assert.deepEqual(fileActionHandler.lastCall.args[0].detail, + {action: GrEditConstants.Actions.DELETE.id, path: 'foo'}); + }); + + test('restore tap emits event', () => { + const actions = element.$.actions; + element.filePath = 'foo'; + actions._open(); + flushAsynchronousOperations(); + + MockInteractions.tap(actions.shadowRoot + .querySelector('li [data-id="restore"]')); + assert.isTrue(fileActionHandler.called); + assert.deepEqual(fileActionHandler.lastCall.args[0].detail, + {action: GrEditConstants.Actions.RESTORE.id, path: 'foo'}); + }); + + test('rename tap emits event', () => { + const actions = element.$.actions; + element.filePath = 'foo'; + actions._open(); + flushAsynchronousOperations(); + + MockInteractions.tap(actions.shadowRoot + .querySelector('li [data-id="rename"]')); + assert.isTrue(fileActionHandler.called); + assert.deepEqual(fileActionHandler.lastCall.args[0].detail, + {action: GrEditConstants.Actions.RENAME.id, path: 'foo'}); + }); + + test('computed properties', () => { + assert.equal(element._allFileActions.length, 4); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js index 64158d0..2303005 100644 --- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js +++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.js
@@ -14,257 +14,277 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const RESTORED_MESSAGE = 'Content restored from a previous edit.'; - const SAVING_MESSAGE = 'Saving changes...'; - const SAVED_MESSAGE = 'All changes saved'; - const SAVE_FAILED_MSG = 'Failed to save changes'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-editable-label/gr-editable-label.js'; +import '../../shared/gr-fixed-panel/gr-fixed-panel.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-storage/gr-storage.js'; +import '../gr-default-editor/gr-default-editor.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-editor-view_html.js'; - const STORAGE_DEBOUNCE_INTERVAL_MS = 100; +const RESTORED_MESSAGE = 'Content restored from a previous edit.'; +const SAVING_MESSAGE = 'Saving changes...'; +const SAVED_MESSAGE = 'All changes saved'; +const SAVE_FAILED_MSG = 'Failed to save changes'; + +const STORAGE_DEBOUNCE_INTERVAL_MS = 100; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.PathListMixin + * @extends Polymer.Element + */ +class GrEditorView extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PatchSetBehavior, + Gerrit.PathListBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-editor-view'; } + /** + * Fired when the title of the page should change. + * + * @event title-change + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.PathListMixin - * @extends Polymer.Element + * Fired to notify the user of + * + * @event show-alert */ - class GrEditorView extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PatchSetBehavior, - Gerrit.PathListBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-editor-view'; } + + static get properties() { + return { /** - * Fired when the title of the page should change. - * - * @event title-change + * URL params passed from the router. */ + params: { + type: Object, + observer: '_paramsChanged', + }, - /** - * Fired to notify the user of - * - * @event show-alert - */ - - static get properties() { - return { - /** - * URL params passed from the router. - */ - params: { - type: Object, - observer: '_paramsChanged', - }, - - _change: Object, - _changeEditDetail: Object, - _changeNum: String, - _patchNum: String, - _path: String, - _type: String, - _content: String, - _newContent: String, - _saving: { - type: Boolean, - value: false, - }, - _successfulSave: { - type: Boolean, - value: false, - }, - _saveDisabled: { - type: Boolean, - value: true, - computed: '_computeSaveDisabled(_content, _newContent, _saving)', - }, - _prefs: Object, - _lineNum: Number, - }; - } - - get keyBindings() { - return { - 'ctrl+s meta+s': '_handleSaveShortcut', - }; - } - - /** @override */ - created() { - super.created(); - this.addEventListener('content-change', - e => this._handleContentChange(e)); - } - - /** @override */ - attached() { - super.attached(); - this._getEditPrefs().then(prefs => { this._prefs = prefs; }); - } - - get storageKey() { - return `c${this._changeNum}_ps${this._patchNum}_${this._path}`; - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _getEditPrefs() { - return this.$.restAPI.getEditPreferences(); - } - - _paramsChanged(value) { - if (value.view !== Gerrit.Nav.View.EDIT) { - return; - } - - this._changeNum = value.changeNum; - this._path = value.path; - this._patchNum = value.patchNum || this.EDIT_NAME; - this._lineNum = value.lineNum; - - // NOTE: This may be called before attachment (e.g. while parentElement is - // null). Fire title-change in an async so that, if attachment to the DOM - // has been queued, the event can bubble up to the handler in gr-app. - this.async(() => { - const title = `Editing ${this.computeTruncatedPath(this._path)}`; - this.fire('title-change', {title}); - }); - - const promises = []; - - promises.push(this._getChangeDetail(this._changeNum)); - promises.push( - this._getFileData(this._changeNum, this._path, this._patchNum)); - return Promise.all(promises); - } - - _getChangeDetail(changeNum) { - return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { - this._change = change; - }); - } - - _handlePathChanged(e) { - const path = e.detail; - if (path === this._path) { - return Promise.resolve(); - } - return this.$.restAPI.renameFileInChangeEdit(this._changeNum, - this._path, path).then(res => { - if (!res.ok) { return; } - - this._successfulSave = true; - this._viewEditInChangeView(); - }); - } - - _viewEditInChangeView() { - const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum; - Gerrit.Nav.navigateToChange(this._change, patch, null, - patch !== this.EDIT_NAME); - } - - _getFileData(changeNum, path, patchNum) { - const storedContent = - this.$.storage.getEditableContentItem(this.storageKey); - - return this.$.restAPI.getFileContent(changeNum, path, patchNum) - .then(res => { - if (storedContent && storedContent.message && - storedContent.message !== res.content) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: RESTORED_MESSAGE}, - bubbles: true, - composed: true, - })); - - this._newContent = storedContent.message; - } else { - this._newContent = res.content || ''; - } - this._content = res.content || ''; - - // A non-ok response may result if the file does not yet exist. - // The `type` field of the response is only valid when the file - // already exists. - if (res.ok && res.type) { - this._type = res.type; - } else { - this._type = ''; - } - }); - } - - _saveEdit() { - this._saving = true; - this._showAlert(SAVING_MESSAGE); - this.$.storage.eraseEditableContentItem(this.storageKey); - return this.$.restAPI.saveChangeEdit(this._changeNum, this._path, - this._newContent).then(res => { - this._saving = false; - this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG); - if (!res.ok) { return; } - - this._content = this._newContent; - this._successfulSave = true; - }); - } - - _showAlert(message) { - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message}, - bubbles: true, - composed: true, - })); - } - - _computeSaveDisabled(content, newContent, saving) { - // Polymer 2: check for undefined - if ([ - content, - newContent, - saving, - ].some(arg => arg === undefined)) { - return true; - } - - if (saving) { - return true; - } - return content === newContent; - } - - _handleCloseTap() { - // TODO(kaspern): Add a confirm dialog if there are unsaved changes. - this._viewEditInChangeView(); - } - - _handleContentChange(e) { - this.debounce('store', () => { - const content = e.detail.value; - if (content) { - this.set('_newContent', e.detail.value); - this.$.storage.setEditableContentItem(this.storageKey, content); - } else { - this.$.storage.eraseEditableContentItem(this.storageKey); - } - }, STORAGE_DEBOUNCE_INTERVAL_MS); - } - - _handleSaveShortcut(e) { - e.preventDefault(); - if (!this._saveDisabled) { - this._saveEdit(); - } - } + _change: Object, + _changeEditDetail: Object, + _changeNum: String, + _patchNum: String, + _path: String, + _type: String, + _content: String, + _newContent: String, + _saving: { + type: Boolean, + value: false, + }, + _successfulSave: { + type: Boolean, + value: false, + }, + _saveDisabled: { + type: Boolean, + value: true, + computed: '_computeSaveDisabled(_content, _newContent, _saving)', + }, + _prefs: Object, + _lineNum: Number, + }; } - customElements.define(GrEditorView.is, GrEditorView); -})(); + get keyBindings() { + return { + 'ctrl+s meta+s': '_handleSaveShortcut', + }; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('content-change', + e => this._handleContentChange(e)); + } + + /** @override */ + attached() { + super.attached(); + this._getEditPrefs().then(prefs => { this._prefs = prefs; }); + } + + get storageKey() { + return `c${this._changeNum}_ps${this._patchNum}_${this._path}`; + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _getEditPrefs() { + return this.$.restAPI.getEditPreferences(); + } + + _paramsChanged(value) { + if (value.view !== Gerrit.Nav.View.EDIT) { + return; + } + + this._changeNum = value.changeNum; + this._path = value.path; + this._patchNum = value.patchNum || this.EDIT_NAME; + this._lineNum = value.lineNum; + + // NOTE: This may be called before attachment (e.g. while parentElement is + // null). Fire title-change in an async so that, if attachment to the DOM + // has been queued, the event can bubble up to the handler in gr-app. + this.async(() => { + const title = `Editing ${this.computeTruncatedPath(this._path)}`; + this.fire('title-change', {title}); + }); + + const promises = []; + + promises.push(this._getChangeDetail(this._changeNum)); + promises.push( + this._getFileData(this._changeNum, this._path, this._patchNum)); + return Promise.all(promises); + } + + _getChangeDetail(changeNum) { + return this.$.restAPI.getDiffChangeDetail(changeNum).then(change => { + this._change = change; + }); + } + + _handlePathChanged(e) { + const path = e.detail; + if (path === this._path) { + return Promise.resolve(); + } + return this.$.restAPI.renameFileInChangeEdit(this._changeNum, + this._path, path).then(res => { + if (!res.ok) { return; } + + this._successfulSave = true; + this._viewEditInChangeView(); + }); + } + + _viewEditInChangeView() { + const patch = this._successfulSave ? this.EDIT_NAME : this._patchNum; + Gerrit.Nav.navigateToChange(this._change, patch, null, + patch !== this.EDIT_NAME); + } + + _getFileData(changeNum, path, patchNum) { + const storedContent = + this.$.storage.getEditableContentItem(this.storageKey); + + return this.$.restAPI.getFileContent(changeNum, path, patchNum) + .then(res => { + if (storedContent && storedContent.message && + storedContent.message !== res.content) { + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message: RESTORED_MESSAGE}, + bubbles: true, + composed: true, + })); + + this._newContent = storedContent.message; + } else { + this._newContent = res.content || ''; + } + this._content = res.content || ''; + + // A non-ok response may result if the file does not yet exist. + // The `type` field of the response is only valid when the file + // already exists. + if (res.ok && res.type) { + this._type = res.type; + } else { + this._type = ''; + } + }); + } + + _saveEdit() { + this._saving = true; + this._showAlert(SAVING_MESSAGE); + this.$.storage.eraseEditableContentItem(this.storageKey); + return this.$.restAPI.saveChangeEdit(this._changeNum, this._path, + this._newContent).then(res => { + this._saving = false; + this._showAlert(res.ok ? SAVED_MESSAGE : SAVE_FAILED_MSG); + if (!res.ok) { return; } + + this._content = this._newContent; + this._successfulSave = true; + }); + } + + _showAlert(message) { + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message}, + bubbles: true, + composed: true, + })); + } + + _computeSaveDisabled(content, newContent, saving) { + // Polymer 2: check for undefined + if ([ + content, + newContent, + saving, + ].some(arg => arg === undefined)) { + return true; + } + + if (saving) { + return true; + } + return content === newContent; + } + + _handleCloseTap() { + // TODO(kaspern): Add a confirm dialog if there are unsaved changes. + this._viewEditInChangeView(); + } + + _handleContentChange(e) { + this.debounce('store', () => { + const content = e.detail.value; + if (content) { + this.set('_newContent', e.detail.value); + this.$.storage.setEditableContentItem(this.storageKey, content); + } else { + this.$.storage.eraseEditableContentItem(this.storageKey); + } + }, STORAGE_DEBOUNCE_INTERVAL_MS); + } + + _handleSaveShortcut(e) { + e.preventDefault(); + if (!this._saveDisabled) { + this._saveEdit(); + } + } +} + +customElements.define(GrEditorView.is, GrEditorView);
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js index 1ae74e1..72d29dd 100644 --- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js +++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_html.js
@@ -1,39 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-editable-label/gr-editable-label.html"> -<link rel="import" href="../../shared/gr-fixed-panel/gr-fixed-panel.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-storage/gr-storage.html"> -<link rel="import" href="../gr-default-editor/gr-default-editor.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-editor-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background-color: var(--view-background-color); @@ -93,28 +76,16 @@ } } </style> - <gr-fixed-panel keep-on-scroll> + <gr-fixed-panel keep-on-scroll=""> <header> <span class="controlGroup"> <span>Edit mode</span> <span class="separator"></span> - <gr-editable-label - label-text="File path" - value="[[_path]]" - placeholder="File path..." - on-changed="_handlePathChanged"></gr-editable-label> + <gr-editable-label label-text="File path" value="[[_path]]" placeholder="File path..." on-changed="_handlePathChanged"></gr-editable-label> </span> <span class="controlGroup rightControls"> - <gr-button - id="close" - link - on-click="_handleCloseTap">Close</gr-button> - <gr-button - id="save" - disabled$="[[_saveDisabled]]" - primary - link - on-click="_saveEdit">Save</gr-button> + <gr-button id="close" link="" on-click="_handleCloseTap">Close</gr-button> + <gr-button id="save" disabled\$="[[_saveDisabled]]" primary="" link="" on-click="_saveEdit">Save</gr-button> </span> </header> </gr-fixed-panel> @@ -129,6 +100,4 @@ </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> - </template> - <script src="gr-editor-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html index 1d264bc..8c0d491 100644 --- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html +++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.html
@@ -18,16 +18,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editor-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-editor-view.html"> +<script type="module" src="./gr-editor-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-editor-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,380 +40,382 @@ </template> </test-fixture> -<script> - suite('gr-editor-view tests', async () => { - await readyToTest(); - let element; - let sandbox; - let savePathStub; - let saveFileStub; - let changeDetailStub; - let navigateStub; - const mockParams = { - changeNum: '42', - path: 'foo/bar.baz', - patchNum: 'edit', - }; - - setup(() => { - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getEditPreferences() { return Promise.resolve({}); }, - }); - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit'); - saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit'); - changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail'); - navigateStub = sandbox.stub(element, '_viewEditInChangeView'); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-editor-view.js'; +suite('gr-editor-view tests', () => { + let element; + let sandbox; + let savePathStub; + let saveFileStub; + let changeDetailStub; + let navigateStub; + const mockParams = { + changeNum: '42', + path: 'foo/bar.baz', + patchNum: 'edit', + }; + + setup(() => { + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getEditPreferences() { return Promise.resolve({}); }, }); - - teardown(() => { sandbox.restore(); }); - - suite('_paramsChanged', () => { - test('incorrect view returns immediately', () => { - element._paramsChanged( - Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF})); - assert.notOk(element._changeNum); - }); - - test('good params proceed', () => { - changeDetailStub.returns(Promise.resolve({})); - const fileStub = sandbox.stub(element, '_getFileData', () => { - element._content = 'text'; - element._newContent = 'text'; - element._type = 'application/octet-stream'; - }); - - const promises = element._paramsChanged( - Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT})); - - flushAsynchronousOperations(); - assert.equal(element._changeNum, mockParams.changeNum); - assert.equal(element._path, mockParams.path); - assert.deepEqual(changeDetailStub.lastCall.args[0], - mockParams.changeNum); - assert.deepEqual(fileStub.lastCall.args, - [mockParams.changeNum, mockParams.path, mockParams.patchNum]); - - return promises.then(() => { - assert.equal(element._content, 'text'); - assert.equal(element._newContent, 'text'); - assert.equal(element._type, 'application/octet-stream'); - }); - }); + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + savePathStub = sandbox.stub(element.$.restAPI, 'renameFileInChangeEdit'); + saveFileStub = sandbox.stub(element.$.restAPI, 'saveChangeEdit'); + changeDetailStub = sandbox.stub(element.$.restAPI, 'getDiffChangeDetail'); + navigateStub = sandbox.stub(element, '_viewEditInChangeView'); + }); + + teardown(() => { sandbox.restore(); }); + + suite('_paramsChanged', () => { + test('incorrect view returns immediately', () => { + element._paramsChanged( + Object.assign({}, mockParams, {view: Gerrit.Nav.View.DIFF})); + assert.notOk(element._changeNum); }); - - test('edit file path', () => { - element._changeNum = mockParams.changeNum; - element._path = mockParams.path; - savePathStub.onFirstCall().returns(Promise.resolve({})); - savePathStub.onSecondCall().returns(Promise.resolve({ok: true})); - - // Calling with the same path should not navigate. - return element._handlePathChanged({detail: mockParams.path}).then(() => { - assert.isFalse(savePathStub.called); - // !ok response - element._handlePathChanged({detail: 'newPath'}).then(() => { - assert.isTrue(savePathStub.called); - assert.isFalse(navigateStub.called); - // ok response - element._handlePathChanged({detail: 'newPath'}).then(() => { - assert.isTrue(navigateStub.called); - assert.isTrue(element._successfulSave); - }); - }); + + test('good params proceed', () => { + changeDetailStub.returns(Promise.resolve({})); + const fileStub = sandbox.stub(element, '_getFileData', () => { + element._content = 'text'; + element._newContent = 'text'; + element._type = 'application/octet-stream'; }); - }); - - test('reacts to content-change event', () => { - const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem'); - element._newContent = 'test'; - element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', { - bubbles: true, composed: true, - detail: {value: 'new content value'}, - })); - element.flushDebouncer('store'); + + const promises = element._paramsChanged( + Object.assign({}, mockParams, {view: Gerrit.Nav.View.EDIT})); + flushAsynchronousOperations(); - - assert.equal(element._newContent, 'new content value'); - assert.isTrue(storeStub.called); - assert.equal(storeStub.lastCall.args[1], 'new content value'); - }); - - suite('edit file content', () => { - const originalText = 'file text'; - const newText = 'file text changed'; - - setup(() => { - element._changeNum = mockParams.changeNum; - element._path = mockParams.path; - element._content = originalText; - element._newContent = originalText; - flushAsynchronousOperations(); - }); - - test('initial load', () => { - assert.equal(element.$.file.fileContent, originalText); - assert.isTrue(element.$.save.hasAttribute('disabled')); - }); - - test('file modification and save, !ok response', () => { - const saveSpy = sandbox.spy(element, '_saveEdit'); - const eraseStub = sandbox.stub(element.$.storage, - 'eraseEditableContentItem'); - const alertStub = sandbox.stub(element, '_showAlert'); - saveFileStub.returns(Promise.resolve({ok: false})); - element._newContent = newText; - flushAsynchronousOperations(); - - assert.isFalse(element.$.save.hasAttribute('disabled')); - assert.isFalse(element._saving); - - MockInteractions.tap(element.$.save); - assert.isTrue(saveSpy.called); - assert.equal(alertStub.lastCall.args[0], 'Saving changes...'); - assert.isTrue(element._saving); - assert.isTrue(element.$.save.hasAttribute('disabled')); - - return saveSpy.lastCall.returnValue.then(() => { - assert.isTrue(saveFileStub.called); - assert.isTrue(eraseStub.called); - assert.isFalse(element._saving); - assert.equal(alertStub.lastCall.args[0], 'Failed to save changes'); - assert.deepEqual(saveFileStub.lastCall.args, - [mockParams.changeNum, mockParams.path, newText]); - assert.isFalse(navigateStub.called); - assert.isFalse(element.$.save.hasAttribute('disabled')); - assert.notEqual(element._content, element._newContent); - }); - }); - - test('file modification and save', () => { - const saveSpy = sandbox.spy(element, '_saveEdit'); - const alertStub = sandbox.stub(element, '_showAlert'); - saveFileStub.returns(Promise.resolve({ok: true})); - element._newContent = newText; - flushAsynchronousOperations(); - - assert.isFalse(element._saving); - assert.isFalse(element.$.save.hasAttribute('disabled')); - - MockInteractions.tap(element.$.save); - assert.isTrue(saveSpy.called); - assert.equal(alertStub.lastCall.args[0], 'Saving changes...'); - assert.isTrue(element._saving); - assert.isTrue(element.$.save.hasAttribute('disabled')); - - return saveSpy.lastCall.returnValue.then(() => { - assert.isTrue(saveFileStub.called); - assert.isFalse(element._saving); - assert.equal(alertStub.lastCall.args[0], 'All changes saved'); - assert.isFalse(navigateStub.called); - assert.isTrue(element.$.save.hasAttribute('disabled')); - assert.equal(element._content, element._newContent); - assert.isTrue(element._successfulSave); - }); - }); - - test('file modification and close', () => { - const closeSpy = sandbox.spy(element, '_handleCloseTap'); - element._newContent = newText; - flushAsynchronousOperations(); - - assert.isFalse(element.$.save.hasAttribute('disabled')); - - MockInteractions.tap(element.$.close); - assert.isTrue(closeSpy.called); - assert.isFalse(saveFileStub.called); - assert.isTrue(navigateStub.called); - }); - }); - - suite('_getFileData', () => { - setup(() => { - element._newContent = 'initial'; - element._content = 'initial'; - element._type = 'initial'; - sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null); - }); - - test('res.ok', () => { - sandbox.stub(element.$.restAPI, 'getFileContent') - .returns(Promise.resolve({ - ok: true, - type: 'text/javascript', - content: 'new content', - })); - - // Ensure no data is set with a bad response. - return element._getFileData('1', 'test/path', 'edit').then(() => { - assert.equal(element._newContent, 'new content'); - assert.equal(element._content, 'new content'); - assert.equal(element._type, 'text/javascript'); - }); - }); - - test('!res.ok', () => { - sandbox.stub(element.$.restAPI, 'getFileContent') - .returns(Promise.resolve({})); - - // Ensure no data is set with a bad response. - return element._getFileData('1', 'test/path', 'edit').then(() => { - assert.equal(element._newContent, ''); - assert.equal(element._content, ''); - assert.equal(element._type, ''); - }); - }); - - test('content is undefined', () => { - sandbox.stub(element.$.restAPI, 'getFileContent') - .returns(Promise.resolve({ - ok: true, - type: 'text/javascript', - })); - - return element._getFileData('1', 'test/path', 'edit').then(() => { - assert.equal(element._newContent, ''); - assert.equal(element._content, ''); - assert.equal(element._type, 'text/javascript'); - }); - }); - - test('content and type is undefined', () => { - sandbox.stub(element.$.restAPI, 'getFileContent') - .returns(Promise.resolve({ - ok: true, - })); - - return element._getFileData('1', 'test/path', 'edit').then(() => { - assert.equal(element._newContent, ''); - assert.equal(element._content, ''); - assert.equal(element._type, ''); - }); - }); - }); - - test('_showAlert', done => { - element.addEventListener('show-alert', e => { - assert.deepEqual(e.detail, {message: 'test message'}); - assert.isTrue(e.bubbles); - done(); - }); - - element._showAlert('test message'); - }); - - test('_viewEditInChangeView respects _patchNum', () => { - navigateStub.restore(); - const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); - element._patchNum = element.EDIT_NAME; - element._viewEditInChangeView(); - assert.equal(navStub.lastCall.args[1], element.EDIT_NAME); - element._patchNum = '1'; - element._viewEditInChangeView(); - assert.equal(navStub.lastCall.args[1], '1'); - element._successfulSave = true; - element._viewEditInChangeView(); - assert.equal(navStub.lastCall.args[1], element.EDIT_NAME); - }); - - suite('keyboard shortcuts', () => { - // Used as the spy on the handler for each entry in keyBindings. - let handleSpy; - - suite('_handleSaveShortcut', () => { - let saveStub; - setup(() => { - handleSpy = sandbox.spy(element, '_handleSaveShortcut'); - saveStub = sandbox.stub(element, '_saveEdit'); - }); - - test('save enabled', () => { - element._content = ''; - element._newContent = '_test'; - MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's'); - flushAsynchronousOperations(); - - assert.isTrue(handleSpy.calledOnce); - assert.isTrue(saveStub.calledOnce); - - MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's'); - flushAsynchronousOperations(); - - assert.equal(handleSpy.callCount, 2); - assert.equal(saveStub.callCount, 2); - }); - - test('save disabled', () => { - MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's'); - flushAsynchronousOperations(); - - assert.isTrue(handleSpy.calledOnce); - assert.isFalse(saveStub.called); - - MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's'); - flushAsynchronousOperations(); - - assert.equal(handleSpy.callCount, 2); - assert.isFalse(saveStub.called); - }); - }); - }); - - suite('gr-storage caching', () => { - test('local edit exists', () => { - sandbox.stub(element.$.storage, 'getEditableContentItem') - .returns({message: 'pending edit'}); - sandbox.stub(element.$.restAPI, 'getFileContent') - .returns(Promise.resolve({ - ok: true, - type: 'text/javascript', - content: 'old content', - })); - - const alertStub = sandbox.stub(); - element.addEventListener('show-alert', alertStub); - - return element._getFileData(1, 'test', 1).then(() => { - flushAsynchronousOperations(); - - assert.isTrue(alertStub.called); - assert.equal(element._newContent, 'pending edit'); - assert.equal(element._content, 'old content'); - assert.equal(element._type, 'text/javascript'); - }); - }); - - test('local edit exists, is same as remote edit', () => { - sandbox.stub(element.$.storage, 'getEditableContentItem') - .returns({message: 'pending edit'}); - sandbox.stub(element.$.restAPI, 'getFileContent') - .returns(Promise.resolve({ - ok: true, - type: 'text/javascript', - content: 'pending edit', - })); - - const alertStub = sandbox.stub(); - element.addEventListener('show-alert', alertStub); - - return element._getFileData(1, 'test', 1).then(() => { - flushAsynchronousOperations(); - - assert.isFalse(alertStub.called); - assert.equal(element._newContent, 'pending edit'); - assert.equal(element._content, 'pending edit'); - assert.equal(element._type, 'text/javascript'); - }); - }); - - test('storage key computation', () => { - element._changeNum = 1; - element._patchNum = 1; - element._path = 'test'; - assert.equal(element.storageKey, 'c1_ps1_test'); + assert.equal(element._changeNum, mockParams.changeNum); + assert.equal(element._path, mockParams.path); + assert.deepEqual(changeDetailStub.lastCall.args[0], + mockParams.changeNum); + assert.deepEqual(fileStub.lastCall.args, + [mockParams.changeNum, mockParams.path, mockParams.patchNum]); + + return promises.then(() => { + assert.equal(element._content, 'text'); + assert.equal(element._newContent, 'text'); + assert.equal(element._type, 'application/octet-stream'); }); }); }); + + test('edit file path', () => { + element._changeNum = mockParams.changeNum; + element._path = mockParams.path; + savePathStub.onFirstCall().returns(Promise.resolve({})); + savePathStub.onSecondCall().returns(Promise.resolve({ok: true})); + + // Calling with the same path should not navigate. + return element._handlePathChanged({detail: mockParams.path}).then(() => { + assert.isFalse(savePathStub.called); + // !ok response + element._handlePathChanged({detail: 'newPath'}).then(() => { + assert.isTrue(savePathStub.called); + assert.isFalse(navigateStub.called); + // ok response + element._handlePathChanged({detail: 'newPath'}).then(() => { + assert.isTrue(navigateStub.called); + assert.isTrue(element._successfulSave); + }); + }); + }); + }); + + test('reacts to content-change event', () => { + const storeStub = sandbox.spy(element.$.storage, 'setEditableContentItem'); + element._newContent = 'test'; + element.$.editorEndpoint.dispatchEvent(new CustomEvent('content-change', { + bubbles: true, composed: true, + detail: {value: 'new content value'}, + })); + element.flushDebouncer('store'); + flushAsynchronousOperations(); + + assert.equal(element._newContent, 'new content value'); + assert.isTrue(storeStub.called); + assert.equal(storeStub.lastCall.args[1], 'new content value'); + }); + + suite('edit file content', () => { + const originalText = 'file text'; + const newText = 'file text changed'; + + setup(() => { + element._changeNum = mockParams.changeNum; + element._path = mockParams.path; + element._content = originalText; + element._newContent = originalText; + flushAsynchronousOperations(); + }); + + test('initial load', () => { + assert.equal(element.$.file.fileContent, originalText); + assert.isTrue(element.$.save.hasAttribute('disabled')); + }); + + test('file modification and save, !ok response', () => { + const saveSpy = sandbox.spy(element, '_saveEdit'); + const eraseStub = sandbox.stub(element.$.storage, + 'eraseEditableContentItem'); + const alertStub = sandbox.stub(element, '_showAlert'); + saveFileStub.returns(Promise.resolve({ok: false})); + element._newContent = newText; + flushAsynchronousOperations(); + + assert.isFalse(element.$.save.hasAttribute('disabled')); + assert.isFalse(element._saving); + + MockInteractions.tap(element.$.save); + assert.isTrue(saveSpy.called); + assert.equal(alertStub.lastCall.args[0], 'Saving changes...'); + assert.isTrue(element._saving); + assert.isTrue(element.$.save.hasAttribute('disabled')); + + return saveSpy.lastCall.returnValue.then(() => { + assert.isTrue(saveFileStub.called); + assert.isTrue(eraseStub.called); + assert.isFalse(element._saving); + assert.equal(alertStub.lastCall.args[0], 'Failed to save changes'); + assert.deepEqual(saveFileStub.lastCall.args, + [mockParams.changeNum, mockParams.path, newText]); + assert.isFalse(navigateStub.called); + assert.isFalse(element.$.save.hasAttribute('disabled')); + assert.notEqual(element._content, element._newContent); + }); + }); + + test('file modification and save', () => { + const saveSpy = sandbox.spy(element, '_saveEdit'); + const alertStub = sandbox.stub(element, '_showAlert'); + saveFileStub.returns(Promise.resolve({ok: true})); + element._newContent = newText; + flushAsynchronousOperations(); + + assert.isFalse(element._saving); + assert.isFalse(element.$.save.hasAttribute('disabled')); + + MockInteractions.tap(element.$.save); + assert.isTrue(saveSpy.called); + assert.equal(alertStub.lastCall.args[0], 'Saving changes...'); + assert.isTrue(element._saving); + assert.isTrue(element.$.save.hasAttribute('disabled')); + + return saveSpy.lastCall.returnValue.then(() => { + assert.isTrue(saveFileStub.called); + assert.isFalse(element._saving); + assert.equal(alertStub.lastCall.args[0], 'All changes saved'); + assert.isFalse(navigateStub.called); + assert.isTrue(element.$.save.hasAttribute('disabled')); + assert.equal(element._content, element._newContent); + assert.isTrue(element._successfulSave); + }); + }); + + test('file modification and close', () => { + const closeSpy = sandbox.spy(element, '_handleCloseTap'); + element._newContent = newText; + flushAsynchronousOperations(); + + assert.isFalse(element.$.save.hasAttribute('disabled')); + + MockInteractions.tap(element.$.close); + assert.isTrue(closeSpy.called); + assert.isFalse(saveFileStub.called); + assert.isTrue(navigateStub.called); + }); + }); + + suite('_getFileData', () => { + setup(() => { + element._newContent = 'initial'; + element._content = 'initial'; + element._type = 'initial'; + sandbox.stub(element.$.storage, 'getEditableContentItem').returns(null); + }); + + test('res.ok', () => { + sandbox.stub(element.$.restAPI, 'getFileContent') + .returns(Promise.resolve({ + ok: true, + type: 'text/javascript', + content: 'new content', + })); + + // Ensure no data is set with a bad response. + return element._getFileData('1', 'test/path', 'edit').then(() => { + assert.equal(element._newContent, 'new content'); + assert.equal(element._content, 'new content'); + assert.equal(element._type, 'text/javascript'); + }); + }); + + test('!res.ok', () => { + sandbox.stub(element.$.restAPI, 'getFileContent') + .returns(Promise.resolve({})); + + // Ensure no data is set with a bad response. + return element._getFileData('1', 'test/path', 'edit').then(() => { + assert.equal(element._newContent, ''); + assert.equal(element._content, ''); + assert.equal(element._type, ''); + }); + }); + + test('content is undefined', () => { + sandbox.stub(element.$.restAPI, 'getFileContent') + .returns(Promise.resolve({ + ok: true, + type: 'text/javascript', + })); + + return element._getFileData('1', 'test/path', 'edit').then(() => { + assert.equal(element._newContent, ''); + assert.equal(element._content, ''); + assert.equal(element._type, 'text/javascript'); + }); + }); + + test('content and type is undefined', () => { + sandbox.stub(element.$.restAPI, 'getFileContent') + .returns(Promise.resolve({ + ok: true, + })); + + return element._getFileData('1', 'test/path', 'edit').then(() => { + assert.equal(element._newContent, ''); + assert.equal(element._content, ''); + assert.equal(element._type, ''); + }); + }); + }); + + test('_showAlert', done => { + element.addEventListener('show-alert', e => { + assert.deepEqual(e.detail, {message: 'test message'}); + assert.isTrue(e.bubbles); + done(); + }); + + element._showAlert('test message'); + }); + + test('_viewEditInChangeView respects _patchNum', () => { + navigateStub.restore(); + const navStub = sandbox.stub(Gerrit.Nav, 'navigateToChange'); + element._patchNum = element.EDIT_NAME; + element._viewEditInChangeView(); + assert.equal(navStub.lastCall.args[1], element.EDIT_NAME); + element._patchNum = '1'; + element._viewEditInChangeView(); + assert.equal(navStub.lastCall.args[1], '1'); + element._successfulSave = true; + element._viewEditInChangeView(); + assert.equal(navStub.lastCall.args[1], element.EDIT_NAME); + }); + + suite('keyboard shortcuts', () => { + // Used as the spy on the handler for each entry in keyBindings. + let handleSpy; + + suite('_handleSaveShortcut', () => { + let saveStub; + setup(() => { + handleSpy = sandbox.spy(element, '_handleSaveShortcut'); + saveStub = sandbox.stub(element, '_saveEdit'); + }); + + test('save enabled', () => { + element._content = ''; + element._newContent = '_test'; + MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's'); + flushAsynchronousOperations(); + + assert.isTrue(handleSpy.calledOnce); + assert.isTrue(saveStub.calledOnce); + + MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's'); + flushAsynchronousOperations(); + + assert.equal(handleSpy.callCount, 2); + assert.equal(saveStub.callCount, 2); + }); + + test('save disabled', () => { + MockInteractions.pressAndReleaseKeyOn(element, 83, 'ctrl', 's'); + flushAsynchronousOperations(); + + assert.isTrue(handleSpy.calledOnce); + assert.isFalse(saveStub.called); + + MockInteractions.pressAndReleaseKeyOn(element, 83, 'meta', 's'); + flushAsynchronousOperations(); + + assert.equal(handleSpy.callCount, 2); + assert.isFalse(saveStub.called); + }); + }); + }); + + suite('gr-storage caching', () => { + test('local edit exists', () => { + sandbox.stub(element.$.storage, 'getEditableContentItem') + .returns({message: 'pending edit'}); + sandbox.stub(element.$.restAPI, 'getFileContent') + .returns(Promise.resolve({ + ok: true, + type: 'text/javascript', + content: 'old content', + })); + + const alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); + + return element._getFileData(1, 'test', 1).then(() => { + flushAsynchronousOperations(); + + assert.isTrue(alertStub.called); + assert.equal(element._newContent, 'pending edit'); + assert.equal(element._content, 'old content'); + assert.equal(element._type, 'text/javascript'); + }); + }); + + test('local edit exists, is same as remote edit', () => { + sandbox.stub(element.$.storage, 'getEditableContentItem') + .returns({message: 'pending edit'}); + sandbox.stub(element.$.restAPI, 'getFileContent') + .returns(Promise.resolve({ + ok: true, + type: 'text/javascript', + content: 'pending edit', + })); + + const alertStub = sandbox.stub(); + element.addEventListener('show-alert', alertStub); + + return element._getFileData(1, 'test', 1).then(() => { + flushAsynchronousOperations(); + + assert.isFalse(alertStub.called); + assert.equal(element._newContent, 'pending edit'); + assert.equal(element._content, 'pending edit'); + assert.equal(element._type, 'text/javascript'); + }); + }); + + test('storage key computation', () => { + element._changeNum = 1; + element._patchNum = 1; + element._path = 'test'; + assert.equal(element.storageKey, 'c1_ps1_test'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js index ea5a180..785e8f9 100644 --- a/polygerrit-ui/app/elements/gr-app-element.js +++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -14,531 +14,568 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../scripts/util.js'; +import '../scripts/bundled-polymer.js'; +import '../behaviors/base-url-behavior/base-url-behavior.js'; +import '../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../styles/shared-styles.js'; +import '../styles/themes/app-theme.js'; +import './admin/gr-admin-view/gr-admin-view.js'; +import './documentation/gr-documentation-search/gr-documentation-search.js'; +import './change-list/gr-change-list-view/gr-change-list-view.js'; +import './change-list/gr-dashboard-view/gr-dashboard-view.js'; +import './change/gr-change-view/gr-change-view.js'; +import './core/gr-error-manager/gr-error-manager.js'; +import './core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.js'; +import './core/gr-main-header/gr-main-header.js'; +import './core/gr-navigation/gr-navigation.js'; +import './core/gr-reporting/gr-reporting.js'; +import './core/gr-router/gr-router.js'; +import './core/gr-smart-search/gr-smart-search.js'; +import './diff/gr-diff-view/gr-diff-view.js'; +import './edit/gr-editor-view/gr-editor-view.js'; +import './plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './plugins/gr-endpoint-param/gr-endpoint-param.js'; +import './plugins/gr-external-style/gr-external-style.js'; +import './plugins/gr-plugin-host/gr-plugin-host.js'; +import './settings/gr-cla-view/gr-cla-view.js'; +import './settings/gr-registration-dialog/gr-registration-dialog.js'; +import './settings/gr-settings-view/gr-settings-view.js'; +import './shared/gr-fixed-panel/gr-fixed-panel.js'; +import './shared/gr-lib-loader/gr-lib-loader.js'; +import './shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import moment from 'moment/src/moment.js'; +self.moment = moment; +import {htmlTemplate} from './gr-app-element_html.js'; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrAppElement extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-app-element'; } /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when the URL location changes. + * + * @event location-change */ - class GrAppElement extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-app-element'; } - /** - * Fired when the URL location changes. - * - * @event location-change - */ - static get properties() { - return { + static get properties() { + return { + /** + * @type {{ query: string, view: string, screen: string }} + */ + params: Object, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + + _account: { + type: Object, + observer: '_accountChanged', + }, + /** - * @type {{ query: string, view: string, screen: string }} + * The last time the g key was pressed in milliseconds (or a keydown event + * was handled if the key is held down). + * + * @type {number|null} */ - params: Object, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, + _lastGKeyPressTimestamp: { + type: Number, + value: null, + }, - _account: { - type: Object, - observer: '_accountChanged', - }, + /** + * @type {{ plugin: Object }} + */ + _serverConfig: Object, + _version: String, + _showChangeListView: Boolean, + _showDashboardView: Boolean, + _showChangeView: Boolean, + _showDiffView: Boolean, + _showSettingsView: Boolean, + _showAdminView: Boolean, + _showCLAView: Boolean, + _showEditorView: Boolean, + _showPluginScreen: Boolean, + _showDocumentationSearch: Boolean, + /** @type {?} */ + _viewState: Object, + /** @type {?} */ + _lastError: Object, + _lastSearchPage: String, + _path: String, + _pluginScreenName: { + type: String, + computed: '_computePluginScreenName(params)', + }, + _settingsUrl: String, + _feedbackUrl: String, + // Used to allow searching on mobile + mobileSearch: { + type: Boolean, + value: false, + }, - /** - * The last time the g key was pressed in milliseconds (or a keydown event - * was handled if the key is held down). - * - * @type {number|null} - */ - _lastGKeyPressTimestamp: { - type: Number, - value: null, - }, + /** + * Other elements in app must open this URL when + * user login is required. + */ + _loginUrl: { + type: String, + value: '/login', + }, + }; + } - /** - * @type {{ plugin: Object }} - */ - _serverConfig: Object, - _version: String, - _showChangeListView: Boolean, - _showDashboardView: Boolean, - _showChangeView: Boolean, - _showDiffView: Boolean, - _showSettingsView: Boolean, - _showAdminView: Boolean, - _showCLAView: Boolean, - _showEditorView: Boolean, - _showPluginScreen: Boolean, - _showDocumentationSearch: Boolean, - /** @type {?} */ - _viewState: Object, - /** @type {?} */ - _lastError: Object, - _lastSearchPage: String, - _path: String, - _pluginScreenName: { - type: String, - computed: '_computePluginScreenName(params)', - }, - _settingsUrl: String, - _feedbackUrl: String, - // Used to allow searching on mobile - mobileSearch: { - type: Boolean, - value: false, - }, + static get observers() { + return [ + '_viewChanged(params.view)', + '_paramsChanged(params.*)', + ]; + } - /** - * Other elements in app must open this URL when - * user login is required. - */ - _loginUrl: { - type: String, - value: '/login', - }, - }; - } + keyboardShortcuts() { + return { + [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts', + [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard', + [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges', + [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges', + [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges', + [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges', + }; + } - static get observers() { - return [ - '_viewChanged(params.view)', - '_paramsChanged(params.*)', - ]; - } + /** @override */ + created() { + super.created(); + this._bindKeyboardShortcuts(); + this.addEventListener('page-error', + e => this._handlePageError(e)); + this.addEventListener('title-change', + e => this._handleTitleChange(e)); + this.addEventListener('location-change', + e => this._handleLocationChange(e)); + this.addEventListener('rpc-log', + e => this._handleRpcLog(e)); + this.addEventListener('shortcut-triggered', + e => this._handleShortcutTriggered(e)); + } - keyboardShortcuts() { - return { - [this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG]: '_showKeyboardShortcuts', - [this.Shortcut.GO_TO_USER_DASHBOARD]: '_goToUserDashboard', - [this.Shortcut.GO_TO_OPENED_CHANGES]: '_goToOpenedChanges', - [this.Shortcut.GO_TO_MERGED_CHANGES]: '_goToMergedChanges', - [this.Shortcut.GO_TO_ABANDONED_CHANGES]: '_goToAbandonedChanges', - [this.Shortcut.GO_TO_WATCHED_CHANGES]: '_goToWatchedChanges', - }; - } + /** @override */ + ready() { + super.ready(); + this._updateLoginUrl(); + this.$.reporting.appStarted(); + this.$.router.start(); - /** @override */ - created() { - super.created(); - this._bindKeyboardShortcuts(); - this.addEventListener('page-error', - e => this._handlePageError(e)); - this.addEventListener('title-change', - e => this._handleTitleChange(e)); - this.addEventListener('location-change', - e => this._handleLocationChange(e)); - this.addEventListener('rpc-log', - e => this._handleRpcLog(e)); - this.addEventListener('shortcut-triggered', - e => this._handleShortcutTriggered(e)); - } + this.$.restAPI.getAccount().then(account => { + this._account = account; + }); + this.$.restAPI.getConfig().then(config => { + this._serverConfig = config; - /** @override */ - ready() { - super.ready(); - this._updateLoginUrl(); - this.$.reporting.appStarted(); - this.$.router.start(); - - this.$.restAPI.getAccount().then(account => { - this._account = account; - }); - this.$.restAPI.getConfig().then(config => { - this._serverConfig = config; - - if (config && config.gerrit && config.gerrit.report_bug_url) { - this._feedbackUrl = config.gerrit.report_bug_url; - } - }); - this.$.restAPI.getVersion().then(version => { - this._version = version; - this._logWelcome(); - }); - - if (window.localStorage.getItem('dark-theme')) { - // No need to add the style module to element again as it's imported - // by importHref already - this.$.libLoader.getDarkTheme(); + if (config && config.gerrit && config.gerrit.report_bug_url) { + this._feedbackUrl = config.gerrit.report_bug_url; } + }); + this.$.restAPI.getVersion().then(version => { + this._version = version; + this._logWelcome(); + }); - // Note: this is evaluated here to ensure that it only happens after the - // router has been initialized. @see Issue 7837 - this._settingsUrl = Gerrit.Nav.getUrlForSettings(); - - this._viewState = { - changeView: { - changeNum: null, - patchRange: null, - selectedFileIndex: 0, - showReplyDialog: false, - diffMode: null, - numFilesShown: null, - scrollTop: 0, - }, - changeListView: { - query: null, - offset: 0, - selectedChangeIndex: 0, - }, - dashboardView: { - selectedChangeIndex: 0, - }, - }; + if (window.localStorage.getItem('dark-theme')) { + // No need to add the style module to element again as it's imported + // by importHref already + this.$.libLoader.getDarkTheme(); } - _bindKeyboardShortcuts() { - this.bindShortcut(this.Shortcut.SEND_REPLY, - this.DOC_ONLY, 'ctrl+enter', 'meta+enter'); - this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN, - this.DOC_ONLY, ':'); + // Note: this is evaluated here to ensure that it only happens after the + // router has been initialized. @see Issue 7837 + this._settingsUrl = Gerrit.Nav.getUrlForSettings(); - this.bindShortcut( - this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?'); - this.bindShortcut( - this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i'); - this.bindShortcut( - this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o'); - this.bindShortcut( - this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm'); - this.bindShortcut( - this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a'); - this.bindShortcut( - this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w'); + this._viewState = { + changeView: { + changeNum: null, + patchRange: null, + selectedFileIndex: 0, + showReplyDialog: false, + diffMode: null, + numFilesShown: null, + scrollTop: 0, + }, + changeListView: { + query: null, + offset: 0, + selectedChangeIndex: 0, + }, + dashboardView: { + selectedChangeIndex: 0, + }, + }; + } - this.bindShortcut( - this.Shortcut.CURSOR_NEXT_CHANGE, 'j'); - this.bindShortcut( - this.Shortcut.CURSOR_PREV_CHANGE, 'k'); - this.bindShortcut( - this.Shortcut.OPEN_CHANGE, 'o'); - this.bindShortcut( - this.Shortcut.NEXT_PAGE, 'n', ']'); - this.bindShortcut( - this.Shortcut.PREV_PAGE, 'p', '['); - this.bindShortcut( - this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup'); - this.bindShortcut( - this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup'); - this.bindShortcut( - this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup'); - this.bindShortcut( - this.Shortcut.EDIT_TOPIC, 't'); + _bindKeyboardShortcuts() { + this.bindShortcut(this.Shortcut.SEND_REPLY, + this.DOC_ONLY, 'ctrl+enter', 'meta+enter'); + this.bindShortcut(this.Shortcut.EMOJI_DROPDOWN, + this.DOC_ONLY, ':'); - this.bindShortcut( - this.Shortcut.OPEN_REPLY_DIALOG, 'a'); - this.bindShortcut( - this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd'); - this.bindShortcut( - this.Shortcut.EXPAND_ALL_MESSAGES, 'x'); - this.bindShortcut( - this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z'); - this.bindShortcut( - this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup'); - this.bindShortcut( - this.Shortcut.UP_TO_DASHBOARD, 'u'); - this.bindShortcut( - this.Shortcut.UP_TO_CHANGE, 'u'); - this.bindShortcut( - this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup'); + this.bindShortcut( + this.Shortcut.OPEN_SHORTCUT_HELP_DIALOG, '?'); + this.bindShortcut( + this.Shortcut.GO_TO_USER_DASHBOARD, this.GO_KEY, 'i'); + this.bindShortcut( + this.Shortcut.GO_TO_OPENED_CHANGES, this.GO_KEY, 'o'); + this.bindShortcut( + this.Shortcut.GO_TO_MERGED_CHANGES, this.GO_KEY, 'm'); + this.bindShortcut( + this.Shortcut.GO_TO_ABANDONED_CHANGES, this.GO_KEY, 'a'); + this.bindShortcut( + this.Shortcut.GO_TO_WATCHED_CHANGES, this.GO_KEY, 'w'); - this.bindShortcut( - this.Shortcut.NEXT_LINE, 'j', 'down'); - this.bindShortcut( - this.Shortcut.PREV_LINE, 'k', 'up'); - if (this._isCursorManagerSupportMoveToVisibleLine()) { - this.bindShortcut( - this.Shortcut.VISIBLE_LINE, '.'); - } - this.bindShortcut( - this.Shortcut.NEXT_CHUNK, 'n'); - this.bindShortcut( - this.Shortcut.PREV_CHUNK, 'p'); - this.bindShortcut( - this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x'); - this.bindShortcut( - this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n'); - this.bindShortcut( - this.Shortcut.PREV_COMMENT_THREAD, 'shift+p'); - this.bindShortcut( - this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e'); - this.bindShortcut( - this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, - this.DOC_ONLY, 'shift+e'); - this.bindShortcut( - this.Shortcut.LEFT_PANE, 'shift+left'); - this.bindShortcut( - this.Shortcut.RIGHT_PANE, 'shift+right'); - this.bindShortcut( - this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); - this.bindShortcut( - this.Shortcut.NEW_COMMENT, 'c'); - this.bindShortcut( - this.Shortcut.SAVE_COMMENT, - 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s'); - this.bindShortcut( - this.Shortcut.OPEN_DIFF_PREFS, ','); - this.bindShortcut( - this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup'); + this.bindShortcut( + this.Shortcut.CURSOR_NEXT_CHANGE, 'j'); + this.bindShortcut( + this.Shortcut.CURSOR_PREV_CHANGE, 'k'); + this.bindShortcut( + this.Shortcut.OPEN_CHANGE, 'o'); + this.bindShortcut( + this.Shortcut.NEXT_PAGE, 'n', ']'); + this.bindShortcut( + this.Shortcut.PREV_PAGE, 'p', '['); + this.bindShortcut( + this.Shortcut.TOGGLE_CHANGE_REVIEWED, 'r:keyup'); + this.bindShortcut( + this.Shortcut.TOGGLE_CHANGE_STAR, 's:keyup'); + this.bindShortcut( + this.Shortcut.REFRESH_CHANGE_LIST, 'shift+r:keyup'); + this.bindShortcut( + this.Shortcut.EDIT_TOPIC, 't'); - this.bindShortcut( - this.Shortcut.NEXT_FILE, ']'); - this.bindShortcut( - this.Shortcut.PREV_FILE, '['); - this.bindShortcut( - this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j'); - this.bindShortcut( - this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k'); - this.bindShortcut( - this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down'); - this.bindShortcut( - this.Shortcut.CURSOR_PREV_FILE, 'k', 'up'); - this.bindShortcut( - this.Shortcut.OPEN_FILE, 'o', 'enter'); - this.bindShortcut( - this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup'); - this.bindShortcut( - this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m'); - this.bindShortcut( - this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup'); - this.bindShortcut( - this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup'); - this.bindShortcut( - this.Shortcut.TOGGLE_BLAME, 'b'); + this.bindShortcut( + this.Shortcut.OPEN_REPLY_DIALOG, 'a'); + this.bindShortcut( + this.Shortcut.OPEN_DOWNLOAD_DIALOG, 'd'); + this.bindShortcut( + this.Shortcut.EXPAND_ALL_MESSAGES, 'x'); + this.bindShortcut( + this.Shortcut.COLLAPSE_ALL_MESSAGES, 'z'); + this.bindShortcut( + this.Shortcut.REFRESH_CHANGE, 'shift+r:keyup'); + this.bindShortcut( + this.Shortcut.UP_TO_DASHBOARD, 'u'); + this.bindShortcut( + this.Shortcut.UP_TO_CHANGE, 'u'); + this.bindShortcut( + this.Shortcut.TOGGLE_DIFF_MODE, 'm:keyup'); + this.bindShortcut( + this.Shortcut.NEXT_LINE, 'j', 'down'); + this.bindShortcut( + this.Shortcut.PREV_LINE, 'k', 'up'); + if (this._isCursorManagerSupportMoveToVisibleLine()) { this.bindShortcut( - this.Shortcut.OPEN_FIRST_FILE, ']'); - this.bindShortcut( - this.Shortcut.OPEN_LAST_FILE, '['); - - this.bindShortcut( - this.Shortcut.SEARCH, '/'); + this.Shortcut.VISIBLE_LINE, '.'); } + this.bindShortcut( + this.Shortcut.NEXT_CHUNK, 'n'); + this.bindShortcut( + this.Shortcut.PREV_CHUNK, 'p'); + this.bindShortcut( + this.Shortcut.EXPAND_ALL_DIFF_CONTEXT, 'shift+x'); + this.bindShortcut( + this.Shortcut.NEXT_COMMENT_THREAD, 'shift+n'); + this.bindShortcut( + this.Shortcut.PREV_COMMENT_THREAD, 'shift+p'); + this.bindShortcut( + this.Shortcut.EXPAND_ALL_COMMENT_THREADS, this.DOC_ONLY, 'e'); + this.bindShortcut( + this.Shortcut.COLLAPSE_ALL_COMMENT_THREADS, + this.DOC_ONLY, 'shift+e'); + this.bindShortcut( + this.Shortcut.LEFT_PANE, 'shift+left'); + this.bindShortcut( + this.Shortcut.RIGHT_PANE, 'shift+right'); + this.bindShortcut( + this.Shortcut.TOGGLE_LEFT_PANE, 'shift+a'); + this.bindShortcut( + this.Shortcut.NEW_COMMENT, 'c'); + this.bindShortcut( + this.Shortcut.SAVE_COMMENT, + 'ctrl+enter', 'meta+enter', 'ctrl+s', 'meta+s'); + this.bindShortcut( + this.Shortcut.OPEN_DIFF_PREFS, ','); + this.bindShortcut( + this.Shortcut.TOGGLE_DIFF_REVIEWED, 'r:keyup'); - _isCursorManagerSupportMoveToVisibleLine() { - // This method is a copy-paste from the - // method _isIntersectionObserverSupported of gr-cursor-manager.js - // It is better share this method with gr-cursor-manager, - // but doing it require a lot if changes instead of 1-line copied code - return 'IntersectionObserver' in window; + this.bindShortcut( + this.Shortcut.NEXT_FILE, ']'); + this.bindShortcut( + this.Shortcut.PREV_FILE, '['); + this.bindShortcut( + this.Shortcut.NEXT_FILE_WITH_COMMENTS, 'shift+j'); + this.bindShortcut( + this.Shortcut.PREV_FILE_WITH_COMMENTS, 'shift+k'); + this.bindShortcut( + this.Shortcut.CURSOR_NEXT_FILE, 'j', 'down'); + this.bindShortcut( + this.Shortcut.CURSOR_PREV_FILE, 'k', 'up'); + this.bindShortcut( + this.Shortcut.OPEN_FILE, 'o', 'enter'); + this.bindShortcut( + this.Shortcut.TOGGLE_FILE_REVIEWED, 'r:keyup'); + this.bindShortcut( + this.Shortcut.NEXT_UNREVIEWED_FILE, 'shift+m'); + this.bindShortcut( + this.Shortcut.TOGGLE_ALL_INLINE_DIFFS, 'shift+i:keyup'); + this.bindShortcut( + this.Shortcut.TOGGLE_INLINE_DIFF, 'i:keyup'); + this.bindShortcut( + this.Shortcut.TOGGLE_BLAME, 'b'); + + this.bindShortcut( + this.Shortcut.OPEN_FIRST_FILE, ']'); + this.bindShortcut( + this.Shortcut.OPEN_LAST_FILE, '['); + + this.bindShortcut( + this.Shortcut.SEARCH, '/'); + } + + _isCursorManagerSupportMoveToVisibleLine() { + // This method is a copy-paste from the + // method _isIntersectionObserverSupported of gr-cursor-manager.js + // It is better share this method with gr-cursor-manager, + // but doing it require a lot if changes instead of 1-line copied code + return 'IntersectionObserver' in window; + } + + _accountChanged(account) { + if (!account) { return; } + + // Preferences are cached when a user is logged in; warm them. + this.$.restAPI.getPreferences(); + this.$.restAPI.getDiffPreferences(); + this.$.restAPI.getEditPreferences(); + this.$.errorManager.knownAccountId = + this._account && this._account._account_id || null; + } + + _viewChanged(view) { + this.$.errorView.classList.remove('show'); + this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH); + this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD); + this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE); + this.set('_showDiffView', view === Gerrit.Nav.View.DIFF); + this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS); + this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN || + view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO); + this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS); + this.set('_showEditorView', view === Gerrit.Nav.View.EDIT); + const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN; + this.set('_showPluginScreen', false); + // Navigation within plugin screens does not restamp gr-endpoint-decorator + // because _showPluginScreen value does not change. To force restamp, + // change _showPluginScreen value between true and false. + if (isPluginScreen) { + this.async(() => this.set('_showPluginScreen', true), 1); } - - _accountChanged(account) { - if (!account) { return; } - - // Preferences are cached when a user is logged in; warm them. - this.$.restAPI.getPreferences(); - this.$.restAPI.getDiffPreferences(); - this.$.restAPI.getEditPreferences(); - this.$.errorManager.knownAccountId = - this._account && this._account._account_id || null; - } - - _viewChanged(view) { - this.$.errorView.classList.remove('show'); - this.set('_showChangeListView', view === Gerrit.Nav.View.SEARCH); - this.set('_showDashboardView', view === Gerrit.Nav.View.DASHBOARD); - this.set('_showChangeView', view === Gerrit.Nav.View.CHANGE); - this.set('_showDiffView', view === Gerrit.Nav.View.DIFF); - this.set('_showSettingsView', view === Gerrit.Nav.View.SETTINGS); - this.set('_showAdminView', view === Gerrit.Nav.View.ADMIN || - view === Gerrit.Nav.View.GROUP || view === Gerrit.Nav.View.REPO); - this.set('_showCLAView', view === Gerrit.Nav.View.AGREEMENTS); - this.set('_showEditorView', view === Gerrit.Nav.View.EDIT); - const isPluginScreen = view === Gerrit.Nav.View.PLUGIN_SCREEN; - this.set('_showPluginScreen', false); - // Navigation within plugin screens does not restamp gr-endpoint-decorator - // because _showPluginScreen value does not change. To force restamp, - // change _showPluginScreen value between true and false. - if (isPluginScreen) { - this.async(() => this.set('_showPluginScreen', true), 1); - } - this.set('_showDocumentationSearch', - view === Gerrit.Nav.View.DOCUMENTATION_SEARCH); - if (this.params.justRegistered) { - this.$.registrationOverlay.open(); - this.$.registrationDialog.loadData().then(() => { - this.$.registrationOverlay.refit(); - }); - } - this.$.header.unfloat(); - } - - _handleShortcutTriggered(event) { - const {event: e, goKey} = event.detail; - // eg: {key: "k:keydown", ..., from: "gr-diff-view"} - let key = `${e.key}:${e.type}`; - if (goKey) key = 'g+' + key; - if (e.shiftKey) key = 'shift+' + key; - if (e.ctrlKey) key = 'ctrl+' + key; - if (e.metaKey) key = 'meta+' + key; - if (e.altKey) key = 'alt+' + key; - this.$.reporting.reportInteraction('shortcut-triggered', { - key, - from: event.path && event.path[0] - && event.path[0].nodeName || 'unknown', + this.set('_showDocumentationSearch', + view === Gerrit.Nav.View.DOCUMENTATION_SEARCH); + if (this.params.justRegistered) { + this.$.registrationOverlay.open(); + this.$.registrationDialog.loadData().then(() => { + this.$.registrationOverlay.refit(); }); } + this.$.header.unfloat(); + } - _handlePageError(e) { - const props = [ - '_showChangeListView', - '_showDashboardView', - '_showChangeView', - '_showDiffView', - '_showSettingsView', - '_showAdminView', - ]; - for (const showProp of props) { - this.set(showProp, false); - } + _handleShortcutTriggered(event) { + const {event: e, goKey} = event.detail; + // eg: {key: "k:keydown", ..., from: "gr-diff-view"} + let key = `${e.key}:${e.type}`; + if (goKey) key = 'g+' + key; + if (e.shiftKey) key = 'shift+' + key; + if (e.ctrlKey) key = 'ctrl+' + key; + if (e.metaKey) key = 'meta+' + key; + if (e.altKey) key = 'alt+' + key; + this.$.reporting.reportInteraction('shortcut-triggered', { + key, + from: event.path && event.path[0] + && event.path[0].nodeName || 'unknown', + }); + } - this.$.errorView.classList.add('show'); - const response = e.detail.response; - const err = {text: [response.status, response.statusText].join(' ')}; - if (response.status === 404) { - err.emoji = '¯\\_(ツ)_/¯'; + _handlePageError(e) { + const props = [ + '_showChangeListView', + '_showDashboardView', + '_showChangeView', + '_showDiffView', + '_showSettingsView', + '_showAdminView', + ]; + for (const showProp of props) { + this.set(showProp, false); + } + + this.$.errorView.classList.add('show'); + const response = e.detail.response; + const err = {text: [response.status, response.statusText].join(' ')}; + if (response.status === 404) { + err.emoji = '¯\\_(ツ)_/¯'; + this._lastError = err; + } else { + err.emoji = 'o_O'; + response.text().then(text => { + err.moreInfo = text; this._lastError = err; - } else { - err.emoji = 'o_O'; - response.text().then(text => { - err.moreInfo = text; - this._lastError = err; - }); - } - } - - _handleLocationChange(e) { - this._updateLoginUrl(); - - const hash = e.detail.hash.substring(1); - let pathname = e.detail.pathname; - if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) { - pathname += '@' + hash; - } - this.set('_path', pathname); - } - - _updateLoginUrl() { - const baseUrl = this.getBaseUrl(); - if (baseUrl) { - // Strip the canonical path from the path since needing canonical in - // the path is uneeded and breaks the url. - this._loginUrl = baseUrl + '/login/' + encodeURIComponent( - '/' + window.location.pathname.substring(baseUrl.length) + - window.location.search + - window.location.hash); - } else { - this._loginUrl = '/login/' + encodeURIComponent( - window.location.pathname + - window.location.search + - window.location.hash); - } - } - - _paramsChanged(paramsRecord) { - const params = paramsRecord.base; - const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD]; - if (viewsToCheck.includes(params.view)) { - this.set('_lastSearchPage', location.pathname); - } - } - - _handleTitleChange(e) { - if (e.detail.title) { - document.title = e.detail.title + ' · Gerrit Code Review'; - } else { - document.title = ''; - } - } - - _showKeyboardShortcuts(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - this.$.keyboardShortcuts.open(); - } - - _handleKeyboardShortcutDialogClose() { - this.$.keyboardShortcuts.close(); - } - - _handleAccountDetailUpdate(e) { - this.$.mainHeader.reload(); - if (this.params.view === Gerrit.Nav.View.SETTINGS) { - this.shadowRoot.querySelector('gr-settings-view').reloadAccountDetail(); - } - } - - _handleRegistrationDialogClose(e) { - this.params.justRegistered = false; - this.$.registrationOverlay.close(); - } - - _goToOpenedChanges() { - Gerrit.Nav.navigateToStatusSearch('open'); - } - - _goToUserDashboard() { - Gerrit.Nav.navigateToUserDashboard(); - } - - _goToMergedChanges() { - Gerrit.Nav.navigateToStatusSearch('merged'); - } - - _goToAbandonedChanges() { - Gerrit.Nav.navigateToStatusSearch('abandoned'); - } - - _goToWatchedChanges() { - // The query is hardcoded, and doesn't respect custom menu entries - Gerrit.Nav.navigateToSearchQuery('is:watched is:open'); - } - - _computePluginScreenName({plugin, screen}) { - if (!plugin || !screen) return ''; - return `${plugin}-screen-${screen}`; - } - - _logWelcome() { - console.group('Runtime Info'); - console.log('Gerrit UI (PolyGerrit)'); - console.log(`Gerrit Server Version: ${this._version}`); - if (window.VERSION_INFO) { - console.log(`UI Version Info: ${window.VERSION_INFO}`); - } - if (this._feedbackUrl) { - console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`); - } - console.groupEnd(); - } - - /** - * Intercept RPC log events emitted by REST API interfaces. - * Note: the REST API interface cannot use gr-reporting directly because - * that would create a cyclic dependency. - */ - _handleRpcLog(e) { - this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl, - e.detail.elapsed); - } - - _mobileSearchToggle(e) { - this.mobileSearch = !this.mobileSearch; - } - - getThemeEndpoint() { - // For now, we only have dark mode and light mode - return window.localStorage.getItem('dark-theme') ? - 'app-theme-dark' : - 'app-theme-light'; + }); } } - customElements.define(GrAppElement.is, GrAppElement); -})(); + _handleLocationChange(e) { + this._updateLoginUrl(); + + const hash = e.detail.hash.substring(1); + let pathname = e.detail.pathname; + if (pathname.startsWith('/c/') && parseInt(hash, 10) > 0) { + pathname += '@' + hash; + } + this.set('_path', pathname); + } + + _updateLoginUrl() { + const baseUrl = this.getBaseUrl(); + if (baseUrl) { + // Strip the canonical path from the path since needing canonical in + // the path is uneeded and breaks the url. + this._loginUrl = baseUrl + '/login/' + encodeURIComponent( + '/' + window.location.pathname.substring(baseUrl.length) + + window.location.search + + window.location.hash); + } else { + this._loginUrl = '/login/' + encodeURIComponent( + window.location.pathname + + window.location.search + + window.location.hash); + } + } + + _paramsChanged(paramsRecord) { + const params = paramsRecord.base; + const viewsToCheck = [Gerrit.Nav.View.SEARCH, Gerrit.Nav.View.DASHBOARD]; + if (viewsToCheck.includes(params.view)) { + this.set('_lastSearchPage', location.pathname); + } + } + + _handleTitleChange(e) { + if (e.detail.title) { + document.title = e.detail.title + ' · Gerrit Code Review'; + } else { + document.title = ''; + } + } + + _showKeyboardShortcuts(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + this.$.keyboardShortcuts.open(); + } + + _handleKeyboardShortcutDialogClose() { + this.$.keyboardShortcuts.close(); + } + + _handleAccountDetailUpdate(e) { + this.$.mainHeader.reload(); + if (this.params.view === Gerrit.Nav.View.SETTINGS) { + this.shadowRoot.querySelector('gr-settings-view').reloadAccountDetail(); + } + } + + _handleRegistrationDialogClose(e) { + this.params.justRegistered = false; + this.$.registrationOverlay.close(); + } + + _goToOpenedChanges() { + Gerrit.Nav.navigateToStatusSearch('open'); + } + + _goToUserDashboard() { + Gerrit.Nav.navigateToUserDashboard(); + } + + _goToMergedChanges() { + Gerrit.Nav.navigateToStatusSearch('merged'); + } + + _goToAbandonedChanges() { + Gerrit.Nav.navigateToStatusSearch('abandoned'); + } + + _goToWatchedChanges() { + // The query is hardcoded, and doesn't respect custom menu entries + Gerrit.Nav.navigateToSearchQuery('is:watched is:open'); + } + + _computePluginScreenName({plugin, screen}) { + if (!plugin || !screen) return ''; + return `${plugin}-screen-${screen}`; + } + + _logWelcome() { + console.group('Runtime Info'); + console.log('Gerrit UI (PolyGerrit)'); + console.log(`Gerrit Server Version: ${this._version}`); + if (window.VERSION_INFO) { + console.log(`UI Version Info: ${window.VERSION_INFO}`); + } + if (this._feedbackUrl) { + console.log(`Please file bugs and feedback at: ${this._feedbackUrl}`); + } + console.groupEnd(); + } + + /** + * Intercept RPC log events emitted by REST API interfaces. + * Note: the REST API interface cannot use gr-reporting directly because + * that would create a cyclic dependency. + */ + _handleRpcLog(e) { + this.$.reporting.reportRpcTiming(e.detail.anonymizedUrl, + e.detail.elapsed); + } + + _mobileSearchToggle(e) { + this.mobileSearch = !this.mobileSearch; + } + + getThemeEndpoint() { + // For now, we only have dark mode and light mode + return window.localStorage.getItem('dark-theme') ? + 'app-theme-dark' : + 'app-theme-light'; + } +} + +customElements.define(GrAppElement.is, GrAppElement);
diff --git a/polygerrit-ui/app/elements/gr-app-element_html.js b/polygerrit-ui/app/elements/gr-app-element_html.js index 62f2967..3951d9d 100644 --- a/polygerrit-ui/app/elements/gr-app-element_html.js +++ b/polygerrit-ui/app/elements/gr-app-element_html.js
@@ -1,54 +1,22 @@ -<!-- -@license -Copyright (C) 2019 The Android Open Source Project +/** + * @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.js'; -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. ---> -<script src="/bower_components/moment/moment.js"></script> -<script src="../scripts/util.js"></script> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../styles/shared-styles.html"> -<link rel="import" href="../styles/themes/app-theme.html"> -<link rel="import" href="./admin/gr-admin-view/gr-admin-view.html"> -<link rel="import" href="./documentation/gr-documentation-search/gr-documentation-search.html"> -<link rel="import" href="./change-list/gr-change-list-view/gr-change-list-view.html"> -<link rel="import" href="./change-list/gr-dashboard-view/gr-dashboard-view.html"> -<link rel="import" href="./change/gr-change-view/gr-change-view.html"> -<link rel="import" href="./core/gr-error-manager/gr-error-manager.html"> -<link rel="import" href="./core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html"> -<link rel="import" href="./core/gr-main-header/gr-main-header.html"> -<link rel="import" href="./core/gr-navigation/gr-navigation.html"> -<link rel="import" href="./core/gr-reporting/gr-reporting.html"> -<link rel="import" href="./core/gr-router/gr-router.html"> -<link rel="import" href="./core/gr-smart-search/gr-smart-search.html"> -<link rel="import" href="./diff/gr-diff-view/gr-diff-view.html"> -<link rel="import" href="./edit/gr-editor-view/gr-editor-view.html"> -<link rel="import" href="./plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="./plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="./plugins/gr-external-style/gr-external-style.html"> -<link rel="import" href="./plugins/gr-plugin-host/gr-plugin-host.html"> -<link rel="import" href="./settings/gr-cla-view/gr-cla-view.html"> -<link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html"> -<link rel="import" href="./settings/gr-settings-view/gr-settings-view.html"> -<link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html"> -<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html"> -<link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-app-element"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background-color: var(--background-color-tertiary); @@ -126,56 +94,33 @@ </style> <gr-endpoint-decorator name="banner"></gr-endpoint-decorator> <gr-fixed-panel id="header"> - <gr-main-header - id="mainHeader" - search-query="{{params.query}}" - on-mobile-search="_mobileSearchToggle" - login-url="[[_loginUrl]]" - > + <gr-main-header id="mainHeader" search-query="{{params.query}}" on-mobile-search="_mobileSearchToggle" login-url="[[_loginUrl]]"> </gr-main-header> </gr-fixed-panel> <main> - <gr-smart-search - id="search" - search-query="{{params.query}}" - hidden="[[!mobileSearch]]"> + <gr-smart-search id="search" search-query="{{params.query}}" hidden="[[!mobileSearch]]"> </gr-smart-search> <template is="dom-if" if="[[_showChangeListView]]" restamp="true"> - <gr-change-list-view - params="[[params]]" - account="[[_account]]" - view-state="{{_viewState.changeListView}}"></gr-change-list-view> + <gr-change-list-view params="[[params]]" account="[[_account]]" view-state="{{_viewState.changeListView}}"></gr-change-list-view> </template> <template is="dom-if" if="[[_showDashboardView]]" restamp="true"> - <gr-dashboard-view - account="[[_account]]" - params="[[params]]" - view-state="{{_viewState.dashboardView}}"></gr-dashboard-view> + <gr-dashboard-view account="[[_account]]" params="[[params]]" view-state="{{_viewState.dashboardView}}"></gr-dashboard-view> </template> <template is="dom-if" if="[[_showChangeView]]" restamp="true"> - <gr-change-view - params="[[params]]" - view-state="{{_viewState.changeView}}" - back-page="[[_lastSearchPage]]"></gr-change-view> + <gr-change-view params="[[params]]" view-state="{{_viewState.changeView}}" back-page="[[_lastSearchPage]]"></gr-change-view> </template> <template is="dom-if" if="[[_showEditorView]]" restamp="true"> - <gr-editor-view - params="[[params]]"></gr-editor-view> + <gr-editor-view params="[[params]]"></gr-editor-view> </template> <template is="dom-if" if="[[_showDiffView]]" restamp="true"> - <gr-diff-view - params="[[params]]" - change-view-state="{{_viewState.changeView}}"></gr-diff-view> + <gr-diff-view params="[[params]]" change-view-state="{{_viewState.changeView}}"></gr-diff-view> </template> <template is="dom-if" if="[[_showSettingsView]]" restamp="true"> - <gr-settings-view - params="[[params]]" - on-account-detail-update="_handleAccountDetailUpdate"> + <gr-settings-view params="[[params]]" on-account-detail-update="_handleAccountDetailUpdate"> </gr-settings-view> </template> <template is="dom-if" if="[[_showAdminView]]" restamp="true"> - <gr-admin-view path="[[_path]]" - params=[[params]]></gr-admin-view> + <gr-admin-view path="[[_path]]" params="[[params]]"></gr-admin-view> </template> <template is="dom-if" if="[[_showPluginScreen]]" restamp="true"> <gr-endpoint-decorator name="[[_pluginScreenName]]"> @@ -186,8 +131,7 @@ <gr-cla-view></gr-cla-view> </template> <template is="dom-if" if="[[_showDocumentationSearch]]" restamp="true"> - <gr-documentation-search - params="[[params]]"> + <gr-documentation-search params="[[params]]"> </gr-documentation-search> </template> <div id="errorView" class="errorView"> @@ -198,32 +142,23 @@ </main> <footer r="contentinfo"> <div> - Powered by <a href="https://www.gerritcodereview.com/" rel="noopener" - target="_blank">Gerrit Code Review</a> + Powered by <a href="https://www.gerritcodereview.com/" rel="noopener" target="_blank">Gerrit Code Review</a> ([[_version]]) <gr-endpoint-decorator name="footer-left"></gr-endpoint-decorator> </div> <div> <template is="dom-if" if="[[_feedbackUrl]]"> - <a class="feedback" - href$="[[_feedbackUrl]]" - rel="noopener" - target="_blank">Report bug</a> | + <a class="feedback" href\$="[[_feedbackUrl]]" rel="noopener" target="_blank">Report bug</a> | </template> - Press “?” for keyboard shortcuts + Press “?” for keyboard shortcuts <gr-endpoint-decorator name="footer-right"></gr-endpoint-decorator> </div> </footer> - <gr-overlay id="keyboardShortcuts" with-backdrop> - <gr-keyboard-shortcuts-dialog - on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog> + <gr-overlay id="keyboardShortcuts" with-backdrop=""> + <gr-keyboard-shortcuts-dialog on-close="_handleKeyboardShortcutDialogClose"></gr-keyboard-shortcuts-dialog> </gr-overlay> - <gr-overlay id="registrationOverlay" with-backdrop> - <gr-registration-dialog - id="registrationDialog" - settings-url="[[_settingsUrl]]" - on-account-detail-update="_handleAccountDetailUpdate" - on-close="_handleRegistrationDialogClose"> + <gr-overlay id="registrationOverlay" with-backdrop=""> + <gr-registration-dialog id="registrationDialog" settings-url="[[_settingsUrl]]" on-account-detail-update="_handleAccountDetailUpdate" on-close="_handleRegistrationDialogClose"> </gr-registration-dialog> </gr-overlay> <gr-endpoint-decorator name="plugin-overlay"></gr-endpoint-decorator> @@ -231,12 +166,9 @@ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-reporting id="reporting"></gr-reporting> <gr-router id="router"></gr-router> - <gr-plugin-host id="plugins" - config="[[_serverConfig]]"> + <gr-plugin-host id="plugins" config="[[_serverConfig]]"> </gr-plugin-host> <gr-lib-loader id="libLoader"></gr-lib-loader> <gr-external-style id="externalStyleForAll" name="app-theme"></gr-external-style> <gr-external-style id="externalStyleForTheme" name="[[getThemeEndpoint()]]"></gr-external-style> - </template> - <script src="gr-app-element.js" crossorigin="anonymous"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html new file mode 100644 index 0000000..1483f7a --- /dev/null +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -0,0 +1 @@ +<script src='./gr-app.js' type='module'></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index da54ac4..ac5a04d 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,15 +14,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +/* TODO(taoalpha): Remove once all legacyUndefinedCheck removed. */ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +import './gr-app-init.js'; - /** @extends Polymer.Element */ - class GrApp extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-app'; } - } +import './font-roboto-local-loader.js'; +import '../scripts/bundled-polymer.js'; +import 'polymer-resin/standalone/polymer-resin.js'; +import '../behaviors/safe-types-behavior/safe-types-behavior.js'; +import './gr-app-element.js'; +import './change-list/gr-embed-dashboard/gr-embed-dashboard.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-app_html.js'; - customElements.define(GrApp.is, GrApp); -})(); +security.polymer_resin.install({ + allowedIdentifierPrefixes: [''], + reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER, + safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge, +}); + +/** @extends Polymer.Element */ +class GrApp extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-app'; } +} + +customElements.define(GrApp.is, GrApp);
diff --git a/polygerrit-ui/app/elements/gr-app_html.js b/polygerrit-ui/app/elements/gr-app_html.js index 2a28bc1..fcf773f 100644 --- a/polygerrit-ui/app/elements/gr-app_html.js +++ b/polygerrit-ui/app/elements/gr-app_html.js
@@ -1,38 +1,21 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> -<script src="gr-app-init.js"></script> -<script src="./font-roboto-local-loader.js" type="module" /> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/polymer-resin/standalone/polymer-resin.html"> -<!-- TODO(taoalpha): Remove once all legacyUndefinedCheck removed. --> -<link rel="import" href="../behaviors/safe-types-behavior/safe-types-behavior.html"> -<script> - security.polymer_resin.install({ - allowedIdentifierPrefixes: [''], - reportHandler: security.polymer_resin.CONSOLE_LOGGING_REPORT_HANDLER, - safeTypesBridge: Gerrit.SafeTypes.safeTypesBridge, - }); -</script> - -<link rel="import" href="./gr-app-element.html"> -<dom-module id="gr-app"> - <template> +export const htmlTemplate = html` <gr-app-element id="app-element"></gr-app-element> - </template> - <script src="gr-app.js" crossorigin="anonymous"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html index 47a8e06..447aae4 100644 --- a/polygerrit-ui/app/elements/gr-app_test.html +++ b/polygerrit-ui/app/elements/gr-app_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-app</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../test/test-pre-setup.js"></script> -<link rel="import" href="../test/common-test-setup.html"/> -<link rel="import" href="gr-app.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../test/test-pre-setup.js"></script> +<script type="module" src="../test/common-test-setup.js"></script> +<script type="module" src="./gr-app.js"></script> -<script>void(0);</script> +<script type="module"> +import '../test/test-pre-setup.js'; +import '../test/common-test-setup.js'; +import './gr-app.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,74 +40,76 @@ </template> </test-fixture> -<script> - suite('gr-app tests', async () => { - await readyToTest(); - let sandbox; - let element; +<script type="module"> +import '../test/test-pre-setup.js'; +import '../test/common-test-setup.js'; +import './gr-app.js'; +suite('gr-app tests', () => { + let sandbox; + let element; - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-reporting', { - appStarted: sandbox.stub(), - }); - stub('gr-account-dropdown', { - _getTopContent: sinon.stub(), - }); - stub('gr-router', { - start: sandbox.stub(), - }); - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve({}); }, - getAccountCapabilities() { return Promise.resolve({}); }, - getConfig() { - return Promise.resolve({ - plugin: {}, - auth: { - auth_type: undefined, - }, - }); - }, - getPreferences() { return Promise.resolve({my: []}); }, - getDiffPreferences() { return Promise.resolve({}); }, - getEditPreferences() { return Promise.resolve({}); }, - getVersion() { return Promise.resolve(42); }, - probePath() { return Promise.resolve(42); }, - }); - - element = fixture('basic'); - flush(done); + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-reporting', { + appStarted: sandbox.stub(), + }); + stub('gr-account-dropdown', { + _getTopContent: sinon.stub(), + }); + stub('gr-router', { + start: sandbox.stub(), + }); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve({}); }, + getAccountCapabilities() { return Promise.resolve({}); }, + getConfig() { + return Promise.resolve({ + plugin: {}, + auth: { + auth_type: undefined, + }, + }); + }, + getPreferences() { return Promise.resolve({my: []}); }, + getDiffPreferences() { return Promise.resolve({}); }, + getEditPreferences() { return Promise.resolve({}); }, + getVersion() { return Promise.resolve(42); }, + probePath() { return Promise.resolve(42); }, }); - teardown(() => { - sandbox.restore(); - }); + element = fixture('basic'); + flush(done); + }); - const appElement = () => element.$['app-element']; + teardown(() => { + sandbox.restore(); + }); - test('reporting', () => { - assert.isTrue(appElement().$.reporting.appStarted.calledOnce); - }); + const appElement = () => element.$['app-element']; - test('reporting called before router start', () => { - const element = appElement(); - const appStartedStub = element.$.reporting.appStarted; - const routerStartStub = element.$.router.start; - sinon.assert.callOrder(appStartedStub, routerStartStub); - }); + test('reporting', () => { + assert.isTrue(appElement().$.reporting.appStarted.calledOnce); + }); - test('passes config to gr-plugin-host', () => { - const config = appElement().$.restAPI.getConfig; - return config.lastCall.returnValue.then(config => { - assert.deepEqual(appElement().$.plugins.config, config); - }); - }); + test('reporting called before router start', () => { + const element = appElement(); + const appStartedStub = element.$.reporting.appStarted; + const routerStartStub = element.$.router.start; + sinon.assert.callOrder(appStartedStub, routerStartStub); + }); - test('_paramsChanged sets search page', () => { - appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}}); - assert.notOk(appElement()._lastSearchPage); - appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}}); - assert.ok(appElement()._lastSearchPage); + test('passes config to gr-plugin-host', () => { + const config = appElement().$.restAPI.getConfig; + return config.lastCall.returnValue.then(config => { + assert.deepEqual(appElement().$.plugins.config, config); }); }); + + test('_paramsChanged sets search page', () => { + appElement()._paramsChanged({base: {view: Gerrit.Nav.View.CHANGE}}); + assert.notOk(appElement()._lastSearchPage); + appElement()._paramsChanged({base: {view: Gerrit.Nav.View.SEARCH}}); + assert.ok(appElement()._lastSearchPage); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html deleted file mode 100644 index 756c435b..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api.html +++ /dev/null
@@ -1,18 +0,0 @@ -<!-- -@license -Copyright (C) 2018 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. ---> - -<script src="gr-admin-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html index f10f922..21e46b8 100644 --- a/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-admin-api/gr-admin-api_test.html
@@ -19,54 +19,63 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="gr-admin-api.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script> +<script type="module" src="./gr-admin-api.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import './gr-admin-api.js'; +void(0); +</script> -<script> - suite('gr-admin-api tests', async () => { - await readyToTest(); - let sandbox; - let adminApi; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import './gr-admin-api.js'; +suite('gr-admin-api tests', () => { + let sandbox; + let adminApi; - setup(() => { - sandbox = sinon.sandbox.create(); - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - Gerrit._loadPlugins([]); - adminApi = plugin.admin(); - }); - - teardown(() => { - adminApi = null; - sandbox.restore(); - }); - - test('exists', () => { - assert.isOk(adminApi); - }); - - test('addMenuLink', () => { - adminApi.addMenuLink('text', 'url'); - const links = adminApi.getMenuLinks(); - assert.equal(links.length, 1); - assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null}); - }); - - test('addMenuLinkWithCapability', () => { - adminApi.addMenuLink('text', 'url', 'capability'); - const links = adminApi.getMenuLinks(); - assert.equal(links.length, 1); - assert.deepEqual(links[0], - {text: 'text', url: 'url', capability: 'capability'}); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + Gerrit._loadPlugins([]); + adminApi = plugin.admin(); }); + + teardown(() => { + adminApi = null; + sandbox.restore(); + }); + + test('exists', () => { + assert.isOk(adminApi); + }); + + test('addMenuLink', () => { + adminApi.addMenuLink('text', 'url'); + const links = adminApi.getMenuLinks(); + assert.equal(links.length, 1); + assert.deepEqual(links[0], {text: 'text', url: 'url', capability: null}); + }); + + test('addMenuLinkWithCapability', () => { + adminApi.addMenuLink('text', 'url', 'capability'); + const links = adminApi.getMenuLinks(); + assert.equal(links.length, 1); + assert.deepEqual(links[0], + {text: 'text', url: 'url', capability: 'capability'}); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html deleted file mode 100644 index ece8677..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.html +++ /dev/null
@@ -1,22 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-attribute-helper"> - <script src="gr-attribute-helper.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js index 0cff8e9..09620ef 100644 --- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js +++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js
@@ -14,6 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-attribute-helper"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html index 2dfd036..cfb51f0 100644 --- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper_test.html
@@ -19,28 +19,37 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-attribute-helper</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-attribute-helper.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-attribute-helper.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-attribute-helper.js'; +void(0); +</script> <dom-element id="some-element"> - <script> - Polymer({ - is: 'some-element', - properties: { - fooBar: { - type: Object, - notify: true, - }, - }, - }); - </script> + <script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-attribute-helper.js'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +Polymer({ + is: 'some-element', + properties: { + fooBar: { + type: Object, + notify: true, + }, + }, +}); +</script> </dom-element> @@ -50,54 +59,56 @@ </template> </test-fixture> -<script> - suite('gr-attribute-helper tests', async () => { - await readyToTest(); - let element; - let instance; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-attribute-helper.js'; +suite('gr-attribute-helper tests', () => { + let element; + let instance; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - instance = new GrAttributeHelper(element); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('resolved on value change from undefined', () => { - const promise = instance.get('fooBar').then(value => { - assert.equal(value, 'foo! bar!'); - }); - element.fooBar = 'foo! bar!'; - return promise; - }); - - test('resolves to current attribute value', () => { - element.fooBar = 'foo-foo-bar'; - const promise = instance.get('fooBar').then(value => { - assert.equal(value, 'foo-foo-bar'); - }); - element.fooBar = 'no bar'; - return promise; - }); - - test('bind', () => { - const stub = sandbox.stub(); - element.fooBar = 'bar foo'; - const unbind = instance.bind('fooBar', stub); - element.fooBar = 'partridge in a foo tree'; - element.fooBar = 'five gold bars'; - assert.equal(stub.callCount, 3); - assert.deepEqual(stub.args[0], ['bar foo']); - assert.deepEqual(stub.args[1], ['partridge in a foo tree']); - assert.deepEqual(stub.args[2], ['five gold bars']); - stub.reset(); - unbind(); - instance.fooBar = 'ladies dancing'; - assert.isFalse(stub.called); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + instance = new GrAttributeHelper(element); }); + + teardown(() => { + sandbox.restore(); + }); + + test('resolved on value change from undefined', () => { + const promise = instance.get('fooBar').then(value => { + assert.equal(value, 'foo! bar!'); + }); + element.fooBar = 'foo! bar!'; + return promise; + }); + + test('resolves to current attribute value', () => { + element.fooBar = 'foo-foo-bar'; + const promise = instance.get('fooBar').then(value => { + assert.equal(value, 'foo-foo-bar'); + }); + element.fooBar = 'no bar'; + return promise; + }); + + test('bind', () => { + const stub = sandbox.stub(); + element.fooBar = 'bar foo'; + const unbind = instance.bind('fooBar', stub); + element.fooBar = 'partridge in a foo tree'; + element.fooBar = 'five gold bars'; + assert.equal(stub.callCount, 3); + assert.deepEqual(stub.args[0], ['bar foo']); + assert.deepEqual(stub.args[1], ['partridge in a foo tree']); + assert.deepEqual(stub.args[2], ['five gold bars']); + stub.reset(); + unbind(); + instance.fooBar = 'ladies dancing'; + assert.isFalse(stub.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html deleted file mode 100644 index dd532e1..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.html +++ /dev/null
@@ -1,22 +0,0 @@ -<!-- -@license -Copyright (C) 2018 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-change-metadata-api"> - <script src="gr-change-metadata-api.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js index 80abf23..daf48f0 100644 --- a/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-change-metadata-api/gr-change-metadata-api.js
@@ -14,6 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-change-metadata-api"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html deleted file mode 100644 index 8b9000f..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.html +++ /dev/null
@@ -1,22 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-dom-hooks"> - <script src="gr-dom-hooks.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js index fb9adb5..9497493 100644 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js +++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks.js
@@ -14,6 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-dom-hooks"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html index 524b1b9..8e23b0d 100644 --- a/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-dom-hooks/gr-dom-hooks_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dom-hooks</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-dom-hooks.html"/> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-dom-hooks.js"></script> +<script type="module" src="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dom-hooks.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,131 +42,134 @@ </template> </test-fixture> -<script> - suite('gr-dom-hooks tests', async () => { - await readyToTest(); - const PUBLIC_METHODS =[ - 'onAttached', - 'onDetached', - 'getLastAttached', - 'getAllAttached', - 'getModuleName', - ]; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dom-hooks.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +suite('gr-dom-hooks tests', () => { + const PUBLIC_METHODS =[ + 'onAttached', + 'onDetached', + 'getLastAttached', + 'getAllAttached', + 'getModuleName', + ]; - let instance; - let sandbox; - let hook; - let hookInternal; + let instance; + let sandbox; + let hook; + let hookInternal; - setup(() => { - sandbox = sinon.sandbox.create(); - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - instance = new GrDomHooksManager(plugin); + setup(() => { + sandbox = sinon.sandbox.create(); + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + instance = new GrDomHooksManager(plugin); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('placeholder', () => { + setup(()=>{ + sandbox.stub(GrDomHook.prototype, '_createPlaceholder'); + hookInternal = instance.getDomHook('foo-bar'); + hook = hookInternal.getPublicAPI(); }); - teardown(() => { - sandbox.restore(); + test('public hook API has only public methods', () => { + assert.deepEqual(Object.keys(hook), PUBLIC_METHODS); }); - suite('placeholder', () => { - setup(()=>{ - sandbox.stub(GrDomHook.prototype, '_createPlaceholder'); - hookInternal = instance.getDomHook('foo-bar'); - hook = hookInternal.getPublicAPI(); - }); - - test('public hook API has only public methods', () => { - assert.deepEqual(Object.keys(hook), PUBLIC_METHODS); - }); - - test('registers placeholder class', () => { - assert.isTrue(hookInternal._createPlaceholder.calledWithExactly( - 'testplugin-autogenerated-foo-bar')); - }); - - test('getModuleName()', () => { - const hookName = Object.keys(instance._hooks).pop(); - assert.equal(hookName, 'testplugin-autogenerated-foo-bar'); - assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar'); - }); + test('registers placeholder class', () => { + assert.isTrue(hookInternal._createPlaceholder.calledWithExactly( + 'testplugin-autogenerated-foo-bar')); }); - suite('custom element', () => { - setup(() => { - hookInternal = instance.getDomHook('foo-bar', 'my-el'); - hook = hookInternal.getPublicAPI(); - }); - - test('public hook API has only public methods', () => { - assert.deepEqual(Object.keys(hook), PUBLIC_METHODS); - }); - - test('getModuleName()', () => { - const hookName = Object.keys(instance._hooks).pop(); - assert.equal(hookName, 'foo-bar my-el'); - assert.equal(hook.getModuleName(), 'my-el'); - }); - - test('onAttached', () => { - const onAttachedSpy = sandbox.spy(); - hook.onAttached(onAttachedSpy); - const [el1, el2] = [ - document.createElement(hook.getModuleName()), - document.createElement(hook.getModuleName()), - ]; - hookInternal.handleInstanceAttached(el1); - hookInternal.handleInstanceAttached(el2); - assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1)); - assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2)); - }); - - test('onDetached', () => { - const onDetachedSpy = sandbox.spy(); - hook.onDetached(onDetachedSpy); - const [el1, el2] = [ - document.createElement(hook.getModuleName()), - document.createElement(hook.getModuleName()), - ]; - hookInternal.handleInstanceDetached(el1); - assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1)); - hookInternal.handleInstanceDetached(el2); - assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2)); - }); - - test('getAllAttached', () => { - const [el1, el2] = [ - document.createElement(hook.getModuleName()), - document.createElement(hook.getModuleName()), - ]; - el1.textContent = 'one'; - el2.textContent = 'two'; - hookInternal.handleInstanceAttached(el1); - hookInternal.handleInstanceAttached(el2); - assert.deepEqual([el1, el2], hook.getAllAttached()); - hookInternal.handleInstanceDetached(el1); - assert.deepEqual([el2], hook.getAllAttached()); - }); - - test('getLastAttached', () => { - const beforeAttachedPromise = hook.getLastAttached().then( - el => assert.strictEqual(el1, el)); - const [el1, el2] = [ - document.createElement(hook.getModuleName()), - document.createElement(hook.getModuleName()), - ]; - el1.textContent = 'one'; - el2.textContent = 'two'; - hookInternal.handleInstanceAttached(el1); - hookInternal.handleInstanceAttached(el2); - const afterAttachedPromise = hook.getLastAttached().then( - el => assert.strictEqual(el2, el)); - return Promise.all([ - beforeAttachedPromise, - afterAttachedPromise, - ]); - }); + test('getModuleName()', () => { + const hookName = Object.keys(instance._hooks).pop(); + assert.equal(hookName, 'testplugin-autogenerated-foo-bar'); + assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar'); }); }); + + suite('custom element', () => { + setup(() => { + hookInternal = instance.getDomHook('foo-bar', 'my-el'); + hook = hookInternal.getPublicAPI(); + }); + + test('public hook API has only public methods', () => { + assert.deepEqual(Object.keys(hook), PUBLIC_METHODS); + }); + + test('getModuleName()', () => { + const hookName = Object.keys(instance._hooks).pop(); + assert.equal(hookName, 'foo-bar my-el'); + assert.equal(hook.getModuleName(), 'my-el'); + }); + + test('onAttached', () => { + const onAttachedSpy = sandbox.spy(); + hook.onAttached(onAttachedSpy); + const [el1, el2] = [ + document.createElement(hook.getModuleName()), + document.createElement(hook.getModuleName()), + ]; + hookInternal.handleInstanceAttached(el1); + hookInternal.handleInstanceAttached(el2); + assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1)); + assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2)); + }); + + test('onDetached', () => { + const onDetachedSpy = sandbox.spy(); + hook.onDetached(onDetachedSpy); + const [el1, el2] = [ + document.createElement(hook.getModuleName()), + document.createElement(hook.getModuleName()), + ]; + hookInternal.handleInstanceDetached(el1); + assert.isTrue(onDetachedSpy.firstCall.calledWithExactly(el1)); + hookInternal.handleInstanceDetached(el2); + assert.isTrue(onDetachedSpy.secondCall.calledWithExactly(el2)); + }); + + test('getAllAttached', () => { + const [el1, el2] = [ + document.createElement(hook.getModuleName()), + document.createElement(hook.getModuleName()), + ]; + el1.textContent = 'one'; + el2.textContent = 'two'; + hookInternal.handleInstanceAttached(el1); + hookInternal.handleInstanceAttached(el2); + assert.deepEqual([el1, el2], hook.getAllAttached()); + hookInternal.handleInstanceDetached(el1); + assert.deepEqual([el2], hook.getAllAttached()); + }); + + test('getLastAttached', () => { + const beforeAttachedPromise = hook.getLastAttached().then( + el => assert.strictEqual(el1, el)); + const [el1, el2] = [ + document.createElement(hook.getModuleName()), + document.createElement(hook.getModuleName()), + ]; + el1.textContent = 'one'; + el2.textContent = 'two'; + hookInternal.handleInstanceAttached(el1); + hookInternal.handleInstanceAttached(el2); + const afterAttachedPromise = hook.getLastAttached().then( + el => assert.strictEqual(el2, el)); + return Promise.all([ + beforeAttachedPromise, + afterAttachedPromise, + ]); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js index 1c10642..e1624b9 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator.js
@@ -14,155 +14,163 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const INIT_PROPERTIES_TIMEOUT_MS = 10000; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {importHref} from '../../../scripts/import-href.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-endpoint-decorator_html.js'; - /** @extends Polymer.Element */ - class GrEndpointDecorator extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-endpoint-decorator'; } +const INIT_PROPERTIES_TIMEOUT_MS = 10000; - static get properties() { - return { - name: String, - /** @type {!Map} */ - _domHooks: { - type: Map, - value() { return new Map(); }, - }, - /** - * This map prevents importing the same endpoint twice. - * Without caching, if a plugin is loaded after the loaded plugins - * callback fires, it will be imported twice and appear twice on the page. - * - * @type {!Map} - */ - _initializedPlugins: { - type: Map, - value() { return new Map(); }, - }, - }; - } +/** @extends Polymer.Element */ +class GrEndpointDecorator extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** @override */ - detached() { - super.detached(); - for (const [el, domHook] of this._domHooks) { - domHook.handleInstanceDetached(el); - } - } + static get is() { return 'gr-endpoint-decorator'; } - /** - * @suppress {checkTypes} - */ - _import(url) { - return new Promise((resolve, reject) => { - Polymer.importHref(url, resolve, reject); - }); - } + static get properties() { + return { + name: String, + /** @type {!Map} */ + _domHooks: { + type: Map, + value() { return new Map(); }, + }, + /** + * This map prevents importing the same endpoint twice. + * Without caching, if a plugin is loaded after the loaded plugins + * callback fires, it will be imported twice and appear twice on the page. + * + * @type {!Map} + */ + _initializedPlugins: { + type: Map, + value() { return new Map(); }, + }, + }; + } - _initDecoration(name, plugin) { - const el = document.createElement(name); - return this._initProperties(el, plugin, - this.getContentChildren().find( - el => el.nodeName !== 'GR-ENDPOINT-PARAM')) - .then(el => this._appendChild(el)); - } - - _initReplacement(name, plugin) { - this.getContentChildNodes() - .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM') - .forEach(node => node.remove()); - const el = document.createElement(name); - return this._initProperties(el, plugin).then( - el => this._appendChild(el)); - } - - _getEndpointParams() { - return Array.from( - Polymer.dom(this).querySelectorAll('gr-endpoint-param')); - } - - /** - * @param {!Element} el - * @param {!Object} plugin - * @param {!Element=} opt_content - * @return {!Promise<Element>} - */ - _initProperties(el, plugin, opt_content) { - el.plugin = plugin; - if (opt_content) { - el.content = opt_content; - } - const expectProperties = this._getEndpointParams().map(paramEl => { - const helper = plugin.attributeHelper(paramEl); - const paramName = paramEl.getAttribute('name'); - return helper.get('value').then( - value => helper.bind('value', - value => plugin.attributeHelper(el).set(paramName, value)) - ); - }); - let timeoutId; - const timeout = new Promise( - resolve => timeoutId = setTimeout(() => { - console.warn( - 'Timeout waiting for endpoint properties initialization: ' + - `plugin ${plugin.getPluginName()}, endpoint ${this.name}`); - }, INIT_PROPERTIES_TIMEOUT_MS)); - return Promise.race([timeout, Promise.all(expectProperties)]) - .then(() => { - clearTimeout(timeoutId); - return el; - }); - } - - _appendChild(el) { - return Polymer.dom(this.root).appendChild(el); - } - - _initModule({moduleName, plugin, type, domHook}) { - const name = plugin.getPluginName() + '.' + moduleName; - if (this._initializedPlugins.get(name)) { - return; - } - let initPromise; - switch (type) { - case 'decorate': - initPromise = this._initDecoration(moduleName, plugin); - break; - case 'replace': - initPromise = this._initReplacement(moduleName, plugin); - break; - } - if (!initPromise) { - console.warn('Unable to initialize module ' + name); - } - this._initializedPlugins.set(name, true); - initPromise.then(el => { - domHook.handleInstanceAttached(el); - this._domHooks.set(el, domHook); - }); - } - - /** @override */ - ready() { - super.ready(); - Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this)); - Gerrit.awaitPluginsLoaded() - .then(() => Promise.all( - Gerrit._endpoints.getPlugins(this.name).map( - pluginUrl => this._import(pluginUrl))) - ) - .then(() => - Gerrit._endpoints - .getDetails(this.name) - .forEach(this._initModule, this) - ); + /** @override */ + detached() { + super.detached(); + for (const [el, domHook] of this._domHooks) { + domHook.handleInstanceDetached(el); } } - customElements.define(GrEndpointDecorator.is, GrEndpointDecorator); -})(); + /** + * @suppress {checkTypes} + */ + _import(url) { + return new Promise((resolve, reject) => { + importHref(url, resolve, reject); + }); + } + + _initDecoration(name, plugin) { + const el = document.createElement(name); + return this._initProperties(el, plugin, + this.getContentChildren().find( + el => el.nodeName !== 'GR-ENDPOINT-PARAM')) + .then(el => this._appendChild(el)); + } + + _initReplacement(name, plugin) { + this.getContentChildNodes() + .filter(node => node.nodeName !== 'GR-ENDPOINT-PARAM') + .forEach(node => node.remove()); + const el = document.createElement(name); + return this._initProperties(el, plugin).then( + el => this._appendChild(el)); + } + + _getEndpointParams() { + return Array.from( + dom(this).querySelectorAll('gr-endpoint-param')); + } + + /** + * @param {!Element} el + * @param {!Object} plugin + * @param {!Element=} opt_content + * @return {!Promise<Element>} + */ + _initProperties(el, plugin, opt_content) { + el.plugin = plugin; + if (opt_content) { + el.content = opt_content; + } + const expectProperties = this._getEndpointParams().map(paramEl => { + const helper = plugin.attributeHelper(paramEl); + const paramName = paramEl.getAttribute('name'); + return helper.get('value').then( + value => helper.bind('value', + value => plugin.attributeHelper(el).set(paramName, value)) + ); + }); + let timeoutId; + const timeout = new Promise( + resolve => timeoutId = setTimeout(() => { + console.warn( + 'Timeout waiting for endpoint properties initialization: ' + + `plugin ${plugin.getPluginName()}, endpoint ${this.name}`); + }, INIT_PROPERTIES_TIMEOUT_MS)); + return Promise.race([timeout, Promise.all(expectProperties)]) + .then(() => { + clearTimeout(timeoutId); + return el; + }); + } + + _appendChild(el) { + return dom(this.root).appendChild(el); + } + + _initModule({moduleName, plugin, type, domHook}) { + const name = plugin.getPluginName() + '.' + moduleName; + if (this._initializedPlugins.get(name)) { + return; + } + let initPromise; + switch (type) { + case 'decorate': + initPromise = this._initDecoration(moduleName, plugin); + break; + case 'replace': + initPromise = this._initReplacement(moduleName, plugin); + break; + } + if (!initPromise) { + console.warn('Unable to initialize module ' + name); + } + this._initializedPlugins.set(name, true); + initPromise.then(el => { + domHook.handleInstanceAttached(el); + this._domHooks.set(el, domHook); + }); + } + + /** @override */ + ready() { + super.ready(); + Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this)); + Gerrit.awaitPluginsLoaded() + .then(() => Promise.all( + Gerrit._endpoints.getPlugins(this.name).map( + pluginUrl => this._import(pluginUrl))) + ) + .then(() => + Gerrit._endpoints + .getDetails(this.name) + .forEach(this._initModule, this) + ); + } +} + +customElements.define(GrEndpointDecorator.is, GrEndpointDecorator);
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js index 1b27c0a..1644c07 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_html.js
@@ -1,26 +1,21 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-endpoint-decorator"> - <template> +export const htmlTemplate = html` <slot></slot> - </template> - <script src="gr-endpoint-decorator.js"></script> -</dom-module> \ No newline at end of file +`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html index e0d91fa..fcac174 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-endpoint-decorator</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-endpoint-decorator.html"> -<link rel="import" href="../gr-endpoint-param/gr-endpoint-param.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-endpoint-decorator.js"></script> +<script type="module" src="../gr-endpoint-param/gr-endpoint-param.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-endpoint-decorator.js'; +import '../gr-endpoint-param/gr-endpoint-param.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -46,149 +52,153 @@ </template> </test-fixture> -<script> - suite('gr-endpoint-decorator', async () => { - await readyToTest(); - let container; - let sandbox; - let plugin; - let decorationHook; - let replacementHook; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-endpoint-decorator.js'; +import '../gr-endpoint-param/gr-endpoint-param.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-endpoint-decorator', () => { + let container; + let sandbox; + let plugin; + let decorationHook; + let replacementHook; - setup(done => { - sandbox = sinon.sandbox.create(); - stub('gr-endpoint-decorator', { - _import: sandbox.stub().returns(Promise.resolve()), - }); - Gerrit._testOnly_resetPlugins(); - container = fixture('basic'); - Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html'); - // Decoration - decorationHook = plugin.registerCustomComponent('first', 'some-module'); - // Replacement - replacementHook = plugin.registerCustomComponent( - 'second', 'other-module', {replace: true}); - // Mimic all plugins loaded. - Gerrit._loadPlugins([]); - flush(done); + setup(done => { + sandbox = sinon.sandbox.create(); + stub('gr-endpoint-decorator', { + _import: sandbox.stub().returns(Promise.resolve()), }); + Gerrit._testOnly_resetPlugins(); + container = fixture('basic'); + Gerrit.install(p => plugin = p, '0.1', 'http://some/plugin/url.html'); + // Decoration + decorationHook = plugin.registerCustomComponent('first', 'some-module'); + // Replacement + replacementHook = plugin.registerCustomComponent( + 'second', 'other-module', {replace: true}); + // Mimic all plugins loaded. + Gerrit._loadPlugins([]); + flush(done); + }); - teardown(() => { - sandbox.restore(); + teardown(() => { + sandbox.restore(); + }); + + test('imports plugin-provided modules into endpoints', () => { + const endpoints = + Array.from(container.querySelectorAll('gr-endpoint-decorator')); + assert.equal(endpoints.length, 3); + endpoints.forEach(element => { + assert.isTrue( + element._import.calledWith(new URL('http://some/plugin/url.html'))); }); + }); - test('imports plugin-provided modules into endpoints', () => { - const endpoints = - Array.from(container.querySelectorAll('gr-endpoint-decorator')); - assert.equal(endpoints.length, 3); - endpoints.forEach(element => { - assert.isTrue( - element._import.calledWith(new URL('http://some/plugin/url.html'))); - }); - }); + test('decoration', () => { + const element = + container.querySelector('gr-endpoint-decorator[name="first"]'); + const modules = Array.from(dom(element.root).children).filter( + element => element.nodeName === 'SOME-MODULE'); + assert.equal(modules.length, 1); + const [module] = modules; + assert.isOk(module); + assert.equal(module['someparam'], 'barbar'); + return decorationHook.getLastAttached().then(element => { + assert.strictEqual(element, module); + }) + .then(() => { + element.remove(); + assert.equal(decorationHook.getAllAttached().length, 0); + }); + }); - test('decoration', () => { + test('replacement', () => { + const element = + container.querySelector('gr-endpoint-decorator[name="second"]'); + const module = Array.from(dom(element.root).children).find( + element => element.nodeName === 'OTHER-MODULE'); + assert.isOk(module); + assert.equal(module['someparam'], 'foofoo'); + return replacementHook.getLastAttached() + .then(element => { + assert.strictEqual(element, module); + }) + .then(() => { + element.remove(); + assert.equal(replacementHook.getAllAttached().length, 0); + }); + }); + + test('late registration', done => { + plugin.registerCustomComponent('banana', 'noob-noob'); + flush(() => { const element = - container.querySelector('gr-endpoint-decorator[name="first"]'); - const modules = Array.from(Polymer.dom(element.root).children).filter( - element => element.nodeName === 'SOME-MODULE'); - assert.equal(modules.length, 1); - const [module] = modules; + container.querySelector('gr-endpoint-decorator[name="banana"]'); + const module = Array.from(dom(element.root).children).find( + element => element.nodeName === 'NOOB-NOOB'); assert.isOk(module); - assert.equal(module['someparam'], 'barbar'); - return decorationHook.getLastAttached().then(element => { - assert.strictEqual(element, module); - }) - .then(() => { - element.remove(); - assert.equal(decorationHook.getAllAttached().length, 0); - }); + done(); }); + }); - test('replacement', () => { + test('two modules', done => { + plugin.registerCustomComponent('banana', 'mod-one'); + plugin.registerCustomComponent('banana', 'mod-two'); + flush(() => { const element = - container.querySelector('gr-endpoint-decorator[name="second"]'); - const module = Array.from(Polymer.dom(element.root).children).find( - element => element.nodeName === 'OTHER-MODULE'); - assert.isOk(module); - assert.equal(module['someparam'], 'foofoo'); - return replacementHook.getLastAttached() - .then(element => { - assert.strictEqual(element, module); - }) - .then(() => { - element.remove(); - assert.equal(replacementHook.getAllAttached().length, 0); - }); + container.querySelector('gr-endpoint-decorator[name="banana"]'); + const module1 = Array.from(dom(element.root).children).find( + element => element.nodeName === 'MOD-ONE'); + assert.isOk(module1); + const module2 = Array.from(dom(element.root).children).find( + element => element.nodeName === 'MOD-TWO'); + assert.isOk(module2); + done(); }); + }); - test('late registration', done => { - plugin.registerCustomComponent('banana', 'noob-noob'); + test('late param setup', done => { + const element = + container.querySelector('gr-endpoint-decorator[name="banana"]'); + const param = dom(element).querySelector('gr-endpoint-param'); + param['value'] = undefined; + plugin.registerCustomComponent('banana', 'noob-noob'); + flush(() => { + let module = Array.from(dom(element.root).children).find( + element => element.nodeName === 'NOOB-NOOB'); + // Module waits for param to be defined. + assert.isNotOk(module); + const value = {abc: 'def'}; + param.value = value; flush(() => { - const element = - container.querySelector('gr-endpoint-decorator[name="banana"]'); - const module = Array.from(Polymer.dom(element.root).children).find( + module = Array.from(dom(element.root).children).find( element => element.nodeName === 'NOOB-NOOB'); assert.isOk(module); - done(); - }); - }); - - test('two modules', done => { - plugin.registerCustomComponent('banana', 'mod-one'); - plugin.registerCustomComponent('banana', 'mod-two'); - flush(() => { - const element = - container.querySelector('gr-endpoint-decorator[name="banana"]'); - const module1 = Array.from(Polymer.dom(element.root).children).find( - element => element.nodeName === 'MOD-ONE'); - assert.isOk(module1); - const module2 = Array.from(Polymer.dom(element.root).children).find( - element => element.nodeName === 'MOD-TWO'); - assert.isOk(module2); - done(); - }); - }); - - test('late param setup', done => { - const element = - container.querySelector('gr-endpoint-decorator[name="banana"]'); - const param = Polymer.dom(element).querySelector('gr-endpoint-param'); - param['value'] = undefined; - plugin.registerCustomComponent('banana', 'noob-noob'); - flush(() => { - let module = Array.from(Polymer.dom(element.root).children).find( - element => element.nodeName === 'NOOB-NOOB'); - // Module waits for param to be defined. - assert.isNotOk(module); - const value = {abc: 'def'}; - param.value = value; - flush(() => { - module = Array.from(Polymer.dom(element.root).children).find( - element => element.nodeName === 'NOOB-NOOB'); - assert.isOk(module); - assert.strictEqual(module['someParam'], value); - done(); - }); - }); - }); - - test('param is bound', done => { - const element = - container.querySelector('gr-endpoint-decorator[name="banana"]'); - const param = Polymer.dom(element).querySelector('gr-endpoint-param'); - const value1 = {abc: 'def'}; - const value2 = {def: 'abc'}; - param.value = value1; - plugin.registerCustomComponent('banana', 'noob-noob'); - flush(() => { - const module = Array.from(Polymer.dom(element.root).children).find( - element => element.nodeName === 'NOOB-NOOB'); - assert.strictEqual(module['someParam'], value1); - param.value = value2; - assert.strictEqual(module['someParam'], value2); + assert.strictEqual(module['someParam'], value); done(); }); }); }); + + test('param is bound', done => { + const element = + container.querySelector('gr-endpoint-decorator[name="banana"]'); + const param = dom(element).querySelector('gr-endpoint-param'); + const value1 = {abc: 'def'}; + const value2 = {def: 'abc'}; + param.value = value1; + plugin.registerCustomComponent('banana', 'noob-noob'); + flush(() => { + const module = Array.from(dom(element.root).children).find( + element => element.nodeName === 'NOOB-NOOB'); + assert.strictEqual(module['someParam'], value1); + param.value = value2; + assert.strictEqual(module['someParam'], value2); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html deleted file mode 100644 index 6a5b558..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.html +++ /dev/null
@@ -1,22 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-endpoint-param"> - <script src="gr-endpoint-param.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js index bcad7f9..9574391 100644 --- a/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js +++ b/polygerrit-ui/app/elements/plugins/gr-endpoint-param/gr-endpoint-param.js
@@ -14,41 +14,43 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrEndpointParam extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-endpoint-param'; } +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; - static get properties() { - return { - name: String, - value: { - type: Object, - notify: true, - observer: '_valueChanged', - }, - }; - } +/** @extends Polymer.Element */ +class GrEndpointParam extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get is() { return 'gr-endpoint-param'; } - _valueChanged(newValue, oldValue) { - /* In polymer 2 the following change was made: - "Property change notifications (property-changed events) aren't fired when - the value changes as a result of a binding from the host" - (see https://polymer-library.polymer-project.org/2.0/docs/about_20). - To workaround this problem, we fire the event from the observer. - In some cases this fire the event twice, but our code is - ready for it. - */ - const detail = { - value: newValue, - }; - this.dispatchEvent(new CustomEvent('value-changed', {detail})); - } + static get properties() { + return { + name: String, + value: { + type: Object, + notify: true, + observer: '_valueChanged', + }, + }; } - customElements.define(GrEndpointParam.is, GrEndpointParam); -})(); + _valueChanged(newValue, oldValue) { + /* In polymer 2 the following change was made: + "Property change notifications (property-changed events) aren't fired when + the value changes as a result of a binding from the host" + (see https://polymer-library.polymer-project.org/2.0/docs/about_20). + To workaround this problem, we fire the event from the observer. + In some cases this fire the event twice, but our code is + ready for it. + */ + const detail = { + value: newValue, + }; + this.dispatchEvent(new CustomEvent('value-changed', {detail})); + } +} + +customElements.define(GrEndpointParam.is, GrEndpointParam);
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html deleted file mode 100644 index 15db861..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.html +++ /dev/null
@@ -1,23 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> - -<dom-module id="gr-event-helper"> - <script src="gr-event-helper.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js index 481a467..66d42d3 100644 --- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js +++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js
@@ -14,6 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-event-helper"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html index bb08cfb..8eebe33 100644 --- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper_test.html
@@ -19,33 +19,42 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-event-helper</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-event-helper.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-event-helper.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-event-helper.js'; +void(0); +</script> <dom-element id="some-element"> - <script> - Polymer({ - is: 'some-element', + <script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-event-helper.js'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +Polymer({ + is: 'some-element', - properties: { - fooBar: { - type: Object, - notify: true, - }, - }, + properties: { + fooBar: { + type: Object, + notify: true, + }, + }, - behaviors: [ - Gerrit.FireBehavior, - ], - }); - </script> + behaviors: [ + Gerrit.FireBehavior, + ], +}); +</script> </dom-element> @@ -55,85 +64,88 @@ </template> </test-fixture> -<script> - suite('gr-event-helper tests', async () => { - await readyToTest(); - let element; - let instance; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-event-helper.js'; +import {addListener} from '@polymer/polymer/lib/utils/gestures.js'; +suite('gr-event-helper tests', () => { + let element; + let instance; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - instance = new GrEventHelper(element); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('onTap()', done => { - instance.onTap(() => { - done(); - }); - MockInteractions.tap(element); - }); - - test('onTap() cancel', () => { - const tapStub = sandbox.stub(); - Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub); - instance.onTap(() => false); - MockInteractions.tap(element); - flushAsynchronousOperations(); - assert.isFalse(tapStub.called); - }); - - test('onClick() cancel', () => { - const tapStub = sandbox.stub(); - element.parentElement.addEventListener('click', tapStub); - instance.onTap(() => false); - MockInteractions.tap(element); - flushAsynchronousOperations(); - assert.isFalse(tapStub.called); - }); - - test('captureTap()', done => { - instance.captureTap(() => { - done(); - }); - MockInteractions.tap(element); - }); - - test('captureClick()', done => { - instance.captureClick(() => { - done(); - }); - MockInteractions.tap(element); - }); - - test('captureTap() cancels tap()', () => { - const tapStub = sandbox.stub(); - Polymer.Gestures.addListener(element.parentElement, 'tap', tapStub); - instance.captureTap(() => false); - MockInteractions.tap(element); - flushAsynchronousOperations(); - assert.isFalse(tapStub.called); - }); - - test('captureClick() cancels click()', () => { - const tapStub = sandbox.stub(); - element.addEventListener('click', tapStub); - instance.captureTap(() => false); - MockInteractions.tap(element); - flushAsynchronousOperations(); - assert.isFalse(tapStub.called); - }); - - test('on()', done => { - instance.on('foo', () => { - done(); - }); - element.fire('foo'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + instance = new GrEventHelper(element); }); + + teardown(() => { + sandbox.restore(); + }); + + test('onTap()', done => { + instance.onTap(() => { + done(); + }); + MockInteractions.tap(element); + }); + + test('onTap() cancel', () => { + const tapStub = sandbox.stub(); + addListener(element.parentElement, 'tap', tapStub); + instance.onTap(() => false); + MockInteractions.tap(element); + flushAsynchronousOperations(); + assert.isFalse(tapStub.called); + }); + + test('onClick() cancel', () => { + const tapStub = sandbox.stub(); + element.parentElement.addEventListener('click', tapStub); + instance.onTap(() => false); + MockInteractions.tap(element); + flushAsynchronousOperations(); + assert.isFalse(tapStub.called); + }); + + test('captureTap()', done => { + instance.captureTap(() => { + done(); + }); + MockInteractions.tap(element); + }); + + test('captureClick()', done => { + instance.captureClick(() => { + done(); + }); + MockInteractions.tap(element); + }); + + test('captureTap() cancels tap()', () => { + const tapStub = sandbox.stub(); + addListener(element.parentElement, 'tap', tapStub); + instance.captureTap(() => false); + MockInteractions.tap(element); + flushAsynchronousOperations(); + assert.isFalse(tapStub.called); + }); + + test('captureClick() cancels click()', () => { + const tapStub = sandbox.stub(); + element.addEventListener('click', tapStub); + instance.captureTap(() => false); + MockInteractions.tap(element); + flushAsynchronousOperations(); + assert.isFalse(tapStub.called); + }); + + test('on()', done => { + instance.on('foo', () => { + done(); + }); + element.fire('foo'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js index 7e239f9..ba4dd58 100644 --- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style.js
@@ -14,84 +14,92 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrExternalStyle extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-external-style'; } +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {importHref} from '../../../scripts/import-href.js'; +import {updateStyles} from '@polymer/polymer/lib/mixins/element-mixin.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-external-style_html.js'; - static get properties() { - return { - name: String, - _urlsImported: { - type: Array, - value() { return []; }, - }, - _stylesApplied: { - type: Array, - value() { return []; }, - }, - }; - } +/** @extends Polymer.Element */ +class GrExternalStyle extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _importHref(url, resolve, reject) { - // It is impossible to mock es6-module imported function. - // The _importHref function is mocked in test. - Polymer.importHref(url, resolve, reject); - } + static get is() { return 'gr-external-style'; } - /** - * @suppress {checkTypes} - */ - _import(url) { - if (this._urlsImported.includes(url)) { return Promise.resolve(); } - this._urlsImported.push(url); - return new Promise((resolve, reject) => { - this._importHref(url, resolve, reject); - }); - } - - _applyStyle(name) { - if (this._stylesApplied.includes(name)) { return; } - this._stylesApplied.push(name); - - const s = document.createElement('style'); - s.setAttribute('include', name); - const cs = document.createElement('custom-style'); - cs.appendChild(s); - // When using Shadow DOM <custom-style> must be added to the <body>. - // Within <gr-external-style> itself the styles would have no effect. - const topEl = document.getElementsByTagName('body')[0]; - topEl.insertBefore(cs, topEl.firstChild); - Polymer.updateStyles(); - } - - _importAndApply() { - Promise.all(Gerrit._endpoints.getPlugins(this.name).map( - pluginUrl => this._import(pluginUrl)) - ).then(() => { - const moduleNames = Gerrit._endpoints.getModules(this.name); - for (const name of moduleNames) { - this._applyStyle(name); - } - }); - } - - /** @override */ - attached() { - super.attached(); - this._importAndApply(); - } - - /** @override */ - ready() { - super.ready(); - Gerrit.awaitPluginsLoaded().then(() => this._importAndApply()); - } + static get properties() { + return { + name: String, + _urlsImported: { + type: Array, + value() { return []; }, + }, + _stylesApplied: { + type: Array, + value() { return []; }, + }, + }; } - customElements.define(GrExternalStyle.is, GrExternalStyle); -})(); + _importHref(url, resolve, reject) { + // It is impossible to mock es6-module imported function. + // The _importHref function is mocked in test. + importHref(url, resolve, reject); + } + + /** + * @suppress {checkTypes} + */ + _import(url) { + if (this._urlsImported.includes(url)) { return Promise.resolve(); } + this._urlsImported.push(url); + return new Promise((resolve, reject) => { + this._importHref(url, resolve, reject); + }); + } + + _applyStyle(name) { + if (this._stylesApplied.includes(name)) { return; } + this._stylesApplied.push(name); + + const s = document.createElement('style'); + s.setAttribute('include', name); + const cs = document.createElement('custom-style'); + cs.appendChild(s); + // When using Shadow DOM <custom-style> must be added to the <body>. + // Within <gr-external-style> itself the styles would have no effect. + const topEl = document.getElementsByTagName('body')[0]; + topEl.insertBefore(cs, topEl.firstChild); + updateStyles(); + } + + _importAndApply() { + Promise.all(Gerrit._endpoints.getPlugins(this.name).map( + pluginUrl => this._import(pluginUrl)) + ).then(() => { + const moduleNames = Gerrit._endpoints.getModules(this.name); + for (const name of moduleNames) { + this._applyStyle(name); + } + }); + } + + /** @override */ + attached() { + super.attached(); + this._importAndApply(); + } + + /** @override */ + ready() { + super.ready(); + Gerrit.awaitPluginsLoaded().then(() => this._importAndApply()); + } +} + +customElements.define(GrExternalStyle.is, GrExternalStyle);
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js index 6a55349..1644c07 100644 --- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_html.js
@@ -1,26 +1,21 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-external-style"> - <template> +export const htmlTemplate = html` <slot></slot> - </template> - <script src="gr-external-style.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html index 808de43..7b76f63 100644 --- a/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-external-style/gr-external-style_test.html
@@ -19,13 +19,13 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-external-style</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-external-style.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-external-style.js"></script> <test-fixture id="basic"> <template> @@ -33,98 +33,100 @@ </template> </test-fixture> -<script> - suite('gr-external-style integration tests', async () => { - await readyToTest(); - const TEST_URL = 'http://some/plugin/url.html'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-external-style.js'; +suite('gr-external-style integration tests', () => { + const TEST_URL = 'http://some/plugin/url.html'; - let sandbox; - let element; - let plugin; - let importHrefStub; + let sandbox; + let element; + let plugin; + let importHrefStub; - const installPlugin = () => { - if (plugin) { return; } - Gerrit.install(p => { - plugin = p; - }, '0.1', TEST_URL); - }; + const installPlugin = () => { + if (plugin) { return; } + Gerrit.install(p => { + plugin = p; + }, '0.1', TEST_URL); + }; - const createElement = () => { - element = fixture('basic'); - sandbox.spy(element, '_applyStyle'); - }; + const createElement = () => { + element = fixture('basic'); + sandbox.spy(element, '_applyStyle'); + }; - /** - * Installs the plugin, creates the element, registers style module. - */ - const lateRegister = () => { - installPlugin(); - createElement(); - plugin.registerStyleModule('foo', 'some-module'); - }; + /** + * Installs the plugin, creates the element, registers style module. + */ + const lateRegister = () => { + installPlugin(); + createElement(); + plugin.registerStyleModule('foo', 'some-module'); + }; - /** - * Installs the plugin, registers style module, creates the element. - */ - const earlyRegister = () => { - installPlugin(); - plugin.registerStyleModule('foo', 'some-module'); - createElement(); - }; + /** + * Installs the plugin, registers style module, creates the element. + */ + const earlyRegister = () => { + installPlugin(); + plugin.registerStyleModule('foo', 'some-module'); + createElement(); + }; - setup(() => { - sandbox = sinon.sandbox.create(); - importHrefStub = sandbox.stub().callsArg(1); - stub('gr-external-style', { - _importHref: (url, resolve, reject) => { - importHrefStub(url, resolve, reject); - }, - }); - sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); + setup(() => { + sandbox = sinon.sandbox.create(); + importHrefStub = sandbox.stub().callsArg(1); + stub('gr-external-style', { + _importHref: (url, resolve, reject) => { + importHrefStub(url, resolve, reject); + }, }); - - teardown(() => { - sandbox.restore(); - }); - - test('imports plugin-provided module', async () => { - lateRegister(); - await new Promise(flush); - assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL))); - }); - - test('applies plugin-provided styles', async () => { - lateRegister(); - await new Promise(flush); - assert.isTrue(element._applyStyle.calledWith('some-module')); - }); - - test('does not double import', async () => { - earlyRegister(); - await new Promise(flush); - plugin.registerStyleModule('foo', 'some-module'); - await new Promise(flush); - const urlsImported = - element._urlsImported.filter(url => url.toString() === TEST_URL); - assert.strictEqual(urlsImported.length, 1); - }); - - test('does not double apply', async () => { - earlyRegister(); - await new Promise(flush); - plugin.registerStyleModule('foo', 'some-module'); - await new Promise(flush); - const stylesApplied = - element._stylesApplied.filter(name => name === 'some-module'); - assert.strictEqual(stylesApplied.length, 1); - }); - - test('loads and applies preloaded modules', async () => { - earlyRegister(); - await new Promise(flush); - assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL))); - assert.isTrue(element._applyStyle.calledWith('some-module')); - }); + sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve()); }); + + teardown(() => { + sandbox.restore(); + }); + + test('imports plugin-provided module', async () => { + lateRegister(); + await new Promise(flush); + assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL))); + }); + + test('applies plugin-provided styles', async () => { + lateRegister(); + await new Promise(flush); + assert.isTrue(element._applyStyle.calledWith('some-module')); + }); + + test('does not double import', async () => { + earlyRegister(); + await new Promise(flush); + plugin.registerStyleModule('foo', 'some-module'); + await new Promise(flush); + const urlsImported = + element._urlsImported.filter(url => url.toString() === TEST_URL); + assert.strictEqual(urlsImported.length, 1); + }); + + test('does not double apply', async () => { + earlyRegister(); + await new Promise(flush); + plugin.registerStyleModule('foo', 'some-module'); + await new Promise(flush); + const stylesApplied = + element._stylesApplied.filter(name => name === 'some-module'); + assert.strictEqual(stylesApplied.length, 1); + }); + + test('loads and applies preloaded modules', async () => { + earlyRegister(); + await new Promise(flush); + assert.isTrue(importHrefStub.calledWith(new URL(TEST_URL))); + assert.isTrue(element._applyStyle.calledWith('some-module')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html deleted file mode 100644 index f277899..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.html +++ /dev/null
@@ -1,24 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-plugin-host"> - <script src="gr-plugin-host.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js index da050fb..1236f97 100644 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host.js
@@ -14,59 +14,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrPluginHost extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-plugin-host'; } +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; - static get properties() { - return { - config: { - type: Object, - observer: '_configChanged', - }, - }; - } +/** @extends Polymer.Element */ +class GrPluginHost extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get is() { return 'gr-plugin-host'; } - _configChanged(config) { - const plugins = config.plugin; - const htmlPlugins = (plugins.html_resource_paths || []); - const jsPlugins = - this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins); - const shouldLoadTheme = config.default_theme && - !Gerrit._isPluginPreloaded('preloaded:gerrit-theme'); - const themeToLoad = - shouldLoadTheme ? [config.default_theme] : []; - - // Theme should be loaded first if has one to have better UX - const pluginsPending = - themeToLoad.concat(jsPlugins, htmlPlugins); - - const pluginOpts = {}; - - if (shouldLoadTheme) { - // Theme needs to be loaded synchronous. - pluginOpts[config.default_theme] = {sync: true}; - } - - Gerrit._loadPlugins(pluginsPending, pluginOpts); - } - - /** - * Omit .js plugins that have .html counterparts. - * For example, if plugin provides foo.js and foo.html, skip foo.js. - */ - _handleMigrations(jsPlugins, htmlPlugins) { - return jsPlugins.filter(url => { - const counterpart = url.replace(/\.js$/, '.html'); - return !htmlPlugins.includes(counterpart); - }); - } + static get properties() { + return { + config: { + type: Object, + observer: '_configChanged', + }, + }; } - customElements.define(GrPluginHost.is, GrPluginHost); -})(); + _configChanged(config) { + const plugins = config.plugin; + const htmlPlugins = (plugins.html_resource_paths || []); + const jsPlugins = + this._handleMigrations(plugins.js_resource_paths || [], htmlPlugins); + const shouldLoadTheme = config.default_theme && + !Gerrit._isPluginPreloaded('preloaded:gerrit-theme'); + const themeToLoad = + shouldLoadTheme ? [config.default_theme] : []; + + // Theme should be loaded first if has one to have better UX + const pluginsPending = + themeToLoad.concat(jsPlugins, htmlPlugins); + + const pluginOpts = {}; + + if (shouldLoadTheme) { + // Theme needs to be loaded synchronous. + pluginOpts[config.default_theme] = {sync: true}; + } + + Gerrit._loadPlugins(pluginsPending, pluginOpts); + } + + /** + * Omit .js plugins that have .html counterparts. + * For example, if plugin provides foo.js and foo.html, skip foo.js. + */ + _handleMigrations(jsPlugins, htmlPlugins) { + return jsPlugins.filter(url => { + const counterpart = url.replace(/\.js$/, '.html'); + return !htmlPlugins.includes(counterpart); + }); + } +} + +customElements.define(GrPluginHost.is, GrPluginHost);
diff --git a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html index defd242..894c8b4 100644 --- a/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-plugin-host/gr-plugin-host_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-host</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-plugin-host.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-plugin-host.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-host.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,62 +40,64 @@ </template> </test-fixture> -<script> - suite('gr-plugin-host tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-host.js'; +suite('gr-plugin-host tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - sandbox.stub(document.body, 'appendChild'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('load plugins should be called', () => { - sandbox.stub(Gerrit, '_loadPlugins'); - element.config = { - plugin: { - html_resource_paths: ['plugins/foo/bar', 'plugins/baz'], - js_resource_paths: ['plugins/42'], - }, - }; - assert.isTrue(Gerrit._loadPlugins.calledOnce); - assert.isTrue(Gerrit._loadPlugins.calledWith([ - 'plugins/42', 'plugins/foo/bar', 'plugins/baz', - ], {})); - }); - - test('theme plugins should be loaded if enabled', () => { - sandbox.stub(Gerrit, '_loadPlugins'); - element.config = { - default_theme: 'gerrit-theme.html', - plugin: { - html_resource_paths: ['plugins/foo/bar', 'plugins/baz'], - js_resource_paths: ['plugins/42'], - }, - }; - assert.isTrue(Gerrit._loadPlugins.calledOnce); - assert.isTrue(Gerrit._loadPlugins.calledWith([ - 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz', - ], {'gerrit-theme.html': {sync: true}})); - }); - - test('skip theme if preloaded', () => { - sandbox.stub(Gerrit, '_isPluginPreloaded') - .withArgs('preloaded:gerrit-theme') - .returns(true); - sandbox.stub(Gerrit, '_loadPlugins'); - element.config = { - default_theme: '/oof', - plugin: {}, - }; - assert.isTrue(Gerrit._loadPlugins.calledOnce); - assert.isTrue(Gerrit._loadPlugins.calledWith([], {})); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + sandbox.stub(document.body, 'appendChild'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('load plugins should be called', () => { + sandbox.stub(Gerrit, '_loadPlugins'); + element.config = { + plugin: { + html_resource_paths: ['plugins/foo/bar', 'plugins/baz'], + js_resource_paths: ['plugins/42'], + }, + }; + assert.isTrue(Gerrit._loadPlugins.calledOnce); + assert.isTrue(Gerrit._loadPlugins.calledWith([ + 'plugins/42', 'plugins/foo/bar', 'plugins/baz', + ], {})); + }); + + test('theme plugins should be loaded if enabled', () => { + sandbox.stub(Gerrit, '_loadPlugins'); + element.config = { + default_theme: 'gerrit-theme.html', + plugin: { + html_resource_paths: ['plugins/foo/bar', 'plugins/baz'], + js_resource_paths: ['plugins/42'], + }, + }; + assert.isTrue(Gerrit._loadPlugins.calledOnce); + assert.isTrue(Gerrit._loadPlugins.calledWith([ + 'gerrit-theme.html', 'plugins/42', 'plugins/foo/bar', 'plugins/baz', + ], {'gerrit-theme.html': {sync: true}})); + }); + + test('skip theme if preloaded', () => { + sandbox.stub(Gerrit, '_isPluginPreloaded') + .withArgs('preloaded:gerrit-theme') + .returns(true); + sandbox.stub(Gerrit, '_loadPlugins'); + element.config = { + default_theme: '/oof', + plugin: {}, + }; + assert.isTrue(Gerrit._loadPlugins.calledOnce); + assert.isTrue(Gerrit._loadPlugins.calledWith([], {})); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js index 30bf6c8..db44cea 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup.js
@@ -14,13 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import '../../shared/gr-overlay/gr-overlay.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-plugin-popup_html.js'; + (function(window) { 'use strict'; /** @extends Polymer.Element */ - class GrPluginPopup extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { + class GrPluginPopup extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + static get is() { return 'gr-plugin-popup'; } get opened() {
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js index d084445..779cbad 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_html.js
@@ -1,31 +1,26 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> - -<dom-module id="gr-plugin-popup"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> - <gr-overlay id="overlay" with-backdrop> + <gr-overlay id="overlay" with-backdrop=""> <slot></slot> </gr-overlay> - </template> - <script src="gr-plugin-popup.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html index 1617cd5..00bbd52 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-plugin-popup_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-popup</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-plugin-popup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-plugin-popup.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-popup.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,39 +40,41 @@ </template> </test-fixture> -<script> - suite('gr-plugin-popup tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-plugin-popup.js'; +suite('gr-plugin-popup tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - stub('gr-overlay', { - open: sandbox.stub().returns(Promise.resolve()), - close: sandbox.stub(), - }); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('exists', () => { - assert.isOk(element); - }); - - test('open uses open() from gr-overlay', done => { - element.open().then(() => { - assert.isTrue(element.$.overlay.open.called); - done(); - }); - }); - - test('close uses close() from gr-overlay', () => { - element.close(); - assert.isTrue(element.$.overlay.close.called); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + stub('gr-overlay', { + open: sandbox.stub().returns(Promise.resolve()), + close: sandbox.stub(), }); }); + + teardown(() => { + sandbox.restore(); + }); + + test('exists', () => { + assert.isOk(element); + }); + + test('open uses open() from gr-overlay', done => { + element.open().then(() => { + assert.isTrue(element.$.overlay.open.called); + done(); + }); + }); + + test('close uses close() from gr-overlay', () => { + element.close(); + assert.isTrue(element.$.overlay.close.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html deleted file mode 100644 index a8bb06b..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.html +++ /dev/null
@@ -1,23 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="gr-plugin-popup.html"> - -<dom-module id="gr-popup-interface"> - <script src="gr-popup-interface.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js index c3588a1..e9d3e36 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface.js
@@ -14,6 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import './gr-plugin-popup.js'; +import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-popup-interface"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict'; @@ -35,7 +47,7 @@ } GrPopupInterface.prototype._getElement = function() { - return Polymer.dom(this._popup); + return dom(this._popup); }; /** @@ -52,12 +64,12 @@ .then(hookEl => { const popup = document.createElement('gr-plugin-popup'); if (this._moduleName) { - const el = Polymer.dom(popup).appendChild( + const el = dom(popup).appendChild( document.createElement(this._moduleName)); el.plugin = this.plugin; } - this._popup = Polymer.dom(hookEl).appendChild(popup); - Polymer.dom.flush(); + this._popup = dom(hookEl).appendChild(popup); + flush(); return this._popup.open().then(() => this); }); }
diff --git a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html index c1593b7..aeef29a 100644 --- a/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-popup-interface/gr-popup-interface_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-popup-interface</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-popup-interface.html"/> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-popup-interface.js"></script> +<script type="module" src="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-popup-interface.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="container"> <template> @@ -40,85 +46,94 @@ <template> <div id="barfoo">some test module</div> </template> - <script> - Polymer({is: 'gr-user-test-popup'}); - </script> + <script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-popup-interface.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +Polymer({is: 'gr-user-test-popup'}); +</script> </dom-module> -<script> - suite('gr-popup-interface tests', async () => { - await readyToTest(); - let container; - let instance; - let plugin; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-popup-interface.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-popup-interface tests', () => { + let container; + let instance; + let plugin; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + container = fixture('container'); + sandbox.stub(plugin, 'hook').returns({ + getLastAttached() { + return Promise.resolve(container); + }, + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('manual', () => { setup(() => { - sandbox = sinon.sandbox.create(); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - container = fixture('container'); - sandbox.stub(plugin, 'hook').returns({ - getLastAttached() { - return Promise.resolve(container); - }, + instance = new GrPopupInterface(plugin); + }); + + test('open', done => { + instance.open().then(api => { + assert.strictEqual(api, instance); + const manual = document.createElement('div'); + manual.id = 'foobar'; + manual.innerHTML = 'manual content'; + api._getElement().appendChild(manual); + flushAsynchronousOperations(); + assert.equal( + container.querySelector('#foobar').textContent, 'manual content'); + done(); }); }); - teardown(() => { - sandbox.restore(); - }); - - suite('manual', () => { - setup(() => { - instance = new GrPopupInterface(plugin); - }); - - test('open', done => { - instance.open().then(api => { - assert.strictEqual(api, instance); - const manual = document.createElement('div'); - manual.id = 'foobar'; - manual.innerHTML = 'manual content'; - api._getElement().appendChild(manual); - flushAsynchronousOperations(); - assert.equal( - container.querySelector('#foobar').textContent, 'manual content'); - done(); - }); - }); - - test('close', done => { - instance.open().then(api => { - assert.isTrue(api._getElement().node.opened); - api.close(); - assert.isFalse(api._getElement().node.opened); - done(); - }); - }); - }); - - suite('components', () => { - setup(() => { - instance = new GrPopupInterface(plugin, 'gr-user-test-popup'); - }); - - test('open', done => { - instance.open().then(api => { - assert.isNotNull( - Polymer.dom(container).querySelector('gr-user-test-popup')); - done(); - }); - }); - - test('close', done => { - instance.open().then(api => { - assert.isTrue(api._getElement().node.opened); - api.close(); - assert.isFalse(api._getElement().node.opened); - done(); - }); + test('close', done => { + instance.open().then(api => { + assert.isTrue(api._getElement().node.opened); + api.close(); + assert.isFalse(api._getElement().node.opened); + done(); }); }); }); + + suite('components', () => { + setup(() => { + instance = new GrPopupInterface(plugin, 'gr-user-test-popup'); + }); + + test('open', done => { + instance.open().then(api => { + assert.isNotNull( + dom(container).querySelector('gr-user-test-popup')); + done(); + }); + }); + + test('close', done => { + instance.open().then(api => { + assert.isTrue(api._getElement().node.opened); + api.close(); + assert.isFalse(api._getElement().node.opened); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js index 593c1e0..f9a2bdf 100644 --- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js +++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-plugin-repo-command.js
@@ -1,35 +1,35 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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 '../../../scripts/bundled-polymer.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../admin/gr-repo-command/gr-repo-command.html"> - -<dom-module id="gr-plugin-repo-command"> - <template> +import '../../admin/gr-repo-command/gr-repo-command.js'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; +Polymer({ + _template: html` <gr-repo-command title="[[title]]"> </gr-repo-command> - </template> - <script> - Polymer({ - is: 'gr-plugin-repo-command', - properties: { - title: String, - repoName: String, - config: Object, - }, - }); - </script> -</dom-module> +`, + + is: 'gr-plugin-repo-command', + + properties: { + title: String, + repoName: String, + config: Object, + }, +});
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html deleted file mode 100644 index b3f6aec..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.html +++ /dev/null
@@ -1,23 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="gr-plugin-repo-command.html"> - -<dom-module id="gr-repo-api"> - <script src="gr-repo-api.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js index b59cce6..6c1a3c8 100644 --- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api.js
@@ -14,6 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import './gr-plugin-repo-command.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-repo-api"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html index adc1de5..c177715 100644 --- a/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-repo-api/gr-repo-api_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="gr-repo-api.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../gr-endpoint-decorator/gr-endpoint-decorator.js"></script> +<script type="module" src="./gr-repo-api.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './gr-repo-api.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,52 +43,55 @@ </template> </test-fixture> -<script> - suite('gr-repo-api tests', async () => { - await readyToTest(); - let sandbox; - let repoApi; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './gr-repo-api.js'; +suite('gr-repo-api tests', () => { + let sandbox; + let repoApi; - setup(() => { - sandbox = sinon.sandbox.create(); - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - Gerrit._loadPlugins([]); - repoApi = plugin.project(); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + Gerrit._loadPlugins([]); + repoApi = plugin.project(); + }); - teardown(() => { - repoApi = null; - sandbox.restore(); - }); + teardown(() => { + repoApi = null; + sandbox.restore(); + }); - test('exists', () => { - assert.isOk(repoApi); - }); + test('exists', () => { + assert.isOk(repoApi); + }); - test('works', done => { - const attachedStub = sandbox.stub(); - const tapStub = sandbox.stub(); - repoApi - .createCommand('foo', attachedStub) - .onTap(tapStub); - const element = fixture('basic'); - flush(() => { - assert.isTrue(attachedStub.called); - const pluginCommand = element.shadowRoot - .querySelector('gr-plugin-repo-command'); - assert.isOk(pluginCommand); - const command = pluginCommand.shadowRoot - .querySelector('gr-repo-command'); - assert.isOk(command); - assert.equal(command.title, 'foo'); - assert.isFalse(tapStub.called); - MockInteractions.tap(command.shadowRoot - .querySelector('gr-button')); - assert.isTrue(tapStub.called); - done(); - }); + test('works', done => { + const attachedStub = sandbox.stub(); + const tapStub = sandbox.stub(); + repoApi + .createCommand('foo', attachedStub) + .onTap(tapStub); + const element = fixture('basic'); + flush(() => { + assert.isTrue(attachedStub.called); + const pluginCommand = element.shadowRoot + .querySelector('gr-plugin-repo-command'); + assert.isOk(pluginCommand); + const command = pluginCommand.shadowRoot + .querySelector('gr-repo-command'); + assert.isOk(command); + assert.equal(command.title, 'foo'); + assert.isFalse(tapStub.called); + MockInteractions.tap(command.shadowRoot + .querySelector('gr-button')); + assert.isTrue(tapStub.called); + done(); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html deleted file mode 100644 index 999ecfa..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.html +++ /dev/null
@@ -1,25 +0,0 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Settings - -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../settings/gr-settings-view/gr-settings-item.html"> -<link rel="import" href="../../settings/gr-settings-view/gr-settings-menu-item.html"> - -<dom-module id="gr-settings-api"> - <script src="gr-settings-api.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js index 5ed4c1a..a8bfccdd 100644 --- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api.js
@@ -14,6 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../settings/gr-settings-view/gr-settings-item.js'; +import '../../settings/gr-settings-view/gr-settings-menu-item.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-settings-api"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html index 2efd182..b0dbc3c 100644 --- a/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-settings-api/gr-settings-api_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="gr-settings-api.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../gr-endpoint-decorator/gr-endpoint-decorator.js"></script> +<script type="module" src="./gr-settings-api.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './gr-settings-api.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -39,51 +45,54 @@ </template> </test-fixture> -<script> - suite('gr-settings-api tests', async () => { - await readyToTest(); - let sandbox; - let settingsApi; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './gr-settings-api.js'; +suite('gr-settings-api tests', () => { + let sandbox; + let settingsApi; - setup(() => { - sandbox = sinon.sandbox.create(); - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - Gerrit._loadPlugins([]); - settingsApi = plugin.settings(); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + Gerrit._loadPlugins([]); + settingsApi = plugin.settings(); + }); - teardown(() => { - settingsApi = null; - sandbox.restore(); - }); + teardown(() => { + settingsApi = null; + sandbox.restore(); + }); - test('exists', () => { - assert.isOk(settingsApi); - }); + test('exists', () => { + assert.isOk(settingsApi); + }); - test('works', done => { - settingsApi - .title('foo') - .token('bar') - .module('some-settings-screen') - .build(); - const element = fixture('basic'); - flush(() => { - const [menuItemEl, itemEl] = element; - const menuItem = menuItemEl.shadowRoot - .querySelector('gr-settings-menu-item'); - assert.isOk(menuItem); - assert.equal(menuItem.title, 'foo'); - assert.equal(menuItem.href, '#x/testplugin/bar'); - const item = itemEl.shadowRoot - .querySelector('gr-settings-item'); - assert.isOk(item); - assert.equal(item.title, 'foo'); - assert.equal(item.anchor, 'x/testplugin/bar'); - done(); - }); + test('works', done => { + settingsApi + .title('foo') + .token('bar') + .module('some-settings-screen') + .build(); + const element = fixture('basic'); + flush(() => { + const [menuItemEl, itemEl] = element; + const menuItem = menuItemEl.shadowRoot + .querySelector('gr-settings-menu-item'); + assert.isOk(menuItem); + assert.equal(menuItem.title, 'foo'); + assert.equal(menuItem.href, '#x/testplugin/bar'); + const item = itemEl.shadowRoot + .querySelector('gr-settings-item'); + assert.isOk(item); + assert.equal(item.title, 'foo'); + assert.equal(item.anchor, 'x/testplugin/bar'); + done(); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html deleted file mode 100644 index 74b87c8..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html +++ /dev/null
@@ -1,18 +0,0 @@ -<!-- -@license -Copyright (C) 2019 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. ---> - -<script src="gr-styles-api.js"></script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html index feb59fe..a69db8d 100644 --- a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html
@@ -19,31 +19,75 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-admin-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="gr-styles-api.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../shared/gr-js-api-interface/gr-js-api-interface.js"></script> +<script type="module" src="./gr-styles-api.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import './gr-styles-api.js'; +void(0); +</script> <dom-module id="gr-style-test-element"> <template> <div id="wrapper"></div> </template> - <script> - Polymer({is: 'gr-style-test-element'}); - </script> + <script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import './gr-styles-api.js'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +Polymer({is: 'gr-style-test-element'}); +</script> </dom-module> -<script> - suite('gr-styles-api tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../shared/gr-js-api-interface/gr-js-api-interface.js'; +import './gr-styles-api.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-styles-api tests', () => { + let sandbox; + let stylesApi; + + setup(() => { + sandbox = sinon.sandbox.create(); + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + Gerrit._loadPlugins([]); + stylesApi = plugin.styles(); + }); + + teardown(() => { + stylesApi = null; + sandbox.restore(); + }); + + test('exists', () => { + assert.isOk(stylesApi); + }); + + test('css', () => { + const styleObject = stylesApi.css('background: red'); + assert.isDefined(styleObject); + }); + + suite('GrStyleObject tests', () => { let sandbox; let stylesApi; + let displayInlineStyle; + let displayNoneStyle; setup(() => { sandbox = sinon.sandbox.create(); @@ -52,133 +96,104 @@ 'http://test.com/plugins/testplugin/static/test.js'); Gerrit._loadPlugins([]); stylesApi = plugin.styles(); + displayInlineStyle = stylesApi.css('display: inline'); + displayNoneStyle = stylesApi.css('display: none'); }); teardown(() => { + displayInlineStyle = null; + displayNoneStyle = null; stylesApi = null; sandbox.restore(); }); - test('exists', () => { - assert.isOk(stylesApi); + function createNestedElements(parentElement) { + /* parentElement + * |--- element1 + * |--- element2 + * |--- element3 + **/ + const element1 = document.createElement('div'); + const element2 = document.createElement('div'); + const element3 = document.createElement('div'); + dom(parentElement).appendChild(element1); + dom(parentElement).appendChild(element2); + dom(element2).appendChild(element3); + + return [element1, element2, element3]; + } + + test('getClassName - body level elements', () => { + const bodyLevelElements = createNestedElements(document.body); + + testGetClassName(bodyLevelElements); }); - test('css', () => { - const styleObject = stylesApi.css('background: red'); - assert.isDefined(styleObject); + test('getClassName - elements inside polymer element', () => { + const polymerElement = document.createElement('gr-style-test-element'); + dom(document.body).appendChild(polymerElement); + const contentElements = createNestedElements(polymerElement.$.wrapper); + + testGetClassName(contentElements); }); - suite('GrStyleObject tests', () => { - let sandbox; - let stylesApi; - let displayInlineStyle; - let displayNoneStyle; + function testGetClassName(elements) { + assertAllElementsHaveDefaultStyle(elements); - setup(() => { - sandbox = sinon.sandbox.create(); - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - Gerrit._loadPlugins([]); - stylesApi = plugin.styles(); - displayInlineStyle = stylesApi.css('display: inline'); - displayNoneStyle = stylesApi.css('display: none'); - }); + const className1 = displayInlineStyle.getClassName(elements[0]); + const className2 = displayNoneStyle.getClassName(elements[1]); + const className3 = displayInlineStyle.getClassName(elements[2]); - teardown(() => { - displayInlineStyle = null; - displayNoneStyle = null; - stylesApi = null; - sandbox.restore(); - }); + assert.notEqual(className2, className1); + assert.equal(className3, className1); - function createNestedElements(parentElement) { - /* parentElement - * |--- element1 - * |--- element2 - * |--- element3 - **/ - const element1 = document.createElement('div'); - const element2 = document.createElement('div'); - const element3 = document.createElement('div'); - Polymer.dom(parentElement).appendChild(element1); - Polymer.dom(parentElement).appendChild(element2); - Polymer.dom(element2).appendChild(element3); + assertAllElementsHaveDefaultStyle(elements); - return [element1, element2, element3]; + elements[0].classList.add(className1); + elements[1].classList.add(className2); + elements[2].classList.add(className1); + + assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']); + } + + test('apply - body level elements', () => { + const bodyLevelElements = createNestedElements(document.body); + + testApply(bodyLevelElements); + }); + + test('apply - elements inside polymer element', () => { + const polymerElement = document.createElement('gr-style-test-element'); + dom(document.body).appendChild(polymerElement); + const contentElements = createNestedElements(polymerElement.$.wrapper); + + testApply(contentElements); + }); + + function testApply(elements) { + assertAllElementsHaveDefaultStyle(elements); + displayInlineStyle.apply(elements[0]); + displayNoneStyle.apply(elements[1]); + displayInlineStyle.apply(elements[2]); + assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']); + } + + function assertAllElementsHaveDefaultStyle(elements) { + for (const element of elements) { + assert.equal(getComputedStyle(element).getPropertyValue('display'), + 'block'); } + } - test('getClassName - body level elements', () => { - const bodyLevelElements = createNestedElements(document.body); - - testGetClassName(bodyLevelElements); - }); - - test('getClassName - elements inside polymer element', () => { - const polymerElement = document.createElement('gr-style-test-element'); - Polymer.dom(document.body).appendChild(polymerElement); - const contentElements = createNestedElements(polymerElement.$.wrapper); - - testGetClassName(contentElements); - }); - - function testGetClassName(elements) { - assertAllElementsHaveDefaultStyle(elements); - - const className1 = displayInlineStyle.getClassName(elements[0]); - const className2 = displayNoneStyle.getClassName(elements[1]); - const className3 = displayInlineStyle.getClassName(elements[2]); - - assert.notEqual(className2, className1); - assert.equal(className3, className1); - - assertAllElementsHaveDefaultStyle(elements); - - elements[0].classList.add(className1); - elements[1].classList.add(className2); - elements[2].classList.add(className1); - - assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']); - } - - test('apply - body level elements', () => { - const bodyLevelElements = createNestedElements(document.body); - - testApply(bodyLevelElements); - }); - - test('apply - elements inside polymer element', () => { - const polymerElement = document.createElement('gr-style-test-element'); - Polymer.dom(document.body).appendChild(polymerElement); - const contentElements = createNestedElements(polymerElement.$.wrapper); - - testApply(contentElements); - }); - - function testApply(elements) { - assertAllElementsHaveDefaultStyle(elements); - displayInlineStyle.apply(elements[0]); - displayNoneStyle.apply(elements[1]); - displayInlineStyle.apply(elements[2]); - assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']); - } - - function assertAllElementsHaveDefaultStyle(elements) { - for (const element of elements) { - assert.equal(getComputedStyle(element).getPropertyValue('display'), - 'block'); + function assertDisplayPropertyValues(elements, expectedDisplayValues) { + for (const key in elements) { + if (elements.hasOwnProperty(key)) { + assert.equal( + getComputedStyle(elements[key]).getPropertyValue('display'), + expectedDisplayValues[key]); } } - - function assertDisplayPropertyValues(elements, expectedDisplayValues) { - for (const key in elements) { - if (elements.hasOwnProperty(key)) { - assert.equal( - getComputedStyle(elements[key]).getPropertyValue('display'), - expectedDisplayValues[key]); - } - } - } - }); + } }); +}); </script>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js index f0eacd2..411a7c8 100644 --- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js +++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-custom-plugin-header.js
@@ -1,24 +1,25 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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 '../../../scripts/bundled-polymer.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-custom-plugin-header"> - <template> +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; +Polymer({ + _template: html` <style> img { width: 1em; @@ -30,17 +31,15 @@ } </style> <span> - <img src="[[logoUrl]]" hidden$="[[!logoUrl]]"> + <img src="[[logoUrl]]" hidden\$="[[!logoUrl]]"> <span class="title">[[title]]</span> </span> - </template> - <script> - Polymer({ - is: 'gr-custom-plugin-header', - properties: { - logoUrl: String, - title: String, - }, - }); - </script> -</dom-module> +`, + + is: 'gr-custom-plugin-header', + + properties: { + logoUrl: String, + title: String, + }, +});
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html deleted file mode 100644 index ef1c9d4..0000000 --- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.html +++ /dev/null
@@ -1,23 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="gr-custom-plugin-header.html"> - -<dom-module id="gr-theme-api"> - <script src="gr-theme-api.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js index d145f52f..8da680b 100644 --- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js +++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api.js
@@ -14,6 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +import './gr-custom-plugin-header.js'; +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-theme-api"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html index 80853f5..6428f90 100644 --- a/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html +++ b/polygerrit-ui/app/elements/plugins/gr-theme-api/gr-theme-api_test.html
@@ -19,16 +19,22 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-theme-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="gr-theme-api.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../gr-endpoint-decorator/gr-endpoint-decorator.js"></script> +<script type="module" src="./gr-theme-api.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './gr-theme-api.js'; +void(0); +</script> <test-fixture id="header-title"> <template> @@ -38,50 +44,53 @@ </template> </test-fixture> -<script> - suite('gr-theme-api tests', async () => { - await readyToTest(); - let sandbox; - let theme; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-endpoint-decorator/gr-endpoint-decorator.js'; +import './gr-theme-api.js'; +suite('gr-theme-api tests', () => { + let sandbox; + let theme; + + setup(() => { + sandbox = sinon.sandbox.create(); + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + theme = plugin.theme(); + }); + + teardown(() => { + theme = null; + sandbox.restore(); + }); + + test('exists', () => { + assert.isOk(theme); + }); + + suite('header-title', () => { + let customHeader; setup(() => { - sandbox = sinon.sandbox.create(); - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - theme = plugin.theme(); - }); - - teardown(() => { - theme = null; - sandbox.restore(); - }); - - test('exists', () => { - assert.isOk(theme); - }); - - suite('header-title', () => { - let customHeader; - - setup(() => { - fixture('header-title'); - stub('gr-custom-plugin-header', { - /** @override */ - ready() { customHeader = this; }, - }); - Gerrit._loadPlugins([]); + fixture('header-title'); + stub('gr-custom-plugin-header', { + /** @override */ + ready() { customHeader = this; }, }); + Gerrit._loadPlugins([]); + }); - test('sets logo and title', done => { - theme.setHeaderLogoAndTitle('foo.jpg', 'bar'); - flush(() => { - assert.isNotNull(customHeader); - assert.equal(customHeader.logoUrl, 'foo.jpg'); - assert.equal(customHeader.title, 'bar'); - done(); - }); + test('sets logo and title', done => { + theme.setHeaderLogoAndTitle('foo.jpg', 'bar'); + flush(() => { + assert.isNotNull(customHeader); + assert.equal(customHeader.logoUrl, 'foo.jpg'); + assert.equal(customHeader.title, 'bar'); + done(); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js index 7bf641d..c425318 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -14,195 +14,208 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-input/iron-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../shared/gr-avatar/gr-avatar.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-info_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrAccountInfo extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-account-info'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when account details are changed. + * + * @event account-detail-update */ - class GrAccountInfo extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-info'; } - /** - * Fired when account details are changed. - * - * @event account-detail-update - */ - static get properties() { - return { - usernameMutable: { - type: Boolean, - notify: true, - computed: '_computeUsernameMutable(_serverConfig, _account.username)', - }, - nameMutable: { - type: Boolean, - notify: true, - computed: '_computeNameMutable(_serverConfig)', - }, - hasUnsavedChanges: { - type: Boolean, - notify: true, - computed: '_computeHasUnsavedChanges(_hasNameChange, ' + - '_hasUsernameChange, _hasStatusChange)', - }, + static get properties() { + return { + usernameMutable: { + type: Boolean, + notify: true, + computed: '_computeUsernameMutable(_serverConfig, _account.username)', + }, + nameMutable: { + type: Boolean, + notify: true, + computed: '_computeNameMutable(_serverConfig)', + }, + hasUnsavedChanges: { + type: Boolean, + notify: true, + computed: '_computeHasUnsavedChanges(_hasNameChange, ' + + '_hasUsernameChange, _hasStatusChange)', + }, - _hasNameChange: Boolean, - _hasUsernameChange: Boolean, - _hasStatusChange: Boolean, - _loading: { - type: Boolean, - value: false, - }, - _saving: { - type: Boolean, - value: false, - }, - /** @type {?} */ - _account: Object, - _serverConfig: Object, - _username: { - type: String, - observer: '_usernameChanged', - }, - _avatarChangeUrl: { - type: String, - value: '', - }, - }; + _hasNameChange: Boolean, + _hasUsernameChange: Boolean, + _hasStatusChange: Boolean, + _loading: { + type: Boolean, + value: false, + }, + _saving: { + type: Boolean, + value: false, + }, + /** @type {?} */ + _account: Object, + _serverConfig: Object, + _username: { + type: String, + observer: '_usernameChanged', + }, + _avatarChangeUrl: { + type: String, + value: '', + }, + }; + } + + static get observers() { + return [ + '_nameChanged(_account.name)', + '_statusChanged(_account.status)', + ]; + } + + loadData() { + const promises = []; + + this._loading = true; + + promises.push(this.$.restAPI.getConfig().then(config => { + this._serverConfig = config; + })); + + promises.push(this.$.restAPI.getAccount().then(account => { + this._hasNameChange = false; + this._hasUsernameChange = 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; + })); + + promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => { + this._avatarChangeUrl = url; + })); + + return Promise.all(promises).then(() => { + this._loading = false; + }); + } + + save() { + if (!this.hasUnsavedChanges) { + return Promise.resolve(); } - static get observers() { - return [ - '_nameChanged(_account.name)', - '_statusChanged(_account.status)', - ]; + 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.bind(this)) + .then(this._maybeSetStatus.bind(this)) + .then(() => { + this._hasNameChange = false; + this._hasStatusChange = false; + this._saving = false; + this.fire('account-detail-update'); + }); + } + + _maybeSetName() { + return this._hasNameChange && this.nameMutable ? + this.$.restAPI.setAccountName(this._account.name) : + Promise.resolve(); + } + + _maybeSetUsername() { + return this._hasUsernameChange && this.usernameMutable ? + this.$.restAPI.setAccountUsername(this._username) : + Promise.resolve(); + } + + _maybeSetStatus() { + return this._hasStatusChange ? + this.$.restAPI.setAccountStatus(this._account.status) : + Promise.resolve(); + } + + _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) { + return nameChanged || usernameChanged || statusChanged; + } + + _computeUsernameMutable(config, username) { + // Polymer 2: check for undefined + if ([ + config, + username, + ].some(arg => arg === undefined)) { + return undefined; } - loadData() { - const promises = []; + // Username may not be changed once it is set. + return config.auth.editable_account_fields.includes('USER_NAME') && + !username; + } - this._loading = true; + _computeNameMutable(config) { + return config.auth.editable_account_fields.includes('FULL_NAME'); + } - promises.push(this.$.restAPI.getConfig().then(config => { - this._serverConfig = config; - })); + _statusChanged() { + if (this._loading) { return; } + this._hasStatusChange = true; + } - promises.push(this.$.restAPI.getAccount().then(account => { - this._hasNameChange = false; - this._hasUsernameChange = 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; - })); + _usernameChanged() { + if (this._loading || !this._account) { return; } + this._hasUsernameChange = + (this._account.username || '') !== (this._username || ''); + } - promises.push(this.$.restAPI.getAvatarChangeUrl().then(url => { - this._avatarChangeUrl = url; - })); + _nameChanged() { + if (this._loading) { return; } + this._hasNameChange = true; + } - return Promise.all(promises).then(() => { - this._loading = false; - }); - } - - save() { - if (!this.hasUnsavedChanges) { - return Promise.resolve(); - } - - 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.bind(this)) - .then(this._maybeSetStatus.bind(this)) - .then(() => { - this._hasNameChange = false; - this._hasStatusChange = false; - this._saving = false; - this.fire('account-detail-update'); - }); - } - - _maybeSetName() { - return this._hasNameChange && this.nameMutable ? - this.$.restAPI.setAccountName(this._account.name) : - Promise.resolve(); - } - - _maybeSetUsername() { - return this._hasUsernameChange && this.usernameMutable ? - this.$.restAPI.setAccountUsername(this._username) : - Promise.resolve(); - } - - _maybeSetStatus() { - return this._hasStatusChange ? - this.$.restAPI.setAccountStatus(this._account.status) : - Promise.resolve(); - } - - _computeHasUnsavedChanges(nameChanged, usernameChanged, statusChanged) { - return nameChanged || usernameChanged || statusChanged; - } - - _computeUsernameMutable(config, username) { - // Polymer 2: check for undefined - if ([ - config, - username, - ].some(arg => arg === undefined)) { - return undefined; - } - - // Username may not be changed once it is set. - return config.auth.editable_account_fields.includes('USER_NAME') && - !username; - } - - _computeNameMutable(config) { - return config.auth.editable_account_fields.includes('FULL_NAME'); - } - - _statusChanged() { - if (this._loading) { return; } - this._hasStatusChange = true; - } - - _usernameChanged() { - if (this._loading || !this._account) { return; } - this._hasUsernameChange = - (this._account.username || '') !== (this._username || ''); - } - - _nameChanged() { - if (this._loading) { return; } - this._hasNameChange = true; - } - - _handleKeydown(e) { - if (e.keyCode === 13) { // Enter - e.stopPropagation(); - this.save(); - } - } - - _hideAvatarChangeUrl(avatarChangeUrl) { - if (!avatarChangeUrl) { - return 'hide'; - } - - return ''; + _handleKeydown(e) { + if (e.keyCode === 13) { // Enter + e.stopPropagation(); + this.save(); } } - customElements.define(GrAccountInfo.is, GrAccountInfo); -})(); + _hideAvatarChangeUrl(avatarChangeUrl) { + if (!avatarChangeUrl) { + return 'hide'; + } + + return ''; + } +} + +customElements.define(GrAccountInfo.is, GrAccountInfo);
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js index e685030..6e37c25 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_html.js
@@ -1,34 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-avatar/gr-avatar.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - - -<dom-module id="gr-account-info"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> gr-avatar { height: 120px; @@ -47,14 +35,13 @@ <section> <span class="title"></span> <span class="value"> - <gr-avatar account="[[_account]]" - image-size="120"></gr-avatar> + <gr-avatar account="[[_account]]" image-size="120"></gr-avatar> </span> </section> - <section class$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]"> + <section class\$="[[_hideAvatarChangeUrl(_avatarChangeUrl)]]"> <span class="title"></span> <span class="value"> - <a href$="[[_avatarChangeUrl]]"> + <a href\$="[[_avatarChangeUrl]]"> Change avatar </a> </span> @@ -70,68 +57,35 @@ <section> <span class="title">Registered</span> <span class="value"> - <gr-date-formatter - has-tooltip - date-str="[[_account.registered_on]]"></gr-date-formatter> + <gr-date-formatter has-tooltip="" 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}}"> - <input - is="iron-input" - id="usernameInput" - disabled="[[_saving]]" - on-keydown="_handleKeydown" - bind-value="{{_username}}"> + <span hidden\$="[[usernameMutable]]" class="value">[[_username]]</span> + <span hidden\$="[[!usernameMutable]]" class="value"> + <iron-input on-keydown="_handleKeydown" bind-value="{{_username}}"> + <input is="iron-input" id="usernameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_username}}"> </iron-input> </span> </section> <section id="nameSection"> <span class="title">Full name</span> - <span - hidden$="[[nameMutable]]" - class="value">[[_account.name]]</span> - <span - hidden$="[[!nameMutable]]" - class="value"> - <iron-input - on-keydown="_handleKeydown" - bind-value="{{_account.name}}"> - <input - is="iron-input" - id="nameInput" - disabled="[[_saving]]" - on-keydown="_handleKeydown" - bind-value="{{_account.name}}"> + <span hidden\$="[[nameMutable]]" class="value">[[_account.name]]</span> + <span hidden\$="[[!nameMutable]]" class="value"> + <iron-input on-keydown="_handleKeydown" bind-value="{{_account.name}}"> + <input is="iron-input" id="nameInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.name}}"> </iron-input> </span> </section> <section> <span class="title">Status (e.g. "Vacation")</span> <span class="value"> - <iron-input - on-keydown="_handleKeydown" - bind-value="{{_account.status}}"> - <input - is="iron-input" - id="statusInput" - disabled="[[_saving]]" - on-keydown="_handleKeydown" - bind-value="{{_account.status}}"> + <iron-input on-keydown="_handleKeydown" bind-value="{{_account.status}}"> + <input is="iron-input" id="statusInput" disabled="[[_saving]]" on-keydown="_handleKeydown" bind-value="{{_account.status}}"> </iron-input> </span> </section> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-account-info.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html index 80c7399..640ae37 100644 --- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html +++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-info</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-account-info.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-account-info.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-info.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,310 +40,313 @@ </template> </test-fixture> -<script> - suite('gr-account-info tests', async () => { - await readyToTest(); - let element; - let account; - let config; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-info.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-account-info tests', () => { + let element; + let account; + let config; + let sandbox; - function valueOf(title) { - const sections = Polymer.dom(element.root).querySelectorAll('section'); - let titleEl; - for (let i = 0; i < sections.length; i++) { - titleEl = sections[i].querySelector('.title'); - if (titleEl.textContent === title) { - return sections[i].querySelector('.value'); - } + function valueOf(title) { + const sections = dom(element.root).querySelectorAll('section'); + let titleEl; + for (let i = 0; i < sections.length; i++) { + titleEl = sections[i].querySelector('.title'); + if (titleEl.textContent === title) { + return sections[i].querySelector('.value'); } } + } - setup(done => { - sandbox = sinon.sandbox.create(); - account = { - _account_id: 123, - name: 'user name', - email: 'user@email', - username: 'user username', - registered: '2000-01-01 00:00:00.000000000', - }; - config = {auth: {editable_account_fields: []}}; + setup(done => { + sandbox = sinon.sandbox.create(); + account = { + _account_id: 123, + name: 'user name', + email: 'user@email', + username: 'user username', + registered: '2000-01-01 00:00:00.000000000', + }; + config = {auth: {editable_account_fields: []}}; - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(account); }, - getConfig() { return Promise.resolve(config); }, - getPreferences() { - return Promise.resolve({time_format: 'HHMM_12'}); - }, - }); - element = fixture('basic'); - // Allow the element to render. - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(account); }, + getConfig() { return Promise.resolve(config); }, + getPreferences() { + return Promise.resolve({time_format: 'HHMM_12'}); + }, }); + element = fixture('basic'); + // Allow the element to render. + element.loadData().then(() => { flush(done); }); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('basic account info render', () => { - assert.isFalse(element._loading); + test('basic account info render', () => { + assert.isFalse(element._loading); - assert.equal(valueOf('ID').textContent, account._account_id); - assert.equal(valueOf('Email').textContent, account.email); - assert.equal(valueOf('Username').textContent, account.username); - }); + assert.equal(valueOf('ID').textContent, account._account_id); + assert.equal(valueOf('Email').textContent, account.email); + assert.equal(valueOf('Username').textContent, account.username); + }); - test('full name render (immutable)', () => { - const section = element.$.nameSection; - const displaySpan = section.querySelectorAll('.value')[0]; - const inputSpan = section.querySelectorAll('.value')[1]; + test('full name render (immutable)', () => { + const section = 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.isFalse(element.nameMutable); + assert.isFalse(displaySpan.hasAttribute('hidden')); + assert.equal(displaySpan.textContent, account.name); + assert.isTrue(inputSpan.hasAttribute('hidden')); + }); - test('full name render (mutable)', () => { + test('full name render (mutable)', () => { + element.set('_serverConfig', + {auth: {editable_account_fields: ['FULL_NAME']}}); + + const section = element.$.nameSection; + const displaySpan = section.querySelectorAll('.value')[0]; + const inputSpan = section.querySelectorAll('.value')[1]; + + assert.isTrue(element.nameMutable); + assert.isTrue(displaySpan.hasAttribute('hidden')); + assert.equal(element.$.nameInput.bindValue, account.name); + assert.isFalse(inputSpan.hasAttribute('hidden')); + }); + + test('username render (immutable)', () => { + const section = 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')); + }); + + test('username render (mutable)', () => { + element.set('_serverConfig', + {auth: {editable_account_fields: ['USER_NAME']}}); + element.set('_account.username', ''); + element.set('_username', ''); + + const section = element.$.usernameSection; + const displaySpan = section.querySelectorAll('.value')[0]; + const inputSpan = section.querySelectorAll('.value')[1]; + + assert.isTrue(element.usernameMutable); + assert.isTrue(displaySpan.hasAttribute('hidden')); + assert.equal(element.$.usernameInput.bindValue, account.username); + assert.isFalse(inputSpan.hasAttribute('hidden')); + }); + + suite('account info edit', () => { + let nameChangedSpy; + let usernameChangedSpy; + let statusChangedSpy; + let nameStub; + let usernameStub; + let statusStub; + + setup(() => { + nameChangedSpy = sandbox.spy(element, '_nameChanged'); + usernameChangedSpy = sandbox.spy(element, '_usernameChanged'); + statusChangedSpy = sandbox.spy(element, '_statusChanged'); element.set('_serverConfig', - {auth: {editable_account_fields: ['FULL_NAME']}}); + {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}}); - const section = element.$.nameSection; - const displaySpan = section.querySelectorAll('.value')[0]; - const inputSpan = section.querySelectorAll('.value')[1]; + nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', + name => Promise.resolve()); + usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername', + username => Promise.resolve()); + statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', + status => Promise.resolve()); + }); + test('name', done => { assert.isTrue(element.nameMutable); - assert.isTrue(displaySpan.hasAttribute('hidden')); - assert.equal(element.$.nameInput.bindValue, account.name); - assert.isFalse(inputSpan.hasAttribute('hidden')); - }); + assert.isFalse(element.hasUnsavedChanges); - test('username render (immutable)', () => { - const section = element.$.usernameSection; - const displaySpan = section.querySelectorAll('.value')[0]; - const inputSpan = section.querySelectorAll('.value')[1]; + element.set('_account.name', 'new name'); - assert.isFalse(element.usernameMutable); - assert.isFalse(displaySpan.hasAttribute('hidden')); - assert.equal(displaySpan.textContent, account.username); - assert.isTrue(inputSpan.hasAttribute('hidden')); - }); + assert.isTrue(nameChangedSpy.called); + assert.isFalse(statusChangedSpy.called); + assert.isTrue(element.hasUnsavedChanges); - test('username render (mutable)', () => { - element.set('_serverConfig', - {auth: {editable_account_fields: ['USER_NAME']}}); - element.set('_account.username', ''); - element.set('_username', ''); - - const section = element.$.usernameSection; - const displaySpan = section.querySelectorAll('.value')[0]; - const inputSpan = section.querySelectorAll('.value')[1]; - - assert.isTrue(element.usernameMutable); - assert.isTrue(displaySpan.hasAttribute('hidden')); - assert.equal(element.$.usernameInput.bindValue, account.username); - assert.isFalse(inputSpan.hasAttribute('hidden')); - }); - - suite('account info edit', () => { - let nameChangedSpy; - let usernameChangedSpy; - let statusChangedSpy; - let nameStub; - let usernameStub; - let statusStub; - - setup(() => { - nameChangedSpy = sandbox.spy(element, '_nameChanged'); - usernameChangedSpy = sandbox.spy(element, '_usernameChanged'); - statusChangedSpy = sandbox.spy(element, '_statusChanged'); - element.set('_serverConfig', - {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}}); - - nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', - name => Promise.resolve()); - usernameStub = sandbox.stub(element.$.restAPI, 'setAccountUsername', - username => Promise.resolve()); - statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', - status => Promise.resolve()); - }); - - test('name', done => { - assert.isTrue(element.nameMutable); - assert.isFalse(element.hasUnsavedChanges); - - element.set('_account.name', 'new name'); - - assert.isTrue(nameChangedSpy.called); - assert.isFalse(statusChangedSpy.called); - assert.isTrue(element.hasUnsavedChanges); - - element.save().then(() => { - assert.isFalse(usernameStub.called); - assert.isTrue(nameStub.called); - assert.isFalse(statusStub.called); - nameStub.lastCall.returnValue.then(() => { - assert.equal(nameStub.lastCall.args[0], 'new name'); - done(); - }); - }); - }); - - test('username', done => { - element.set('_account.username', ''); - element._hasUsernameChange = false; - assert.isTrue(element.usernameMutable); - - element.set('_username', 'new username'); - - assert.isTrue(usernameChangedSpy.called); - assert.isFalse(statusChangedSpy.called); - assert.isTrue(element.hasUnsavedChanges); - - element.save().then(() => { - assert.isTrue(usernameStub.called); - assert.isFalse(nameStub.called); - assert.isFalse(statusStub.called); - usernameStub.lastCall.returnValue.then(() => { - assert.equal(usernameStub.lastCall.args[0], 'new username'); - done(); - }); - }); - }); - - test('status', done => { - assert.isFalse(element.hasUnsavedChanges); - - element.set('_account.status', 'new status'); - - assert.isFalse(nameChangedSpy.called); - assert.isTrue(statusChangedSpy.called); - assert.isTrue(element.hasUnsavedChanges); - - element.save().then(() => { - assert.isFalse(usernameStub.called); - assert.isTrue(statusStub.called); - assert.isFalse(nameStub.called); - statusStub.lastCall.returnValue.then(() => { - assert.equal(statusStub.lastCall.args[0], 'new status'); - done(); - }); - }); - }); - }); - - suite('edit name and status', () => { - let nameChangedSpy; - let statusChangedSpy; - let nameStub; - let statusStub; - - setup(() => { - nameChangedSpy = sandbox.spy(element, '_nameChanged'); - statusChangedSpy = sandbox.spy(element, '_statusChanged'); - element.set('_serverConfig', - {auth: {editable_account_fields: ['FULL_NAME']}}); - - nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', - name => Promise.resolve()); - statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', - status => Promise.resolve()); - sandbox.stub(element.$.restAPI, 'setAccountUsername', - username => Promise.resolve()); - }); - - test('set name and status', done => { - assert.isTrue(element.nameMutable); - assert.isFalse(element.hasUnsavedChanges); - - element.set('_account.name', 'new name'); - - assert.isTrue(nameChangedSpy.called); - - element.set('_account.status', 'new status'); - - assert.isTrue(statusChangedSpy.called); - - assert.isTrue(element.hasUnsavedChanges); - - element.save().then(() => { - assert.isTrue(statusStub.called); - assert.isTrue(nameStub.called); - + element.save().then(() => { + assert.isFalse(usernameStub.called); + assert.isTrue(nameStub.called); + assert.isFalse(statusStub.called); + nameStub.lastCall.returnValue.then(() => { assert.equal(nameStub.lastCall.args[0], 'new name'); - - assert.equal(statusStub.lastCall.args[0], 'new status'); - done(); }); }); }); - suite('set status but read name', () => { - let statusChangedSpy; - let statusStub; + test('username', done => { + element.set('_account.username', ''); + element._hasUsernameChange = false; + assert.isTrue(element.usernameMutable); - setup(() => { - statusChangedSpy = sandbox.spy(element, '_statusChanged'); - element.set('_serverConfig', - {auth: {editable_account_fields: []}}); + element.set('_username', 'new username'); - statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', - status => Promise.resolve()); - }); + assert.isTrue(usernameChangedSpy.called); + assert.isFalse(statusChangedSpy.called); + assert.isTrue(element.hasUnsavedChanges); - test('read full name but set status', done => { - const section = element.$.nameSection; - const displaySpan = section.querySelectorAll('.value')[0]; - const inputSpan = section.querySelectorAll('.value')[1]; - - assert.isFalse(element.nameMutable); - - assert.isFalse(element.hasUnsavedChanges); - - assert.isFalse(displaySpan.hasAttribute('hidden')); - assert.equal(displaySpan.textContent, account.name); - assert.isTrue(inputSpan.hasAttribute('hidden')); - - element.set('_account.status', 'new status'); - - assert.isTrue(statusChangedSpy.called); - - assert.isTrue(element.hasUnsavedChanges); - - element.save().then(() => { - assert.isTrue(statusStub.called); - statusStub.lastCall.returnValue.then(() => { - assert.equal(statusStub.lastCall.args[0], 'new status'); - done(); - }); + element.save().then(() => { + assert.isTrue(usernameStub.called); + assert.isFalse(nameStub.called); + assert.isFalse(statusStub.called); + usernameStub.lastCall.returnValue.then(() => { + assert.equal(usernameStub.lastCall.args[0], 'new username'); + done(); }); }); }); - test('_usernameChanged compares usernames with loose equality', () => { - element._account = {}; - 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(); - flushAsynchronousOperations(); + test('status', done => { + assert.isFalse(element.hasUnsavedChanges); - assert.isFalse(element._hasUsernameChange); + element.set('_account.status', 'new status'); - element.set('_username', 'test'); - flushAsynchronousOperations(); + assert.isFalse(nameChangedSpy.called); + assert.isTrue(statusChangedSpy.called); + assert.isTrue(element.hasUnsavedChanges); - assert.isTrue(element._hasUsernameChange); - }); - - test('_hideAvatarChangeUrl', () => { - assert.equal(element._hideAvatarChangeUrl(''), 'hide'); - - assert.equal(element._hideAvatarChangeUrl('https://example.com'), ''); + element.save().then(() => { + assert.isFalse(usernameStub.called); + assert.isTrue(statusStub.called); + assert.isFalse(nameStub.called); + statusStub.lastCall.returnValue.then(() => { + assert.equal(statusStub.lastCall.args[0], 'new status'); + done(); + }); + }); }); }); + + suite('edit name and status', () => { + let nameChangedSpy; + let statusChangedSpy; + let nameStub; + let statusStub; + + setup(() => { + nameChangedSpy = sandbox.spy(element, '_nameChanged'); + statusChangedSpy = sandbox.spy(element, '_statusChanged'); + element.set('_serverConfig', + {auth: {editable_account_fields: ['FULL_NAME']}}); + + nameStub = sandbox.stub(element.$.restAPI, 'setAccountName', + name => Promise.resolve()); + statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', + status => Promise.resolve()); + sandbox.stub(element.$.restAPI, 'setAccountUsername', + username => Promise.resolve()); + }); + + test('set name and status', done => { + assert.isTrue(element.nameMutable); + assert.isFalse(element.hasUnsavedChanges); + + element.set('_account.name', 'new name'); + + assert.isTrue(nameChangedSpy.called); + + element.set('_account.status', 'new status'); + + assert.isTrue(statusChangedSpy.called); + + assert.isTrue(element.hasUnsavedChanges); + + element.save().then(() => { + assert.isTrue(statusStub.called); + assert.isTrue(nameStub.called); + + assert.equal(nameStub.lastCall.args[0], 'new name'); + + assert.equal(statusStub.lastCall.args[0], 'new status'); + + done(); + }); + }); + }); + + suite('set status but read name', () => { + let statusChangedSpy; + let statusStub; + + setup(() => { + statusChangedSpy = sandbox.spy(element, '_statusChanged'); + element.set('_serverConfig', + {auth: {editable_account_fields: []}}); + + statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus', + status => Promise.resolve()); + }); + + test('read full name but set status', done => { + const section = element.$.nameSection; + const displaySpan = section.querySelectorAll('.value')[0]; + const inputSpan = section.querySelectorAll('.value')[1]; + + assert.isFalse(element.nameMutable); + + assert.isFalse(element.hasUnsavedChanges); + + assert.isFalse(displaySpan.hasAttribute('hidden')); + assert.equal(displaySpan.textContent, account.name); + assert.isTrue(inputSpan.hasAttribute('hidden')); + + element.set('_account.status', 'new status'); + + assert.isTrue(statusChangedSpy.called); + + assert.isTrue(element.hasUnsavedChanges); + + element.save().then(() => { + assert.isTrue(statusStub.called); + statusStub.lastCall.returnValue.then(() => { + assert.equal(statusStub.lastCall.args[0], 'new status'); + done(); + }); + }); + }); + }); + + test('_usernameChanged compares usernames with loose equality', () => { + element._account = {}; + 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(); + flushAsynchronousOperations(); + + assert.isFalse(element._hasUsernameChange); + + element.set('_username', 'test'); + flushAsynchronousOperations(); + + assert.isTrue(element._hasUsernameChange); + }); + + test('_hideAvatarChangeUrl', () => { + assert.equal(element._hideAvatarChangeUrl(''), 'hide'); + + assert.equal(element._hideAvatarChangeUrl('https://example.com'), ''); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js index 67dc0c4..18a0419 100644 --- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list.js
@@ -14,46 +14,56 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @extends Polymer.Element - */ - class GrAgreementsList extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-agreements-list'; } +import '../../../scripts/bundled-polymer.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-agreements-list_html.js'; - static get properties() { - return { - _agreements: Array, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @extends Polymer.Element + */ +class GrAgreementsList extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this.loadData(); - } + static get is() { return 'gr-agreements-list'; } - loadData() { - return this.$.restAPI.getAccountAgreements().then(agreements => { - this._agreements = agreements; - }); - } - - getUrl() { - return this.getBaseUrl() + '/settings/new-agreement'; - } - - getUrlBase(item) { - return this.getBaseUrl() + '/' + item; - } + static get properties() { + return { + _agreements: Array, + }; } - customElements.define(GrAgreementsList.is, GrAgreementsList); -})(); + /** @override */ + attached() { + super.attached(); + this.loadData(); + } + + loadData() { + return this.$.restAPI.getAccountAgreements().then(agreements => { + this._agreements = agreements; + }); + } + + getUrl() { + return this.getBaseUrl() + '/settings/new-agreement'; + } + + getUrlBase(item) { + return this.getBaseUrl() + '/' + item; + } +} + +customElements.define(GrAgreementsList.is, GrAgreementsList);
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js index 74d92d3..4bd9365 100644 --- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-agreements-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> #agreements .nameColumn { min-width: 15em; @@ -47,7 +41,7 @@ <template is="dom-repeat" items="[[_agreements]]"> <tr> <td class="nameColumn"> - <a href$="[[getUrlBase(item.url)]]" rel="external"> + <a href\$="[[getUrlBase(item.url)]]" rel="external"> [[item.name]] </a> </td> @@ -56,9 +50,7 @@ </template> </tbody> </table> - <a href$="[[getUrl()]]">New Contributor Agreement</a> + <a href\$="[[getUrl()]]">New Contributor Agreement</a> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-agreements-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html index 39b8663..4aa917b 100644 --- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html +++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-agreements-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-agreements-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-agreements-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,38 +40,41 @@ </template> </test-fixture> -<script> - suite('gr-agreements-list tests', async () => { - await readyToTest(); - let element; - let agreements; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-agreements-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-agreements-list tests', () => { + let element; + let agreements; - setup(done => { - agreements = [{ - url: 'some url', - description: 'Agreements 1 description', - name: 'Agreements 1', - }]; + setup(done => { + agreements = [{ + url: 'some url', + description: 'Agreements 1 description', + name: 'Agreements 1', + }]; - stub('gr-rest-api-interface', { - getAccountAgreements() { return Promise.resolve(agreements); }, - }); - - element = fixture('basic'); - - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getAccountAgreements() { return Promise.resolve(agreements); }, }); - test('renders', () => { - const rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); + element = fixture('basic'); - assert.equal(rows.length, 1); - - const nameCells = Array.from(rows).map(row => - row.querySelectorAll('td')[0].textContent.trim() - ); - - assert.equal(nameCells[0], 'Agreements 1'); - }); + element.loadData().then(() => { flush(done); }); }); + + test('renders', () => { + const rows = dom(element.root).querySelectorAll('tbody tr'); + + assert.equal(rows.length, 1); + + const nameCells = Array.from(rows).map(row => + row.querySelectorAll('td')[0].textContent.trim() + ); + + assert.equal(nameCells[0], 'Agreements 1'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js index 8521126..85c34a4 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor.js
@@ -14,72 +14,85 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js'; - /** - * @appliesMixin Gerrit.ChangeTableMixin - * @extends Polymer.Element - */ - class GrChangeTableEditor extends Polymer.mixinBehaviors( [ - Gerrit.ChangeTableBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-change-table-editor'; } +import '../../../scripts/bundled-polymer.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import '../../../styles/gr-form-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-table-editor_html.js'; - static get properties() { - return { - displayedColumns: { - type: Array, - notify: true, - }, - showNumber: { - type: Boolean, - notify: true, - }, - }; - } +/** + * @appliesMixin Gerrit.ChangeTableMixin + * @extends Polymer.Element + */ +class GrChangeTableEditor extends mixinBehaviors( [ + Gerrit.ChangeTableBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** - * Get the list of enabled column names from whichever checkboxes are - * checked (excluding the number checkbox). - * - * @return {!Array<string>} - */ - _getDisplayedColumns() { - return Array.from(Polymer.dom(this.root) - .querySelectorAll('.checkboxContainer input:not([name=number])')) - .filter(checkbox => checkbox.checked) - .map(checkbox => checkbox.name); - } + static get is() { return 'gr-change-table-editor'; } - /** - * Handle a click on a checkbox container and relay the click to the checkbox it - * contains. - */ - _handleCheckboxContainerClick(e) { - const checkbox = Polymer.dom(e.target).querySelector('input'); - if (!checkbox) { return; } - checkbox.click(); - } - - /** - * Handle a click on the number checkbox and update the showNumber property - * accordingly. - */ - _handleNumberCheckboxClick(e) { - this.showNumber = Polymer.dom(e).rootTarget.checked; - } - - /** - * Handle a click on a displayed column checkboxes (excluding number) and - * update the displayedColumns property accordingly. - */ - _handleTargetClick(e) { - this.set('displayedColumns', this._getDisplayedColumns()); - } + static get properties() { + return { + displayedColumns: { + type: Array, + notify: true, + }, + showNumber: { + type: Boolean, + notify: true, + }, + }; } - customElements.define(GrChangeTableEditor.is, GrChangeTableEditor); -})(); + /** + * Get the list of enabled column names from whichever checkboxes are + * checked (excluding the number checkbox). + * + * @return {!Array<string>} + */ + _getDisplayedColumns() { + return Array.from(dom(this.root) + .querySelectorAll('.checkboxContainer input:not([name=number])')) + .filter(checkbox => checkbox.checked) + .map(checkbox => checkbox.name); + } + + /** + * Handle a click on a checkbox container and relay the click to the checkbox it + * contains. + */ + _handleCheckboxContainerClick(e) { + const checkbox = dom(e.target).querySelector('input'); + if (!checkbox) { return; } + checkbox.click(); + } + + /** + * Handle a click on the number checkbox and update the showNumber property + * accordingly. + */ + _handleNumberCheckboxClick(e) { + this.showNumber = dom(e).rootTarget.checked; + } + + /** + * Handle a click on a displayed column checkboxes (excluding number) and + * update the displayedColumns property accordingly. + */ + _handleTargetClick(e) { + this.set('displayedColumns', this._getDisplayedColumns()); + } +} + +customElements.define(GrChangeTableEditor.is, GrChangeTableEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js index 09a9dbc..7aa785c 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> - -<dom-module id="gr-change-table-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -56,31 +49,19 @@ <tbody> <tr> <td>Number</td> - <td class="checkboxContainer" - on-click="_handleCheckboxContainerClick"> - <input - type="checkbox" - name="number" - on-click="_handleNumberCheckboxClick" - checked$="[[showNumber]]"> + <td class="checkboxContainer" on-click="_handleCheckboxContainerClick"> + <input type="checkbox" name="number" on-click="_handleNumberCheckboxClick" checked\$="[[showNumber]]"> </td> </tr> <template is="dom-repeat" items="[[columnNames]]"> <tr> <td>[[item]]</td> - <td class="checkboxContainer" - on-click="_handleCheckboxContainerClick"> - <input - type="checkbox" - name="[[item]]" - on-click="_handleTargetClick" - checked$="[[!isColumnHidden(item, displayedColumns)]]"> + <td class="checkboxContainer" on-click="_handleCheckboxContainerClick"> + <input type="checkbox" name="[[item]]" on-click="_handleTargetClick" checked\$="[[!isColumnHidden(item, displayedColumns)]]"> </td> </tr> </template> </tbody> </table> </div> - </template> - <script src="gr-change-table-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html index 460d6bc..1d60384 100644 --- a/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-change-table-editor/gr-change-table-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-change-table-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-change-table-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-table-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,138 +40,140 @@ </template> </test-fixture> -<script> - suite('gr-change-table-editor tests', async () => { - await readyToTest(); - let element; - let columns; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-table-editor.js'; +suite('gr-change-table-editor tests', () => { + let element; + let columns; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); - columns = [ - 'Subject', - 'Status', - 'Owner', - 'Assignee', - 'Repo', - 'Branch', - 'Updated', - ]; + columns = [ + 'Subject', + 'Status', + 'Owner', + 'Assignee', + 'Repo', + 'Branch', + 'Updated', + ]; - element.set('displayedColumns', columns); - element.showNumber = false; - flushAsynchronousOperations(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('renders', () => { - const rows = element.shadowRoot - .querySelector('tbody').querySelectorAll('tr'); - let tds; - - // The `+ 1` is for the number column, which isn't included in the change - // table behavior's list. - assert.equal(rows.length, element.columnNames.length + 1); - for (let i = 0; i < columns.length; i++) { - tds = rows[i + 1].querySelectorAll('td'); - assert.equal(tds[0].textContent, columns[i]); - } - }); - - test('hide item', () => { - const checkbox = element.shadowRoot - .querySelector('table tr:nth-child(2) input'); - const isChecked = checkbox.checked; - const displayedLength = element.displayedColumns.length; - assert.isTrue(isChecked); - - MockInteractions.tap(checkbox); - flushAsynchronousOperations(); - - assert.equal(element.displayedColumns.length, displayedLength - 1); - }); - - test('show item', () => { - element.set('displayedColumns', [ - 'Status', - 'Owner', - 'Assignee', - 'Repo', - 'Branch', - 'Updated', - ]); - flushAsynchronousOperations(); - const checkbox = element.shadowRoot - .querySelector('table tr:nth-child(2) input'); - const isChecked = checkbox.checked; - const displayedLength = element.displayedColumns.length; - assert.isFalse(isChecked); - assert.equal(element.shadowRoot - .querySelector('table').style.display, ''); - - MockInteractions.tap(checkbox); - flushAsynchronousOperations(); - - assert.equal(element.displayedColumns.length, - displayedLength + 1); - }); - - test('_getDisplayedColumns', () => { - assert.deepEqual(element._getDisplayedColumns(), columns); - MockInteractions.tap( - element.shadowRoot - .querySelector('.checkboxContainer input[name=Assignee]')); - assert.deepEqual(element._getDisplayedColumns(), - columns.filter(c => c !== 'Assignee')); - }); - - test('_handleCheckboxContainerClick relayes taps to checkboxes', () => { - sandbox.stub(element, '_handleNumberCheckboxClick'); - sandbox.stub(element, '_handleTargetClick'); - - MockInteractions.tap( - element.shadowRoot - .querySelector('table tr:first-of-type .checkboxContainer')); - assert.isTrue(element._handleNumberCheckboxClick.calledOnce); - assert.isFalse(element._handleTargetClick.called); - - MockInteractions.tap( - element.shadowRoot - .querySelector('table tr:last-of-type .checkboxContainer')); - assert.isTrue(element._handleNumberCheckboxClick.calledOnce); - assert.isTrue(element._handleTargetClick.calledOnce); - }); - - test('_handleNumberCheckboxClick', () => { - sandbox.spy(element, '_handleNumberCheckboxClick'); - - MockInteractions - .tap(element.shadowRoot - .querySelector('.checkboxContainer input[name=number]')); - assert.isTrue(element._handleNumberCheckboxClick.calledOnce); - assert.isTrue(element.showNumber); - - MockInteractions - .tap(element.shadowRoot - .querySelector('.checkboxContainer input[name=number]')); - assert.isTrue(element._handleNumberCheckboxClick.calledTwice); - assert.isFalse(element.showNumber); - }); - - test('_handleTargetClick', () => { - sandbox.spy(element, '_handleTargetClick'); - assert.include(element.displayedColumns, 'Assignee'); - MockInteractions - .tap(element.shadowRoot - .querySelector('.checkboxContainer input[name=Assignee]')); - assert.isTrue(element._handleTargetClick.calledOnce); - assert.notInclude(element.displayedColumns, 'Assignee'); - }); + element.set('displayedColumns', columns); + element.showNumber = false; + flushAsynchronousOperations(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('renders', () => { + const rows = element.shadowRoot + .querySelector('tbody').querySelectorAll('tr'); + let tds; + + // The `+ 1` is for the number column, which isn't included in the change + // table behavior's list. + assert.equal(rows.length, element.columnNames.length + 1); + for (let i = 0; i < columns.length; i++) { + tds = rows[i + 1].querySelectorAll('td'); + assert.equal(tds[0].textContent, columns[i]); + } + }); + + test('hide item', () => { + const checkbox = element.shadowRoot + .querySelector('table tr:nth-child(2) input'); + const isChecked = checkbox.checked; + const displayedLength = element.displayedColumns.length; + assert.isTrue(isChecked); + + MockInteractions.tap(checkbox); + flushAsynchronousOperations(); + + assert.equal(element.displayedColumns.length, displayedLength - 1); + }); + + test('show item', () => { + element.set('displayedColumns', [ + 'Status', + 'Owner', + 'Assignee', + 'Repo', + 'Branch', + 'Updated', + ]); + flushAsynchronousOperations(); + const checkbox = element.shadowRoot + .querySelector('table tr:nth-child(2) input'); + const isChecked = checkbox.checked; + const displayedLength = element.displayedColumns.length; + assert.isFalse(isChecked); + assert.equal(element.shadowRoot + .querySelector('table').style.display, ''); + + MockInteractions.tap(checkbox); + flushAsynchronousOperations(); + + assert.equal(element.displayedColumns.length, + displayedLength + 1); + }); + + test('_getDisplayedColumns', () => { + assert.deepEqual(element._getDisplayedColumns(), columns); + MockInteractions.tap( + element.shadowRoot + .querySelector('.checkboxContainer input[name=Assignee]')); + assert.deepEqual(element._getDisplayedColumns(), + columns.filter(c => c !== 'Assignee')); + }); + + test('_handleCheckboxContainerClick relayes taps to checkboxes', () => { + sandbox.stub(element, '_handleNumberCheckboxClick'); + sandbox.stub(element, '_handleTargetClick'); + + MockInteractions.tap( + element.shadowRoot + .querySelector('table tr:first-of-type .checkboxContainer')); + assert.isTrue(element._handleNumberCheckboxClick.calledOnce); + assert.isFalse(element._handleTargetClick.called); + + MockInteractions.tap( + element.shadowRoot + .querySelector('table tr:last-of-type .checkboxContainer')); + assert.isTrue(element._handleNumberCheckboxClick.calledOnce); + assert.isTrue(element._handleTargetClick.calledOnce); + }); + + test('_handleNumberCheckboxClick', () => { + sandbox.spy(element, '_handleNumberCheckboxClick'); + + MockInteractions + .tap(element.shadowRoot + .querySelector('.checkboxContainer input[name=number]')); + assert.isTrue(element._handleNumberCheckboxClick.calledOnce); + assert.isTrue(element.showNumber); + + MockInteractions + .tap(element.shadowRoot + .querySelector('.checkboxContainer input[name=number]')); + assert.isTrue(element._handleNumberCheckboxClick.calledTwice); + assert.isFalse(element.showNumber); + }); + + test('_handleTargetClick', () => { + sandbox.spy(element, '_handleTargetClick'); + assert.include(element.displayedColumns, 'Assignee'); + MockInteractions + .tap(element.shadowRoot + .querySelector('.checkboxContainer input[name=Assignee]')); + assert.isTrue(element._handleTargetClick.calledOnce); + assert.notInclude(element.displayedColumns, 'Assignee'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js index cff1d54..373ac63 100644 --- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js +++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view.js
@@ -14,151 +14,164 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrClaView extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-cla-view'; } +import '@polymer/iron-input/iron-input.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-cla-view_html.js'; - static get properties() { - return { - _groups: Object, - /** @type {?} */ - _serverConfig: Object, - _agreementsText: String, - _agreementName: String, - _signedAgreements: Array, - _showAgreements: { - type: Boolean, - value: false, - }, - _agreementsUrl: String, - }; +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrClaView extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-cla-view'; } + + static get properties() { + return { + _groups: Object, + /** @type {?} */ + _serverConfig: Object, + _agreementsText: String, + _agreementName: String, + _signedAgreements: Array, + _showAgreements: { + type: Boolean, + value: false, + }, + _agreementsUrl: String, + }; + } + + /** @override */ + attached() { + super.attached(); + this.loadData(); + + this.fire('title-change', {title: 'New Contributor Agreement'}); + } + + loadData() { + const promises = []; + promises.push(this.$.restAPI.getConfig(true).then(config => { + this._serverConfig = config; + })); + + promises.push(this.$.restAPI.getAccountGroups().then(groups => { + this._groups = groups.sort((a, b) => a.name.localeCompare(b.name)); + })); + + promises.push(this.$.restAPI.getAccountAgreements().then(agreements => { + this._signedAgreements = agreements || []; + })); + + return Promise.all(promises); + } + + _getAgreementsUrl(configUrl) { + let url; + if (!configUrl) { + return ''; + } + if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) { + url = configUrl; + } else { + url = this.getBaseUrl() + '/' + configUrl; } - /** @override */ - attached() { - super.attached(); + return url; + } + + _handleShowAgreement(e) { + this._agreementName = e.target.getAttribute('data-name'); + this._agreementsUrl = + this._getAgreementsUrl(e.target.getAttribute('data-url')); + this._showAgreements = true; + } + + _handleSaveAgreements(e) { + this._createToast('Agreement saving...'); + + const name = this._agreementName; + return this.$.restAPI.saveAccountAgreement({name}).then(res => { + let message = 'Agreement failed to be submitted, please try again'; + if (res.status === 200) { + message = 'Agreement has been successfully submited.'; + } + this._createToast(message); this.loadData(); + this._agreementsText = ''; + this._showAgreements = false; + }); + } - this.fire('title-change', {title: 'New Contributor Agreement'}); - } + _createToast(message) { + this.dispatchEvent(new CustomEvent( + 'show-alert', {detail: {message}, bubbles: true, composed: true})); + } - loadData() { - const promises = []; - promises.push(this.$.restAPI.getConfig(true).then(config => { - this._serverConfig = config; - })); + _computeShowAgreementsClass(agreements) { + return agreements ? 'show' : ''; + } - promises.push(this.$.restAPI.getAccountGroups().then(groups => { - this._groups = groups.sort((a, b) => a.name.localeCompare(b.name)); - })); - - promises.push(this.$.restAPI.getAccountAgreements().then(agreements => { - this._signedAgreements = agreements || []; - })); - - return Promise.all(promises); - } - - _getAgreementsUrl(configUrl) { - let url; - if (!configUrl) { - return ''; + _disableAgreements(item, groups, signedAgreements) { + if (!groups) return false; + for (const group of groups) { + if ((item && item.auto_verify_group && + item.auto_verify_group.id === group.id) || + signedAgreements.find(i => i.name === item.name)) { + return true; } - if (configUrl.startsWith('http:') || configUrl.startsWith('https:')) { - url = configUrl; - } else { - url = this.getBaseUrl() + '/' + configUrl; + } + return false; + } + + _hideAgreements(item, groups, signedAgreements) { + return this._disableAgreements(item, groups, signedAgreements) ? + '' : 'hide'; + } + + _disableAgreementsText(text) { + return text.toLowerCase() === 'i agree' ? false : true; + } + + // This checks for auto_verify_group, + // if specified it returns 'hideAgreementsTextBox' which + // then hides the text box and submit button. + _computeHideAgreementClass(name, config) { + if (!config) return ''; + for (const key in config) { + if (!config.hasOwnProperty(key)) { + continue; } - - return url; - } - - _handleShowAgreement(e) { - this._agreementName = e.target.getAttribute('data-name'); - this._agreementsUrl = - this._getAgreementsUrl(e.target.getAttribute('data-url')); - this._showAgreements = true; - } - - _handleSaveAgreements(e) { - this._createToast('Agreement saving...'); - - const name = this._agreementName; - return this.$.restAPI.saveAccountAgreement({name}).then(res => { - let message = 'Agreement failed to be submitted, please try again'; - if (res.status === 200) { - message = 'Agreement has been successfully submited.'; - } - this._createToast(message); - this.loadData(); - this._agreementsText = ''; - this._showAgreements = false; - }); - } - - _createToast(message) { - this.dispatchEvent(new CustomEvent( - 'show-alert', {detail: {message}, bubbles: true, composed: true})); - } - - _computeShowAgreementsClass(agreements) { - return agreements ? 'show' : ''; - } - - _disableAgreements(item, groups, signedAgreements) { - if (!groups) return false; - for (const group of groups) { - if ((item && item.auto_verify_group && - item.auto_verify_group.id === group.id) || - signedAgreements.find(i => i.name === item.name)) { - return true; - } - } - return false; - } - - _hideAgreements(item, groups, signedAgreements) { - return this._disableAgreements(item, groups, signedAgreements) ? - '' : 'hide'; - } - - _disableAgreementsText(text) { - return text.toLowerCase() === 'i agree' ? false : true; - } - - // This checks for auto_verify_group, - // if specified it returns 'hideAgreementsTextBox' which - // then hides the text box and submit button. - _computeHideAgreementClass(name, config) { - if (!config) return ''; - for (const key in config) { - if (!config.hasOwnProperty(key)) { + for (const prop in config[key]) { + if (!config[key].hasOwnProperty(prop)) { continue; } - for (const prop in config[key]) { - if (!config[key].hasOwnProperty(prop)) { - continue; - } - if (name === config[key].name && - !config[key].auto_verify_group) { - return 'hideAgreementsTextBox'; - } + if (name === config[key].name && + !config[key].auto_verify_group) { + return 'hideAgreementsTextBox'; } } } } +} - customElements.define(GrClaView.is, GrClaView); -})(); +customElements.define(GrClaView.is, GrClaView);
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js index fb5d64f..2c2fca0 100644 --- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js +++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-cla-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> h1 { margin-bottom: var(--spacing-m); @@ -74,36 +65,26 @@ <h3>Select an agreement type:</h3> <template is="dom-repeat" items="[[_serverConfig.auth.contributor_agreements]]"> <span class="contributorAgreementButton"> - <input id$="claNewAgreementsInput[[item.name]]" - name="claNewAgreementsRadio" - type="radio" - data-name$="[[item.name]]" - data-url$="[[item.url]]" - on-click="_handleShowAgreement" - disabled$="[[_disableAgreements(item, _groups, _signedAgreements)]]"> + <input id\$="claNewAgreementsInput[[item.name]]" name="claNewAgreementsRadio" type="radio" data-name\$="[[item.name]]" data-url\$="[[item.url]]" on-click="_handleShowAgreement" disabled\$="[[_disableAgreements(item, _groups, _signedAgreements)]]"> <label id="claNewAgreementsLabel">[[item.name]]</label> </span> - <div class$="alreadySubmittedText [[_hideAgreements(item, _groups, _signedAgreements)]]"> + <div class\$="alreadySubmittedText [[_hideAgreements(item, _groups, _signedAgreements)]]"> Agreement already submitted. </div> <div class="agreementsUrl"> [[item.description]] </div> </template> - <div id="claNewAgreement" class$="[[_computeShowAgreementsClass(_showAgreements)]]"> + <div id="claNewAgreement" class\$="[[_computeShowAgreementsClass(_showAgreements)]]"> <h3 class="smallHeading">Review the agreement:</h3> <div id="agreementsUrl" class="agreementsUrl"> - <a href$="[[_agreementsUrl]]" target="blank" rel="noopener"> + <a href\$="[[_agreementsUrl]]" target="blank" rel="noopener"> Please review the agreement.</a> </div> - <div class$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]"> + <div class\$="agreementsTextBox [[_computeHideAgreementClass(_agreementName, _serverConfig.auth.contributor_agreements)]]"> <h3 class="smallHeading">Complete the agreement:</h3> - <iron-input bind-value="{{_agreementsText}}" - placeholder="Enter 'I agree' here"> - <input id="input-agreements" - is="iron-input" - bind-value="{{_agreementsText}}" - placeholder="Enter 'I agree' here"> + <iron-input bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here"> + <input id="input-agreements" is="iron-input" bind-value="{{_agreementsText}}" placeholder="Enter 'I agree' here"> </iron-input> <gr-button on-click="_handleSaveAgreements" disabled="[[_disableAgreementsText(_agreementsText)]]"> Submit @@ -112,6 +93,4 @@ </div> </main> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-cla-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html index d40d36d..50f82e2 100644 --- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html +++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-cla-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-cla-view.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-cla-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-cla-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,162 +40,165 @@ </template> </test-fixture> -<script> - suite('gr-cla-view tests', async () => { - await readyToTest(); - let element; - const signedAgreements = [{ - name: 'CLA', - description: 'Contributor License Agreement', - url: 'static/cla.html', - }]; - const auth = { - name: 'Individual', - description: 'test-description', - url: 'static/cla_individual.html', - auto_verify_group: { - url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0', - options: { - visible_to_all: true, - }, - group_id: 20, - owner: 'CLA Accepted - Individual', - owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0', - created_on: '2017-07-31 15:11:04.000000000', - id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0', - name: 'CLA Accepted - Individual', +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-cla-view.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-cla-view tests', () => { + let element; + const signedAgreements = [{ + name: 'CLA', + description: 'Contributor License Agreement', + url: 'static/cla.html', + }]; + const auth = { + name: 'Individual', + description: 'test-description', + url: 'static/cla_individual.html', + auto_verify_group: { + url: '#/admin/groups/uuid-e9aaddc47f305be7661ad4db9b66f9b707bd19a0', + options: { + visible_to_all: true, }, - }; - - const auth2 = { - name: 'Individual2', - description: 'test-description2', - url: 'static/cla_individual2.html', - auto_verify_group: { - url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb', - options: {}, - group_id: 21, - owner: 'CLA Accepted - Individual2', - owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb', - created_on: '2017-07-31 15:25:42.000000000', - id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb', - name: 'CLA Accepted - Individual2', - }, - }; - - const auth3 = { - name: 'CLA', - description: 'Contributor License Agreement', - url: 'static/cla_individual.html', - }; - - const config = { - auth: { - use_contributor_agreements: true, - contributor_agreements: [ - { - name: 'Individual', - description: 'test-description', - url: 'static/cla_individual.html', - }, - { - name: 'CLA', - description: 'Contributor License Agreement', - url: 'static/cla.html', - }], - }, - }; - const config2 = { - auth: { - use_contributor_agreements: true, - contributor_agreements: [ - { - name: 'Individual2', - description: 'test-description2', - url: 'static/cla_individual2.html', - }, - ], - }, - }; - const groups = [{ - options: {visible_to_all: true}, + group_id: 20, + owner: 'CLA Accepted - Individual', + owner_id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0', + created_on: '2017-07-31 15:11:04.000000000', id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0', - group_id: 3, name: 'CLA Accepted - Individual', }, - ]; + }; - setup(done => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve(config); }, - getAccountGroups() { return Promise.resolve(groups); }, - getAccountAgreements() { return Promise.resolve(signedAgreements); }, - }); - element = fixture('basic'); - element.loadData().then(() => { flush(done); }); - }); + const auth2 = { + name: 'Individual2', + description: 'test-description2', + url: 'static/cla_individual2.html', + auto_verify_group: { + url: '#/admin/groups/uuid-bc53f2738ef8ad0b3a4f53846ff59b05822caecb', + options: {}, + group_id: 21, + owner: 'CLA Accepted - Individual2', + owner_id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb', + created_on: '2017-07-31 15:25:42.000000000', + id: 'bc53f2738ef8ad0b3a4f53846ff59b05822caecb', + name: 'CLA Accepted - Individual2', + }, + }; - test('renders as expected with signed agreement', () => { - const agreementSections = Polymer.dom(element.root) - .querySelectorAll('.contributorAgreementButton'); - const agreementSubmittedTexts = Polymer.dom(element.root) - .querySelectorAll('.alreadySubmittedText'); - assert.equal(agreementSections.length, 2); - assert.isFalse(agreementSections[0].querySelector('input').disabled); - assert.equal(getComputedStyle(agreementSubmittedTexts[0]).display, - 'none'); - assert.isTrue(agreementSections[1].querySelector('input').disabled); - assert.notEqual(getComputedStyle(agreementSubmittedTexts[1]).display, - 'none'); - }); + const auth3 = { + name: 'CLA', + description: 'Contributor License Agreement', + url: 'static/cla_individual.html', + }; - test('_disableAgreements', () => { - // In the auto verify group and have not yet signed agreement - assert.isTrue( - element._disableAgreements(auth, groups, signedAgreements)); - // Not in the auto verify group and have not yet signed agreement - assert.isFalse( - element._disableAgreements(auth2, groups, signedAgreements)); - // Not in the auto verify group, have signed agreement - assert.isTrue( - element._disableAgreements(auth3, groups, signedAgreements)); - // Make sure the undefined check works - assert.isFalse( - element._disableAgreements(auth, undefined, signedAgreements)); - }); + const config = { + auth: { + use_contributor_agreements: true, + contributor_agreements: [ + { + name: 'Individual', + description: 'test-description', + url: 'static/cla_individual.html', + }, + { + name: 'CLA', + description: 'Contributor License Agreement', + url: 'static/cla.html', + }], + }, + }; + const config2 = { + auth: { + use_contributor_agreements: true, + contributor_agreements: [ + { + name: 'Individual2', + description: 'test-description2', + url: 'static/cla_individual2.html', + }, + ], + }, + }; + const groups = [{ + options: {visible_to_all: true}, + id: 'e9aaddc47f305be7661ad4db9b66f9b707bd19a0', + group_id: 3, + name: 'CLA Accepted - Individual', + }, + ]; - test('_hideAgreements', () => { - // Not in the auto verify group and have not yet signed agreement - assert.equal( - element._hideAgreements(auth, groups, signedAgreements), ''); - // In the auto verify group - assert.equal( - element._hideAgreements(auth2, groups, signedAgreements), 'hide'); - // Not in the auto verify group, have signed agreement - assert.equal( - element._hideAgreements(auth3, groups, signedAgreements), ''); + setup(done => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve(config); }, + getAccountGroups() { return Promise.resolve(groups); }, + getAccountAgreements() { return Promise.resolve(signedAgreements); }, }); - - test('_disableAgreementsText', () => { - assert.isFalse(element._disableAgreementsText('I AGREE')); - assert.isTrue(element._disableAgreementsText('I DO NOT AGREE')); - }); - - test('_computeHideAgreementClass', () => { - assert.equal( - element._computeHideAgreementClass( - auth.name, config.auth.contributor_agreements), - 'hideAgreementsTextBox'); - assert.isUndefined( - element._computeHideAgreementClass( - auth.name, config2.auth.contributor_agreements)); - }); - - test('_getAgreementsUrl', () => { - assert.equal(element._getAgreementsUrl( - 'http://test.org/test.html'), 'http://test.org/test.html'); - assert.equal(element._getAgreementsUrl( - 'test_cla.html'), '/test_cla.html'); - }); + element = fixture('basic'); + element.loadData().then(() => { flush(done); }); }); + + test('renders as expected with signed agreement', () => { + const agreementSections = dom(element.root) + .querySelectorAll('.contributorAgreementButton'); + const agreementSubmittedTexts = dom(element.root) + .querySelectorAll('.alreadySubmittedText'); + assert.equal(agreementSections.length, 2); + assert.isFalse(agreementSections[0].querySelector('input').disabled); + assert.equal(getComputedStyle(agreementSubmittedTexts[0]).display, + 'none'); + assert.isTrue(agreementSections[1].querySelector('input').disabled); + assert.notEqual(getComputedStyle(agreementSubmittedTexts[1]).display, + 'none'); + }); + + test('_disableAgreements', () => { + // In the auto verify group and have not yet signed agreement + assert.isTrue( + element._disableAgreements(auth, groups, signedAgreements)); + // Not in the auto verify group and have not yet signed agreement + assert.isFalse( + element._disableAgreements(auth2, groups, signedAgreements)); + // Not in the auto verify group, have signed agreement + assert.isTrue( + element._disableAgreements(auth3, groups, signedAgreements)); + // Make sure the undefined check works + assert.isFalse( + element._disableAgreements(auth, undefined, signedAgreements)); + }); + + test('_hideAgreements', () => { + // Not in the auto verify group and have not yet signed agreement + assert.equal( + element._hideAgreements(auth, groups, signedAgreements), ''); + // In the auto verify group + assert.equal( + element._hideAgreements(auth2, groups, signedAgreements), 'hide'); + // Not in the auto verify group, have signed agreement + assert.equal( + element._hideAgreements(auth3, groups, signedAgreements), ''); + }); + + test('_disableAgreementsText', () => { + assert.isFalse(element._disableAgreementsText('I AGREE')); + assert.isTrue(element._disableAgreementsText('I DO NOT AGREE')); + }); + + test('_computeHideAgreementClass', () => { + assert.equal( + element._computeHideAgreementClass( + auth.name, config.auth.contributor_agreements), + 'hideAgreementsTextBox'); + assert.isUndefined( + element._computeHideAgreementClass( + auth.name, config2.auth.contributor_agreements)); + }); + + test('_getAgreementsUrl', () => { + assert.equal(element._getAgreementsUrl( + 'http://test.org/test.html'), 'http://test.org/test.html'); + assert.equal(element._getAgreementsUrl( + 'test_cla.html'), '/test_cla.html'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js index 9523136..2a7ac06 100644 --- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js +++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -14,76 +14,86 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrEditPreferences extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-edit-preferences'; } +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-edit-preferences_html.js'; - static get properties() { - return { - hasUnsavedChanges: { - type: Boolean, - notify: true, - value: false, - }, +/** @extends Polymer.Element */ +class GrEditPreferences extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** @type {?} */ - editPrefs: Object, - }; - } + static get is() { return 'gr-edit-preferences'; } - loadData() { - return this.$.restAPI.getEditPreferences().then(prefs => { - this.editPrefs = prefs; - }); - } + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + notify: true, + value: false, + }, - _handleEditPrefsChanged() { - this.hasUnsavedChanges = true; - } - - _handleEditSyntaxHighlightingChanged() { - this.set('editPrefs.syntax_highlighting', - this.$.editSyntaxHighlighting.checked); - this._handleEditPrefsChanged(); - } - - _handleEditShowTabsChanged() { - this.set('editPrefs.show_tabs', this.$.editShowTabs.checked); - this._handleEditPrefsChanged(); - } - - _handleMatchBracketsChanged() { - this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked); - this._handleEditPrefsChanged(); - } - - _handleEditLineWrappingChanged() { - this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked); - this._handleEditPrefsChanged(); - } - - _handleIndentWithTabsChanged() { - this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked); - this._handleEditPrefsChanged(); - } - - _handleAutoCloseBracketsChanged() { - this.set('editPrefs.auto_close_brackets', - this.$.showAutoCloseBrackets.checked); - this._handleEditPrefsChanged(); - } - - save() { - return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => { - this.hasUnsavedChanges = false; - }); - } + /** @type {?} */ + editPrefs: Object, + }; } - customElements.define(GrEditPreferences.is, GrEditPreferences); -})(); + loadData() { + return this.$.restAPI.getEditPreferences().then(prefs => { + this.editPrefs = prefs; + }); + } + + _handleEditPrefsChanged() { + this.hasUnsavedChanges = true; + } + + _handleEditSyntaxHighlightingChanged() { + this.set('editPrefs.syntax_highlighting', + this.$.editSyntaxHighlighting.checked); + this._handleEditPrefsChanged(); + } + + _handleEditShowTabsChanged() { + this.set('editPrefs.show_tabs', this.$.editShowTabs.checked); + this._handleEditPrefsChanged(); + } + + _handleMatchBracketsChanged() { + this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked); + this._handleEditPrefsChanged(); + } + + _handleEditLineWrappingChanged() { + this.set('editPrefs.line_wrapping', this.$.editShowLineWrapping.checked); + this._handleEditPrefsChanged(); + } + + _handleIndentWithTabsChanged() { + this.set('editPrefs.indent_with_tabs', this.$.showIndentWithTabs.checked); + this._handleEditPrefsChanged(); + } + + _handleAutoCloseBracketsChanged() { + this.set('editPrefs.auto_close_brackets', + this.$.showAutoCloseBrackets.checked); + this._handleEditPrefsChanged(); + } + + save() { + return this.$.restAPI.saveEditPreferences(this.editPrefs).then(res => { + this.hasUnsavedChanges = false; + }); + } +} + +customElements.define(GrEditPreferences.is, GrEditPreferences);
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js index 80440c7..de22dbb 100644 --- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js +++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - -<dom-module id="gr-edit-preferences"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -34,128 +27,63 @@ <section> <span class="title">Tab width</span> <span class="value"> - <iron-input - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{editPrefs.tab_size}}" - on-keypress="_handleEditPrefsChanged" - on-change="_handleEditPrefsChanged"> - <input - is="iron-input" - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{editPrefs.tab_size}}" - on-keypress="_handleEditPrefsChanged" - on-change="_handleEditPrefsChanged"> + <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.tab_size}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged"> + <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.tab_size}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged"> </iron-input> </span> </section> <section> <span class="title">Columns</span> <span class="value"> - <iron-input - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{editPrefs.line_length}}" - on-keypress="_handleEditPrefsChanged" - on-change="_handleEditPrefsChanged"> - <input - is="iron-input" - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{editPrefs.line_length}}" - on-keypress="_handleEditPrefsChanged" - on-change="_handleEditPrefsChanged"> + <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.line_length}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged"> + <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.line_length}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged"> </iron-input> </span> </section> <section> <span class="title">Indent unit</span> <span class="value"> - <iron-input - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{editPrefs.indent_unit}}" - on-keypress="_handleEditPrefsChanged" - on-change="_handleEditPrefsChanged"> - <input - is="iron-input" - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{editPrefs.indent_unit}}" - on-keypress="_handleEditPrefsChanged" - on-change="_handleEditPrefsChanged"> + <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.indent_unit}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged"> + <input is="iron-input" type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{editPrefs.indent_unit}}" on-keypress="_handleEditPrefsChanged" on-change="_handleEditPrefsChanged"> </iron-input> </span> </section> <section> <span class="title">Syntax highlighting</span> <span class="value"> - <input - id="editSyntaxHighlighting" - type="checkbox" - checked$="[[editPrefs.syntax_highlighting]]" - on-change="_handleEditSyntaxHighlightingChanged"> + <input id="editSyntaxHighlighting" type="checkbox" checked\$="[[editPrefs.syntax_highlighting]]" on-change="_handleEditSyntaxHighlightingChanged"> </span> </section> <section> <span class="title">Show tabs</span> <span class="value"> - <input - id="editShowTabs" - type="checkbox" - checked$="[[editPrefs.show_tabs]]" - on-change="_handleEditShowTabsChanged"> + <input id="editShowTabs" type="checkbox" checked\$="[[editPrefs.show_tabs]]" on-change="_handleEditShowTabsChanged"> </span> </section> <section> <span class="title">Match brackets</span> <span class="value"> - <input - id="showMatchBrackets" - type="checkbox" - checked$="[[editPrefs.match_brackets]]" - on-change="_handleMatchBracketsChanged"> + <input id="showMatchBrackets" type="checkbox" checked\$="[[editPrefs.match_brackets]]" on-change="_handleMatchBracketsChanged"> </span> </section> <section> <span class="title">Line wrapping</span> <span class="value"> - <input - id="editShowLineWrapping" - type="checkbox" - checked$="[[editPrefs.line_wrapping]]" - on-change="_handleEditLineWrappingChanged"> + <input id="editShowLineWrapping" type="checkbox" checked\$="[[editPrefs.line_wrapping]]" on-change="_handleEditLineWrappingChanged"> </span> </section> <section> <span class="title">Indent with tabs</span> <span class="value"> - <input - id="showIndentWithTabs" - type="checkbox" - checked$="[[editPrefs.indent_with_tabs]]" - on-change="_handleIndentWithTabsChanged"> + <input id="showIndentWithTabs" type="checkbox" checked\$="[[editPrefs.indent_with_tabs]]" on-change="_handleIndentWithTabsChanged"> </span> </section> <section> <span class="title">Auto close brackets</span> <span class="value"> - <input - id="showAutoCloseBrackets" - type="checkbox" - checked$="[[editPrefs.auto_close_brackets]]" - on-change="_handleAutoCloseBracketsChanged"> + <input id="showAutoCloseBrackets" type="checkbox" checked\$="[[editPrefs.auto_close_brackets]]" on-change="_handleAutoCloseBracketsChanged"> </span> </section> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-edit-preferences.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html index 3c99977..b73b14f 100644 --- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html +++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-edit-preferences</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-edit-preferences.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-edit-preferences.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-edit-preferences.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,95 +40,97 @@ </template> </test-fixture> -<script> - suite('gr-edit-preferences tests', async () => { - await readyToTest(); - let element; - let sandbox; - let editPreferences; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-edit-preferences.js'; +suite('gr-edit-preferences tests', () => { + let element; + let sandbox; + let editPreferences; - function valueOf(title, fieldsetid) { - const sections = element.$[fieldsetid].querySelectorAll('section'); - let titleEl; - for (let i = 0; i < sections.length; i++) { - titleEl = sections[i].querySelector('.title'); - if (titleEl.textContent.trim() === title) { - return sections[i].querySelector('.value'); - } + function valueOf(title, fieldsetid) { + const sections = element.$[fieldsetid].querySelectorAll('section'); + let titleEl; + for (let i = 0; i < sections.length; i++) { + titleEl = sections[i].querySelector('.title'); + if (titleEl.textContent.trim() === title) { + return sections[i].querySelector('.value'); } } + } - setup(() => { - editPreferences = { - auto_close_brackets: false, - cursor_blink_rate: 0, - hide_line_numbers: false, - hide_top_menu: false, - indent_unit: 2, - indent_with_tabs: false, - key_map_type: 'DEFAULT', - line_length: 100, - line_wrapping: false, - match_brackets: true, - show_base: false, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - tab_size: 8, - theme: 'DEFAULT', - }; + setup(() => { + editPreferences = { + auto_close_brackets: false, + cursor_blink_rate: 0, + hide_line_numbers: false, + hide_top_menu: false, + indent_unit: 2, + indent_with_tabs: false, + key_map_type: 'DEFAULT', + line_length: 100, + line_wrapping: false, + match_brackets: true, + show_base: false, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + tab_size: 8, + theme: 'DEFAULT', + }; - stub('gr-rest-api-interface', { - getEditPreferences() { - return Promise.resolve(editPreferences); - }, - }); - - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - return element.loadData(); + stub('gr-rest-api-interface', { + getEditPreferences() { + return Promise.resolve(editPreferences); + }, }); - teardown(() => { sandbox.restore(); }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + return element.loadData(); + }); - test('renders', () => { - // Rendered with the expected preferences selected. - assert.equal(valueOf('Tab width', 'editPreferences') - .firstElementChild.bindValue, editPreferences.tab_size); - assert.equal(valueOf('Columns', 'editPreferences') - .firstElementChild.bindValue, editPreferences.line_length); - assert.equal(valueOf('Indent unit', 'editPreferences') - .firstElementChild.bindValue, editPreferences.indent_unit); - assert.equal(valueOf('Syntax highlighting', 'editPreferences') - .firstElementChild.checked, editPreferences.syntax_highlighting); - assert.equal(valueOf('Show tabs', 'editPreferences') - .firstElementChild.checked, editPreferences.show_tabs); - assert.equal(valueOf('Match brackets', 'editPreferences') - .firstElementChild.checked, editPreferences.match_brackets); - assert.equal(valueOf('Line wrapping', 'editPreferences') - .firstElementChild.checked, editPreferences.line_wrapping); - assert.equal(valueOf('Indent with tabs', 'editPreferences') - .firstElementChild.checked, editPreferences.indent_with_tabs); - assert.equal(valueOf('Auto close brackets', 'editPreferences') - .firstElementChild.checked, editPreferences.auto_close_brackets); + teardown(() => { sandbox.restore(); }); + test('renders', () => { + // Rendered with the expected preferences selected. + assert.equal(valueOf('Tab width', 'editPreferences') + .firstElementChild.bindValue, editPreferences.tab_size); + assert.equal(valueOf('Columns', 'editPreferences') + .firstElementChild.bindValue, editPreferences.line_length); + assert.equal(valueOf('Indent unit', 'editPreferences') + .firstElementChild.bindValue, editPreferences.indent_unit); + assert.equal(valueOf('Syntax highlighting', 'editPreferences') + .firstElementChild.checked, editPreferences.syntax_highlighting); + assert.equal(valueOf('Show tabs', 'editPreferences') + .firstElementChild.checked, editPreferences.show_tabs); + assert.equal(valueOf('Match brackets', 'editPreferences') + .firstElementChild.checked, editPreferences.match_brackets); + assert.equal(valueOf('Line wrapping', 'editPreferences') + .firstElementChild.checked, editPreferences.line_wrapping); + assert.equal(valueOf('Indent with tabs', 'editPreferences') + .firstElementChild.checked, editPreferences.indent_with_tabs); + assert.equal(valueOf('Auto close brackets', 'editPreferences') + .firstElementChild.checked, editPreferences.auto_close_brackets); + + assert.isFalse(element.hasUnsavedChanges); + }); + + test('save changes', () => { + sandbox.stub(element.$.restAPI, 'saveEditPreferences') + .returns(Promise.resolve()); + const showTabsCheckbox = valueOf('Show tabs', 'editPreferences') + .firstElementChild; + showTabsCheckbox.checked = false; + element._handleEditShowTabsChanged(); + + assert.isTrue(element.hasUnsavedChanges); + + // Save the change. + return element.save().then(() => { assert.isFalse(element.hasUnsavedChanges); }); - - test('save changes', () => { - sandbox.stub(element.$.restAPI, 'saveEditPreferences') - .returns(Promise.resolve()); - const showTabsCheckbox = valueOf('Show tabs', 'editPreferences') - .firstElementChild; - showTabsCheckbox.checked = false; - element._handleEditShowTabsChanged(); - - assert.isTrue(element.hasUnsavedChanges); - - // Save the change. - return element.save().then(() => { - assert.isFalse(element.hasUnsavedChanges); - }); - }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js index c60568c..fc97079 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor.js
@@ -14,89 +14,99 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrEmailEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-email-editor'; } +import '@polymer/iron-input/iron-input.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-email-editor_html.js'; - static get properties() { - return { - hasUnsavedChanges: { - type: Boolean, - notify: true, - value: false, - }, +/** @extends Polymer.Element */ +class GrEmailEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _emails: Array, - _emailsToRemove: { - type: Array, - value() { return []; }, - }, - /** @type {?string} */ - _newPreferred: { - type: String, - value: null, - }, - }; + static get is() { return 'gr-email-editor'; } + + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + notify: true, + value: false, + }, + + _emails: Array, + _emailsToRemove: { + type: Array, + value() { return []; }, + }, + /** @type {?string} */ + _newPreferred: { + type: String, + value: null, + }, + }; + } + + loadData() { + return this.$.restAPI.getAccountEmails().then(emails => { + this._emails = emails; + }); + } + + save() { + const promises = []; + + for (const emailObj of this._emailsToRemove) { + promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email)); } - loadData() { - return this.$.restAPI.getAccountEmails().then(emails => { - this._emails = emails; - }); + if (this._newPreferred) { + promises.push(this.$.restAPI.setPreferredAccountEmail( + this._newPreferred)); } - save() { - const promises = []; + return Promise.all(promises).then(() => { + this._emailsToRemove = []; + this._newPreferred = null; + this.hasUnsavedChanges = false; + }); + } - for (const emailObj of this._emailsToRemove) { - promises.push(this.$.restAPI.deleteAccountEmail(emailObj.email)); - } + _handleDeleteButton(e) { + const index = parseInt(dom(e).localTarget + .getAttribute('data-index'), 10); + const email = this._emails[index]; + this.push('_emailsToRemove', email); + this.splice('_emails', index, 1); + this.hasUnsavedChanges = true; + } - if (this._newPreferred) { - promises.push(this.$.restAPI.setPreferredAccountEmail( - this._newPreferred)); - } - - return Promise.all(promises).then(() => { - this._emailsToRemove = []; - this._newPreferred = null; - this.hasUnsavedChanges = false; - }); - } - - _handleDeleteButton(e) { - const index = parseInt(Polymer.dom(e).localTarget - .getAttribute('data-index'), 10); - const email = this._emails[index]; - this.push('_emailsToRemove', email); - this.splice('_emails', index, 1); - this.hasUnsavedChanges = true; - } - - _handlePreferredControlClick(e) { - if (e.target.classList.contains('preferredControl')) { - e.target.firstElementChild.click(); - } - } - - _handlePreferredChange(e) { - const preferred = e.target.value; - for (let i = 0; i < this._emails.length; i++) { - if (preferred === this._emails[i].email) { - this.set(['_emails', i, 'preferred'], true); - this._newPreferred = preferred; - this.hasUnsavedChanges = true; - } else if (this._emails[i].preferred) { - this.set(['_emails', i, 'preferred'], false); - } - } + _handlePreferredControlClick(e) { + if (e.target.classList.contains('preferredControl')) { + e.target.firstElementChild.click(); } } - customElements.define(GrEmailEditor.is, GrEmailEditor); -})(); + _handlePreferredChange(e) { + const preferred = e.target.value; + for (let i = 0; i < this._emails.length; i++) { + if (preferred === this._emails[i].email) { + this.set(['_emails', i, 'preferred'], true); + this._newPreferred = preferred; + this.hasUnsavedChanges = true; + } else if (this._emails[i].preferred) { + this.set(['_emails', i, 'preferred'], false); + } + } + } +} + +customElements.define(GrEmailEditor.is, GrEmailEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js index 041b2a7..b02df3c 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-email-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -65,29 +59,12 @@ <tr> <td class="emailColumn">[[item.email]]</td> <td class="preferredControl" on-click="_handlePreferredControlClick"> - <iron-input - class="preferredRadio" - type="radio" - on-change="_handlePreferredChange" - name="preferred" - bind-value="[[item.email]]" - checked$="[[item.preferred]]"> - <input - is="iron-input" - class="preferredRadio" - type="radio" - on-change="_handlePreferredChange" - name="preferred" - value="[[item.email]]" - checked$="[[item.preferred]]"> + <iron-input class="preferredRadio" type="radio" on-change="_handlePreferredChange" name="preferred" bind-value="[[item.email]]" checked\$="[[item.preferred]]"> + <input is="iron-input" class="preferredRadio" type="radio" on-change="_handlePreferredChange" name="preferred" value="[[item.email]]" checked\$="[[item.preferred]]"> </iron-input> </td> <td> - <gr-button - data-index$="[[index]]" - on-click="_handleDeleteButton" - disabled="[[item.preferred]]" - class="remove-button">Delete</gr-button> + <gr-button data-index\$="[[index]]" on-click="_handleDeleteButton" disabled="[[item.preferred]]" class="remove-button">Delete</gr-button> </td> </tr> </template> @@ -95,6 +72,4 @@ </table> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-email-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html index ecb108d..196f8a9 100644 --- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-email-editor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-email-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-email-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-email-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,121 +40,123 @@ </template> </test-fixture> -<script> - suite('gr-email-editor tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-email-editor.js'; +suite('gr-email-editor tests', () => { + let element; - setup(done => { - const emails = [ - {email: 'email@one.com'}, - {email: 'email@two.com', preferred: true}, - {email: 'email@three.com'}, - ]; + setup(done => { + const emails = [ + {email: 'email@one.com'}, + {email: 'email@two.com', preferred: true}, + {email: 'email@three.com'}, + ]; - stub('gr-rest-api-interface', { - getAccountEmails() { return Promise.resolve(emails); }, - }); - - element = fixture('basic'); - - element.loadData().then(flush(done)); + stub('gr-rest-api-interface', { + getAccountEmails() { return Promise.resolve(emails); }, }); - test('renders', () => { - const rows = element.shadowRoot - .querySelector('table').querySelectorAll('tbody tr'); + element = fixture('basic'); - assert.equal(rows.length, 3); + element.loadData().then(flush(done)); + }); - assert.isFalse(rows[0].querySelector('input[type=radio]').checked); - assert.isNotOk(rows[0].querySelector('gr-button').disabled); + test('renders', () => { + const rows = element.shadowRoot + .querySelector('table').querySelectorAll('tbody tr'); - assert.isTrue(rows[1].querySelector('input[type=radio]').checked); - assert.isOk(rows[1].querySelector('gr-button').disabled); + assert.equal(rows.length, 3); - assert.isFalse(rows[2].querySelector('input[type=radio]').checked); - assert.isNotOk(rows[2].querySelector('gr-button').disabled); + assert.isFalse(rows[0].querySelector('input[type=radio]').checked); + assert.isNotOk(rows[0].querySelector('gr-button').disabled); - assert.isFalse(element.hasUnsavedChanges); - }); + assert.isTrue(rows[1].querySelector('input[type=radio]').checked); + assert.isOk(rows[1].querySelector('gr-button').disabled); - test('edit preferred', () => { - const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange'); - const radios = element.shadowRoot - .querySelector('table').querySelectorAll('input[type=radio]'); + assert.isFalse(rows[2].querySelector('input[type=radio]').checked); + assert.isNotOk(rows[2].querySelector('gr-button').disabled); - assert.isFalse(element.hasUnsavedChanges); - assert.isNotOk(element._newPreferred); - assert.equal(element._emailsToRemove.length, 0); - assert.equal(element._emails.length, 3); - assert.isNotOk(radios[0].checked); - assert.isOk(radios[1].checked); - assert.isFalse(preferredChangedSpy.called); + assert.isFalse(element.hasUnsavedChanges); + }); - radios[0].click(); + test('edit preferred', () => { + const preferredChangedSpy = sinon.spy(element, '_handlePreferredChange'); + const radios = element.shadowRoot + .querySelector('table').querySelectorAll('input[type=radio]'); - assert.isTrue(element.hasUnsavedChanges); - assert.isOk(element._newPreferred); - assert.equal(element._emailsToRemove.length, 0); - assert.equal(element._emails.length, 3); - assert.isOk(radios[0].checked); - assert.isNotOk(radios[1].checked); - assert.isTrue(preferredChangedSpy.called); - }); + assert.isFalse(element.hasUnsavedChanges); + assert.isNotOk(element._newPreferred); + assert.equal(element._emailsToRemove.length, 0); + assert.equal(element._emails.length, 3); + assert.isNotOk(radios[0].checked); + assert.isOk(radios[1].checked); + assert.isFalse(preferredChangedSpy.called); - test('delete email', () => { - const buttons = element.shadowRoot - .querySelector('table').querySelectorAll('gr-button'); + radios[0].click(); - assert.isFalse(element.hasUnsavedChanges); - assert.isNotOk(element._newPreferred); - assert.equal(element._emailsToRemove.length, 0); - assert.equal(element._emails.length, 3); + assert.isTrue(element.hasUnsavedChanges); + assert.isOk(element._newPreferred); + assert.equal(element._emailsToRemove.length, 0); + assert.equal(element._emails.length, 3); + assert.isOk(radios[0].checked); + assert.isNotOk(radios[1].checked); + assert.isTrue(preferredChangedSpy.called); + }); - buttons[2].click(); + test('delete email', () => { + const buttons = element.shadowRoot + .querySelector('table').querySelectorAll('gr-button'); - assert.isTrue(element.hasUnsavedChanges); - assert.isNotOk(element._newPreferred); - assert.equal(element._emailsToRemove.length, 1); - assert.equal(element._emails.length, 2); + assert.isFalse(element.hasUnsavedChanges); + assert.isNotOk(element._newPreferred); + assert.equal(element._emailsToRemove.length, 0); + assert.equal(element._emails.length, 3); - assert.equal(element._emailsToRemove[0].email, 'email@three.com'); - }); + buttons[2].click(); - test('save changes', done => { - const deleteEmailStub = - sinon.stub(element.$.restAPI, 'deleteAccountEmail'); - const setPreferredStub = sinon.stub(element.$.restAPI, - 'setPreferredAccountEmail'); - const rows = element.shadowRoot - .querySelector('table').querySelectorAll('tbody tr'); + assert.isTrue(element.hasUnsavedChanges); + assert.isNotOk(element._newPreferred); + assert.equal(element._emailsToRemove.length, 1); + assert.equal(element._emails.length, 2); - assert.isFalse(element.hasUnsavedChanges); - assert.isNotOk(element._newPreferred); - assert.equal(element._emailsToRemove.length, 0); - assert.equal(element._emails.length, 3); + assert.equal(element._emailsToRemove[0].email, 'email@three.com'); + }); - // Delete the first email and set the last as preferred. - rows[0].querySelector('gr-button').click(); - rows[2].querySelector('input[type=radio]').click(); + test('save changes', done => { + const deleteEmailStub = + sinon.stub(element.$.restAPI, 'deleteAccountEmail'); + const setPreferredStub = sinon.stub(element.$.restAPI, + 'setPreferredAccountEmail'); + const rows = element.shadowRoot + .querySelector('table').querySelectorAll('tbody tr'); - assert.isTrue(element.hasUnsavedChanges); - assert.equal(element._newPreferred, 'email@three.com'); - assert.equal(element._emailsToRemove.length, 1); - assert.equal(element._emailsToRemove[0].email, 'email@one.com'); - assert.equal(element._emails.length, 2); + assert.isFalse(element.hasUnsavedChanges); + assert.isNotOk(element._newPreferred); + assert.equal(element._emailsToRemove.length, 0); + assert.equal(element._emails.length, 3); - // Save the changes. - element.save().then(() => { - assert.equal(deleteEmailStub.callCount, 1); - assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com'); + // Delete the first email and set the last as preferred. + rows[0].querySelector('gr-button').click(); + rows[2].querySelector('input[type=radio]').click(); - assert.isTrue(setPreferredStub.called); - assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com'); + assert.isTrue(element.hasUnsavedChanges); + assert.equal(element._newPreferred, 'email@three.com'); + assert.equal(element._emailsToRemove.length, 1); + assert.equal(element._emailsToRemove[0].email, 'email@one.com'); + assert.equal(element._emails.length, 2); - done(); - }); + // Save the changes. + element.save().then(() => { + assert.equal(deleteEmailStub.callCount, 1); + assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com'); + + assert.isTrue(setPreferredStub.called); + assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com'); + + done(); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js index 9f04915..90631c7 100644 --- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor.js
@@ -14,100 +14,113 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrGpgEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-gpg-editor'; } +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../styles/gr-form-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-gpg-editor_html.js'; - static get properties() { - return { - hasUnsavedChanges: { - type: Boolean, - value: false, - notify: true, - }, - _keys: Array, - /** @type {?} */ - _keyToView: Object, - _newKey: { - type: String, - value: '', - }, - _keysToRemove: { - type: Array, - value() { return []; }, - }, - }; - } +/** @extends Polymer.Element */ +class GrGpgEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - loadData() { - this._keys = []; - return this.$.restAPI.getAccountGPGKeys().then(keys => { - if (!keys) { - return; - } - this._keys = Object.keys(keys) - .map(key => { - const gpgKey = keys[key]; - gpgKey.id = key; - return gpgKey; - }); - }); - } + static get is() { return 'gr-gpg-editor'; } - save() { - const promises = this._keysToRemove.map(key => { - this.$.restAPI.deleteAccountGPGKey(key.id); - }); - - return Promise.all(promises).then(() => { - this._keysToRemove = []; - this.hasUnsavedChanges = false; - }); - } - - _showKey(e) { - const el = Polymer.dom(e).localTarget; - const index = parseInt(el.getAttribute('data-index'), 10); - this._keyToView = this._keys[index]; - this.$.viewKeyOverlay.open(); - } - - _closeOverlay() { - this.$.viewKeyOverlay.close(); - } - - _handleDeleteKey(e) { - const el = Polymer.dom(e).localTarget; - const index = parseInt(el.getAttribute('data-index'), 10); - this.push('_keysToRemove', this._keys[index]); - this.splice('_keys', index, 1); - this.hasUnsavedChanges = true; - } - - _handleAddKey() { - this.$.addButton.disabled = true; - this.$.newKey.disabled = true; - return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]}) - .then(key => { - this.$.newKey.disabled = false; - this._newKey = ''; - this.loadData(); - }) - .catch(() => { - this.$.addButton.disabled = false; - this.$.newKey.disabled = false; - }); - } - - _computeAddButtonDisabled(newKey) { - return !newKey.length; - } + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + value: false, + notify: true, + }, + _keys: Array, + /** @type {?} */ + _keyToView: Object, + _newKey: { + type: String, + value: '', + }, + _keysToRemove: { + type: Array, + value() { return []; }, + }, + }; } - customElements.define(GrGpgEditor.is, GrGpgEditor); -})(); + loadData() { + this._keys = []; + return this.$.restAPI.getAccountGPGKeys().then(keys => { + if (!keys) { + return; + } + this._keys = Object.keys(keys) + .map(key => { + const gpgKey = keys[key]; + gpgKey.id = key; + return gpgKey; + }); + }); + } + + save() { + const promises = this._keysToRemove.map(key => { + this.$.restAPI.deleteAccountGPGKey(key.id); + }); + + return Promise.all(promises).then(() => { + this._keysToRemove = []; + this.hasUnsavedChanges = false; + }); + } + + _showKey(e) { + const el = dom(e).localTarget; + const index = parseInt(el.getAttribute('data-index'), 10); + this._keyToView = this._keys[index]; + this.$.viewKeyOverlay.open(); + } + + _closeOverlay() { + this.$.viewKeyOverlay.close(); + } + + _handleDeleteKey(e) { + const el = dom(e).localTarget; + const index = parseInt(el.getAttribute('data-index'), 10); + this.push('_keysToRemove', this._keys[index]); + this.splice('_keys', index, 1); + this.hasUnsavedChanges = true; + } + + _handleAddKey() { + this.$.addButton.disabled = true; + this.$.newKey.disabled = true; + return this.$.restAPI.addAccountGPGKey({add: [this._newKey.trim()]}) + .then(key => { + this.$.newKey.disabled = false; + this._newKey = ''; + this.loadData(); + }) + .catch(() => { + this.$.addButton.disabled = false; + this.$.newKey.disabled = false; + }); + } + + _computeAddButtonDisabled(newKey) { + return !newKey.length; + } +} + +customElements.define(GrGpgEditor.is, GrGpgEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js index 7b8a191..3ec4642 100644 --- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js +++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-gpg-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -81,29 +72,20 @@ </template> </td> <td class="keyHeader"> - <gr-button - on-click="_showKey" - data-index$="[[index]]" - link>Click to View</gr-button> + <gr-button on-click="_showKey" data-index\$="[[index]]" link="">Click to View</gr-button> </td> <td> - <gr-copy-clipboard - has-tooltip - button-title="Copy GPG public key to clipboard" - hide-input - text="[[key.key]]"> + <gr-copy-clipboard has-tooltip="" button-title="Copy GPG public key to clipboard" hide-input="" text="[[key.key]]"> </gr-copy-clipboard> </td> <td> - <gr-button - data-index$="[[index]]" - on-click="_handleDeleteKey">Delete</gr-button> + <gr-button data-index\$="[[index]]" on-click="_handleDeleteKey">Delete</gr-button> </td> </tr> </template> </tbody> </table> - <gr-overlay id="viewKeyOverlay" with-backdrop> + <gr-overlay id="viewKeyOverlay" with-backdrop=""> <fieldset> <section> <span class="title">Status</span> @@ -114,32 +96,19 @@ <span class="value">[[_keyToView.key]]</span> </section> </fieldset> - <gr-button - class="closeButton" - on-click="_closeOverlay">Close</gr-button> + <gr-button class="closeButton" on-click="_closeOverlay">Close</gr-button> </gr-overlay> - <gr-button - on-click="save" - disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button> + <gr-button on-click="save" disabled\$="[[!hasUnsavedChanges]]">Save changes</gr-button> </fieldset> <fieldset> <section> <span class="title">New GPG key</span> <span class="value"> - <iron-autogrow-textarea - id="newKey" - autocomplete="on" - bind-value="{{_newKey}}" - placeholder="New GPG Key"></iron-autogrow-textarea> + <iron-autogrow-textarea id="newKey" autocomplete="on" bind-value="{{_newKey}}" placeholder="New GPG Key"></iron-autogrow-textarea> </span> </section> - <gr-button - id="addButton" - disabled$="[[_computeAddButtonDisabled(_newKey)]]" - on-click="_handleAddKey">Add new GPG key</gr-button> + <gr-button id="addButton" disabled\$="[[_computeAddButtonDisabled(_newKey)]]" on-click="_handleAddKey">Add new GPG key</gr-button> </fieldset> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-gpg-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html index 08c36fe..5c95222 100644 --- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-gpg-editor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-gpg-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-gpg-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-gpg-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,163 +40,166 @@ </template> </test-fixture> -<script> - suite('gr-gpg-editor tests', async () => { - await readyToTest(); - let element; - let keys; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-gpg-editor.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-gpg-editor tests', () => { + let element; + let keys; - setup(done => { - const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B'; - const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B'; - keys = { - AFC8A49B: { - fingerprint: fingerprint1, - user_ids: [ - 'John Doe john.doe@example.com', - ], - key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' + - '\nVersion: BCPG v1.52\n\t<key 1>', - status: 'TRUSTED', - problems: [], - }, - AED9B59C: { - fingerprint: fingerprint2, - user_ids: [ - 'Gerrit gerrit@example.com', - ], - key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' + - '\nVersion: BCPG v1.52\n\t<key 2>', - status: 'TRUSTED', - problems: [], - }, - }; + setup(done => { + const fingerprint1 = '0192 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B'; + const fingerprint2 = '0196 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B'; + keys = { + AFC8A49B: { + fingerprint: fingerprint1, + user_ids: [ + 'John Doe john.doe@example.com', + ], + key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' + + '\nVersion: BCPG v1.52\n\t<key 1>', + status: 'TRUSTED', + problems: [], + }, + AED9B59C: { + fingerprint: fingerprint2, + user_ids: [ + 'Gerrit gerrit@example.com', + ], + key: '-----BEGIN PGP PUBLIC KEY BLOCK-----' + + '\nVersion: BCPG v1.52\n\t<key 2>', + status: 'TRUSTED', + problems: [], + }, + }; - stub('gr-rest-api-interface', { - getAccountGPGKeys() { return Promise.resolve(keys); }, - }); - - element = fixture('basic'); - - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getAccountGPGKeys() { return Promise.resolve(keys); }, }); - test('renders', () => { - const rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); + element = fixture('basic'); - assert.equal(rows.length, 2); + element.loadData().then(() => { flush(done); }); + }); - let cells = rows[0].querySelectorAll('td'); - assert.equal(cells[0].textContent, 'AFC8A49B'); + test('renders', () => { + const rows = dom(element.root).querySelectorAll('tbody tr'); - cells = rows[1].querySelectorAll('td'); - assert.equal(cells[0].textContent, 'AED9B59C'); - }); + assert.equal(rows.length, 2); - test('remove key', done => { - const lastKey = keys[Object.keys(keys)[1]]; + let cells = rows[0].querySelectorAll('td'); + assert.equal(cells[0].textContent, 'AFC8A49B'); - const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey', - () => Promise.resolve()); + cells = rows[1].querySelectorAll('td'); + assert.equal(cells[0].textContent, 'AED9B59C'); + }); + test('remove key', done => { + const lastKey = keys[Object.keys(keys)[1]]; + + const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountGPGKey', + () => Promise.resolve()); + + assert.equal(element._keysToRemove.length, 0); + assert.isFalse(element.hasUnsavedChanges); + + // Get the delete button for the last row. + const button = dom(element.root).querySelector( + 'tbody tr:last-of-type td:nth-child(6) gr-button'); + + MockInteractions.tap(button); + + assert.equal(element._keys.length, 1); + assert.equal(element._keysToRemove.length, 1); + assert.equal(element._keysToRemove[0], lastKey); + assert.isTrue(element.hasUnsavedChanges); + assert.isFalse(saveStub.called); + + element.save().then(() => { + assert.isTrue(saveStub.called); + assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]); assert.equal(element._keysToRemove.length, 0); assert.isFalse(element.hasUnsavedChanges); - - // Get the delete button for the last row. - const button = Polymer.dom(element.root).querySelector( - 'tbody tr:last-of-type td:nth-child(6) gr-button'); - - MockInteractions.tap(button); - - assert.equal(element._keys.length, 1); - assert.equal(element._keysToRemove.length, 1); - assert.equal(element._keysToRemove[0], lastKey); - assert.isTrue(element.hasUnsavedChanges); - assert.isFalse(saveStub.called); - - element.save().then(() => { - assert.isTrue(saveStub.called); - assert.equal(saveStub.lastCall.args[0], Object.keys(keys)[1]); - assert.equal(element._keysToRemove.length, 0); - assert.isFalse(element.hasUnsavedChanges); - done(); - }); - }); - - test('show key', () => { - const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open'); - - // Get the show button for the last row. - const button = Polymer.dom(element.root).querySelector( - 'tbody tr:last-of-type td:nth-child(4) gr-button'); - - MockInteractions.tap(button); - - assert.equal(element._keyToView, keys[Object.keys(keys)[1]]); - assert.isTrue(openSpy.called); - }); - - test('add key', done => { - const newKeyString = - '-----BEGIN PGP PUBLIC KEY BLOCK-----' + - '\nVersion: BCPG v1.52\n\t<key 3>'; - const newKeyObject = { - ADE8A59B: { - fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B', - user_ids: [ - 'John john@example.com', - ], - key: newKeyString, - status: 'TRUSTED', - problems: [], - }, - }; - - const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey', - () => Promise.resolve(newKeyObject)); - - element._newKey = newKeyString; - - assert.isFalse(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - - element._handleAddKey().then(() => { - assert.isTrue(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - assert.equal(element._keys.length, 2); - done(); - }); - - assert.isTrue(element.$.addButton.disabled); - assert.isTrue(element.$.newKey.disabled); - - assert.isTrue(addStub.called); - assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]}); - }); - - test('add invalid key', done => { - const newKeyString = 'not even close to valid'; - - const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey', - () => Promise.reject(new Error('error'))); - - element._newKey = newKeyString; - - assert.isFalse(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - - element._handleAddKey().then(() => { - assert.isFalse(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - assert.equal(element._keys.length, 2); - done(); - }); - - assert.isTrue(element.$.addButton.disabled); - assert.isTrue(element.$.newKey.disabled); - - assert.isTrue(addStub.called); - assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]}); + done(); }); }); + + test('show key', () => { + const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open'); + + // Get the show button for the last row. + const button = dom(element.root).querySelector( + 'tbody tr:last-of-type td:nth-child(4) gr-button'); + + MockInteractions.tap(button); + + assert.equal(element._keyToView, keys[Object.keys(keys)[1]]); + assert.isTrue(openSpy.called); + }); + + test('add key', done => { + const newKeyString = + '-----BEGIN PGP PUBLIC KEY BLOCK-----' + + '\nVersion: BCPG v1.52\n\t<key 3>'; + const newKeyObject = { + ADE8A59B: { + fingerprint: '0194 723D 42D1 0C5B 32A6 E1E0 9350 9E4B AFC8 A49B', + user_ids: [ + 'John john@example.com', + ], + key: newKeyString, + status: 'TRUSTED', + problems: [], + }, + }; + + const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey', + () => Promise.resolve(newKeyObject)); + + element._newKey = newKeyString; + + assert.isFalse(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + + element._handleAddKey().then(() => { + assert.isTrue(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + assert.equal(element._keys.length, 2); + done(); + }); + + assert.isTrue(element.$.addButton.disabled); + assert.isTrue(element.$.newKey.disabled); + + assert.isTrue(addStub.called); + assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]}); + }); + + test('add invalid key', done => { + const newKeyString = 'not even close to valid'; + + const addStub = sinon.stub(element.$.restAPI, 'addAccountGPGKey', + () => Promise.reject(new Error('error'))); + + element._newKey = newKeyString; + + assert.isFalse(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + + element._handleAddKey().then(() => { + assert.isFalse(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + assert.equal(element._keys.length, 2); + done(); + }); + + assert.isTrue(element.$.addButton.disabled); + assert.isTrue(element.$.newKey.disabled); + + assert.isTrue(addStub.called); + assert.deepEqual(addStub.lastCall.args[0], {add: [newKeyString]}); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js index c7b5faa..01739cd 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list.js
@@ -14,39 +14,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrGroupList extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-group-list'; } +import '../../../styles/shared-styles.js'; +import '../../../styles/gr-form-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-group-list_html.js'; - static get properties() { - return { - _groups: Array, - }; - } +/** @extends Polymer.Element */ +class GrGroupList extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - loadData() { - return this.$.restAPI.getAccountGroups().then(groups => { - this._groups = groups.sort((a, b) => a.name.localeCompare(b.name)); - }); - } + static get is() { return 'gr-group-list'; } - _computeVisibleToAll(group) { - return group.options.visible_to_all ? 'Yes' : 'No'; - } - - _computeGroupPath(group) { - if (!group || !group.id) { return; } - - // Group ID is already encoded from the API - // Decode it here to match with our router encoding behavior - return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id)); - } + static get properties() { + return { + _groups: Array, + }; } - customElements.define(GrGroupList.is, GrGroupList); -})(); + loadData() { + return this.$.restAPI.getAccountGroups().then(groups => { + this._groups = groups.sort((a, b) => a.name.localeCompare(b.name)); + }); + } + + _computeVisibleToAll(group) { + return group.options.visible_to_all ? 'Yes' : 'No'; + } + + _computeGroupPath(group) { + if (!group || !group.id) { return; } + + // Group ID is already encoded from the API + // Decode it here to match with our router encoding behavior + return Gerrit.Nav.getUrlForGroup(decodeURIComponent(group.id)); + } +} + +customElements.define(GrGroupList.is, GrGroupList);
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js index e51294d..ddacd31 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-group-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -52,7 +46,7 @@ <template is="dom-repeat" items="[[_groups]]"> <tr> <td class="nameColumn"> - <a href$="[[_computeGroupPath(item)]]"> + <a href\$="[[_computeGroupPath(item)]]"> [[item.name]] </a> </td> @@ -64,6 +58,4 @@ </table> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-group-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html index 10b67ec..205b413 100644 --- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html +++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-group-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-group-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,81 +40,84 @@ </template> </test-fixture> -<script> - suite('gr-group-list tests', async () => { - await readyToTest(); - let sandbox; - let element; - let groups; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-group-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-group-list tests', () => { + let sandbox; + let element; + let groups; - setup(done => { - sandbox = sinon.sandbox.create(); - groups = [{ - url: 'some url', - options: {}, - description: 'Group 1 description', - group_id: 1, - owner: 'Administrators', - owner_id: '123', - id: 'abc', - name: 'Group 1', - }, { - options: {visible_to_all: true}, - id: '456', - name: 'Group 2', - }, { - options: {}, - id: '789', - name: 'Group 3', - }]; + setup(done => { + sandbox = sinon.sandbox.create(); + groups = [{ + url: 'some url', + options: {}, + description: 'Group 1 description', + group_id: 1, + owner: 'Administrators', + owner_id: '123', + id: 'abc', + name: 'Group 1', + }, { + options: {visible_to_all: true}, + id: '456', + name: 'Group 2', + }, { + options: {}, + id: '789', + name: 'Group 3', + }]; - stub('gr-rest-api-interface', { - getAccountGroups() { return Promise.resolve(groups); }, - }); - - element = fixture('basic'); - - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getAccountGroups() { return Promise.resolve(groups); }, }); - teardown(() => { sandbox.restore(); }); + element = fixture('basic'); - test('renders', () => { - const rows = Array.from( - Polymer.dom(element.root).querySelectorAll('tbody tr')); - - assert.equal(rows.length, 3); - - const nameCells = rows.map(row => - row.querySelectorAll('td a')[0].textContent.trim() - ); - - assert.equal(nameCells[0], 'Group 1'); - assert.equal(nameCells[1], 'Group 2'); - assert.equal(nameCells[2], 'Group 3'); - }); - - test('_computeVisibleToAll', () => { - assert.equal(element._computeVisibleToAll(groups[0]), 'No'); - assert.equal(element._computeVisibleToAll(groups[1]), 'Yes'); - }); - - test('_computeGroupPath', () => { - sandbox.stub(Gerrit.Nav, 'getUrlForGroup', - () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'); - - let group = { - id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5', - }; - - assert.equal(element._computeGroupPath(group), - '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'); - - group = { - name: 'admin', - }; - - assert.isUndefined(element._computeGroupPath(group)); - }); + element.loadData().then(() => { flush(done); }); }); + + teardown(() => { sandbox.restore(); }); + + test('renders', () => { + const rows = Array.from( + dom(element.root).querySelectorAll('tbody tr')); + + assert.equal(rows.length, 3); + + const nameCells = rows.map(row => + row.querySelectorAll('td a')[0].textContent.trim() + ); + + assert.equal(nameCells[0], 'Group 1'); + assert.equal(nameCells[1], 'Group 2'); + assert.equal(nameCells[2], 'Group 3'); + }); + + test('_computeVisibleToAll', () => { + assert.equal(element._computeVisibleToAll(groups[0]), 'No'); + assert.equal(element._computeVisibleToAll(groups[1]), 'Yes'); + }); + + test('_computeGroupPath', () => { + sandbox.stub(Gerrit.Nav, 'getUrlForGroup', + () => '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'); + + let group = { + id: 'e2cd66f88a2db4d391ac068a92d987effbe872f5', + }; + + assert.equal(element._computeGroupPath(group), + '/admin/groups/e2cd66f88a2db4d391ac068a92d987effbe872f5'); + + group = { + name: 'admin', + }; + + assert.isUndefined(element._computeGroupPath(group)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js index efd0c39..02657f8 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password.js
@@ -14,59 +14,70 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrHttpPassword extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-http-password'; } +import '../../../styles/gr-form-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-http-password_html.js'; - static get properties() { - return { - _username: String, - _generatedPassword: String, - _passwordUrl: String, - }; - } +/** @extends Polymer.Element */ +class GrHttpPassword extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this.loadData(); - } + static get is() { return 'gr-http-password'; } - loadData() { - const promises = []; - - promises.push(this.$.restAPI.getAccount().then(account => { - this._username = account.username; - })); - - promises.push(this.$.restAPI.getConfig().then(info => { - this._passwordUrl = info.auth.http_password_url || null; - })); - - return Promise.all(promises); - } - - _handleGenerateTap() { - this._generatedPassword = 'Generating...'; - this.$.generatedPasswordOverlay.open(); - this.$.restAPI.generateAccountHttpPassword().then(newPassword => { - this._generatedPassword = newPassword; - }); - } - - _closeOverlay() { - this.$.generatedPasswordOverlay.close(); - } - - _generatedPasswordOverlayClosed() { - this._generatedPassword = ''; - } + static get properties() { + return { + _username: String, + _generatedPassword: String, + _passwordUrl: String, + }; } - customElements.define(GrHttpPassword.is, GrHttpPassword); -})(); + /** @override */ + attached() { + super.attached(); + this.loadData(); + } + + loadData() { + const promises = []; + + promises.push(this.$.restAPI.getAccount().then(account => { + this._username = account.username; + })); + + promises.push(this.$.restAPI.getConfig().then(info => { + this._passwordUrl = info.auth.http_password_url || null; + })); + + return Promise.all(promises); + } + + _handleGenerateTap() { + this._generatedPassword = 'Generating...'; + this.$.generatedPasswordOverlay.open(); + this.$.restAPI.generateAccountHttpPassword().then(newPassword => { + this._generatedPassword = newPassword; + }); + } + + _closeOverlay() { + this.$.generatedPasswordOverlay.close(); + } + + _generatedPasswordOverlayClosed() { + this._generatedPassword = ''; + } +} + +customElements.define(GrHttpPassword.is, GrHttpPassword);
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js index 22ba457..b75f56e 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-http-password"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .password { font-family: var(--monospace-font-family); @@ -60,47 +52,33 @@ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <div class="gr-form-styles"> - <div hidden$="[[_passwordUrl]]"> + <div hidden\$="[[_passwordUrl]]"> <section> <span class="title">Username</span> <span class="value">[[_username]]</span> </section> - <gr-button - id="generateButton" - on-click="_handleGenerateTap">Generate new password</gr-button> + <gr-button id="generateButton" on-click="_handleGenerateTap">Generate new password</gr-button> </div> - <span hidden$="[[!_passwordUrl]]"> - <a href$="[[_passwordUrl]]" target="_blank" rel="noopener"> + <span hidden\$="[[!_passwordUrl]]"> + <a href\$="[[_passwordUrl]]" target="_blank" rel="noopener"> Obtain password</a> (opens in a new tab) </span> </div> - <gr-overlay - id="generatedPasswordOverlay" - on-iron-overlay-closed="_generatedPasswordOverlayClosed" - with-backdrop> + <gr-overlay id="generatedPasswordOverlay" on-iron-overlay-closed="_generatedPasswordOverlayClosed" with-backdrop=""> <div class="gr-form-styles"> <section id="generatedPasswordDisplay"> <span class="title">New Password:</span> <span class="value">[[_generatedPassword]]</span> - <gr-copy-clipboard - has-tooltip - button-title="Copy password to clipboard" - hide-input - text="[[_generatedPassword]]"> + <gr-copy-clipboard has-tooltip="" button-title="Copy password to clipboard" hide-input="" text="[[_generatedPassword]]"> </gr-copy-clipboard> </section> <section id="passwordWarning"> This password will not be displayed again.<br> If you lose it, you will need to generate a new one. </section> - <gr-button - link - class="closeButton" - on-click="_closeOverlay">Close</gr-button> + <gr-button link="" class="closeButton" on-click="_closeOverlay">Close</gr-button> </div> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-http-password.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html index 974a0f2..57c8622 100644 --- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html +++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-http-password.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-http-password.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-http-password.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,61 +40,62 @@ </template> </test-fixture> -<script> - suite('gr-http-password tests', async () => { - await readyToTest(); - let element; - let account; - let config; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-http-password.js'; +suite('gr-http-password tests', () => { + let element; + let account; + let config; - setup(done => { - account = {username: 'user name'}; - config = {auth: {}}; + setup(done => { + account = {username: 'user name'}; + config = {auth: {}}; - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(account); }, - getConfig() { return Promise.resolve(config); }, - }); - - element = fixture('basic'); - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(account); }, + getConfig() { return Promise.resolve(config); }, }); - test('generate password', () => { - const button = element.$.generateButton; - const nextPassword = 'the new password'; - let generateResolve; - const generateStub = sinon.stub(element.$.restAPI, - 'generateAccountHttpPassword', () => new Promise(resolve => { - generateResolve = resolve; - })); + element = fixture('basic'); + element.loadData().then(() => { flush(done); }); + }); - assert.isNotOk(element._generatedPassword); + test('generate password', () => { + const button = element.$.generateButton; + const nextPassword = 'the new password'; + let generateResolve; + const generateStub = sinon.stub(element.$.restAPI, + 'generateAccountHttpPassword', () => new Promise(resolve => { + generateResolve = resolve; + })); - MockInteractions.tap(button); + assert.isNotOk(element._generatedPassword); - assert.isTrue(generateStub.called); - assert.equal(element._generatedPassword, 'Generating...'); + MockInteractions.tap(button); - generateResolve(nextPassword); + assert.isTrue(generateStub.called); + assert.equal(element._generatedPassword, 'Generating...'); - generateStub.lastCall.returnValue.then(() => { - assert.equal(element._generatedPassword, nextPassword); - }); - }); + generateResolve(nextPassword); - test('without http_password_url', () => { - assert.isNull(element._passwordUrl); - }); - - test('with http_password_url', done => { - config.auth.http_password_url = 'http://example.com/'; - element.loadData().then(() => { - assert.isNotNull(element._passwordUrl); - assert.equal(element._passwordUrl, config.auth.http_password_url); - done(); - }); + generateStub.lastCall.returnValue.then(() => { + assert.equal(element._generatedPassword, nextPassword); }); }); + test('without http_password_url', () => { + assert.isNull(element._passwordUrl); + }); + + test('with http_password_url', done => { + config.auth.http_password_url = 'http://example.com/'; + element.loadData().then(() => { + assert.isNotNull(element._passwordUrl); + assert.equal(element._passwordUrl, config.auth.http_password_url); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js index ac4f9e4..57f0e1d 100644 --- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js +++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities.js
@@ -14,95 +14,108 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const AUTH = [ - 'OPENID', - 'OAUTH', - ]; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../../styles/gr-form-styles.js'; +import '../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-identities_html.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @extends Polymer.Element - */ - class GrIdentities extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-identities'; } +const AUTH = [ + 'OPENID', + 'OAUTH', +]; - static get properties() { - return { - _identities: Object, - _idName: String, - serverConfig: Object, - _showLinkAnotherIdentity: { - type: Boolean, - computed: '_computeShowLinkAnotherIdentity(serverConfig)', - }, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @extends Polymer.Element + */ +class GrIdentities extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - loadData() { - return this.$.restAPI.getExternalIds().then(id => { - this._identities = id; - }); - } + static get is() { return 'gr-identities'; } - _computeIdentity(id) { - return id && id.startsWith('mailto:') ? '' : id; - } - - _computeHideDeleteClass(canDelete) { - return canDelete ? 'show' : ''; - } - - _handleDeleteItemConfirm() { - this.$.overlay.close(); - return this.$.restAPI.deleteAccountIdentity([this._idName]) - .then(() => { this.loadData(); }); - } - - _handleConfirmDialogCancel() { - this.$.overlay.close(); - } - - _handleDeleteItem(e) { - const name = e.model.get('item.identity'); - if (!name) { return; } - this._idName = name; - this.$.overlay.open(); - } - - _computeIsTrusted(item) { - return item ? '' : 'Untrusted'; - } - - filterIdentities(item) { - return !item.identity.startsWith('username:'); - } - - _computeShowLinkAnotherIdentity(config) { - if (config && config.auth && - config.auth.git_basic_auth_policy) { - return AUTH.includes( - config.auth.git_basic_auth_policy.toUpperCase()); - } - - return false; - } - - _computeLinkAnotherIdentity() { - const baseUrl = this.getBaseUrl() || ''; - let pathname = window.location.pathname; - if (baseUrl) { - pathname = '/' + pathname.substring(baseUrl.length); - } - return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link'; - } + static get properties() { + return { + _identities: Object, + _idName: String, + serverConfig: Object, + _showLinkAnotherIdentity: { + type: Boolean, + computed: '_computeShowLinkAnotherIdentity(serverConfig)', + }, + }; } - customElements.define(GrIdentities.is, GrIdentities); -})(); + loadData() { + return this.$.restAPI.getExternalIds().then(id => { + this._identities = id; + }); + } + + _computeIdentity(id) { + return id && id.startsWith('mailto:') ? '' : id; + } + + _computeHideDeleteClass(canDelete) { + return canDelete ? 'show' : ''; + } + + _handleDeleteItemConfirm() { + this.$.overlay.close(); + return this.$.restAPI.deleteAccountIdentity([this._idName]) + .then(() => { this.loadData(); }); + } + + _handleConfirmDialogCancel() { + this.$.overlay.close(); + } + + _handleDeleteItem(e) { + const name = e.model.get('item.identity'); + if (!name) { return; } + this._idName = name; + this.$.overlay.open(); + } + + _computeIsTrusted(item) { + return item ? '' : 'Untrusted'; + } + + filterIdentities(item) { + return !item.identity.startsWith('username:'); + } + + _computeShowLinkAnotherIdentity(config) { + if (config && config.auth && + config.auth.git_basic_auth_policy) { + return AUTH.includes( + config.auth.git_basic_auth_policy.toUpperCase()); + } + + return false; + } + + _computeLinkAnotherIdentity() { + const baseUrl = this.getBaseUrl() || ''; + let pathname = window.location.pathname; + if (baseUrl) { + pathname = '/' + pathname.substring(baseUrl.length); + } + return baseUrl + '/login/' + encodeURIComponent(pathname) + '?link'; + } +} + +customElements.define(GrIdentities.is, GrIdentities);
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js index 53d74f2..f1424cc 100644 --- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js +++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../admin/gr-confirm-delete-item-dialog/gr-confirm-delete-item-dialog.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-identities"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -75,9 +66,7 @@ <td class="emailAddressColumn">[[item.email_address]]</td> <td class="identityColumn">[[_computeIdentity(item.identity)]]</td> <td class="deleteColumn"> - <gr-button - class$="deleteButton [[_computeHideDeleteClass(item.can_delete)]]" - on-click="_handleDeleteItem"> + <gr-button class\$="deleteButton [[_computeHideDeleteClass(item.can_delete)]]" on-click="_handleDeleteItem"> Delete </gr-button> </td> @@ -88,21 +77,14 @@ </fieldset> <template is="dom-if" if="[[_showLinkAnotherIdentity]]"> <fieldset> - <a href$="[[_computeLinkAnotherIdentity()]]"> - <gr-button id="linkAnotherIdentity" link>Link Another Identity</gr-button> + <a href\$="[[_computeLinkAnotherIdentity()]]"> + <gr-button id="linkAnotherIdentity" link="">Link Another Identity</gr-button> </a> </fieldset> </template> </div> - <gr-overlay id="overlay" with-backdrop> - <gr-confirm-delete-item-dialog - class="confirmDialog" - on-confirm="_handleDeleteItemConfirm" - on-cancel="_handleConfirmDialogCancel" - item="[[_idName]]" - item-type="id"></gr-confirm-delete-item-dialog> + <gr-overlay id="overlay" with-backdrop=""> + <gr-confirm-delete-item-dialog class="confirmDialog" on-confirm="_handleDeleteItemConfirm" on-cancel="_handleConfirmDialogCancel" item="[[_idName]]" item-type="id"></gr-confirm-delete-item-dialog> </gr-overlay> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-identities.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html index be73a0c..acf4507 100644 --- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html +++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-identities</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-identities.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-identities.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-identities.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,158 +40,161 @@ </template> </test-fixture> -<script> - suite('gr-identities tests', async () => { - await readyToTest(); - let element; - let sandbox; - const ids = [ - { - identity: 'username:john', - email_address: 'john.doe@example.com', - trusted: true, - }, { - identity: 'gerrit:gerrit', - email_address: 'gerrit@example.com', - }, { - identity: 'mailto:gerrit2@example.com', - email_address: 'gerrit2@example.com', - trusted: true, - can_delete: true, - }, - ]; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-identities.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-identities tests', () => { + let element; + let sandbox; + const ids = [ + { + identity: 'username:john', + email_address: 'john.doe@example.com', + trusted: true, + }, { + identity: 'gerrit:gerrit', + email_address: 'gerrit@example.com', + }, { + identity: 'mailto:gerrit2@example.com', + email_address: 'gerrit2@example.com', + trusted: true, + can_delete: true, + }, + ]; - setup(done => { - sandbox = sinon.sandbox.create(); + setup(done => { + sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getExternalIds() { return Promise.resolve(ids); }, - }); - - element = fixture('basic'); - - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getExternalIds() { return Promise.resolve(ids); }, }); - teardown(() => { - sandbox.restore(); - }); + element = fixture('basic'); - test('renders', () => { - const rows = Array.from( - Polymer.dom(element.root).querySelectorAll('tbody tr')); + element.loadData().then(() => { flush(done); }); + }); - assert.equal(rows.length, 2); + teardown(() => { + sandbox.restore(); + }); - const nameCells = rows.map(row => - row.querySelectorAll('td')[2].textContent - ); + test('renders', () => { + const rows = Array.from( + dom(element.root).querySelectorAll('tbody tr')); - assert.equal(nameCells[0], 'gerrit:gerrit'); - assert.equal(nameCells[1], ''); - }); + assert.equal(rows.length, 2); - test('renders email', () => { - const rows = Array.from( - Polymer.dom(element.root).querySelectorAll('tbody tr')); + const nameCells = rows.map(row => + row.querySelectorAll('td')[2].textContent + ); - assert.equal(rows.length, 2); + assert.equal(nameCells[0], 'gerrit:gerrit'); + assert.equal(nameCells[1], ''); + }); - const nameCells = rows.map(row => - row.querySelectorAll('td')[1].textContent - ); + test('renders email', () => { + const rows = Array.from( + dom(element.root).querySelectorAll('tbody tr')); - assert.equal(nameCells[0], 'gerrit@example.com'); - assert.equal(nameCells[1], 'gerrit2@example.com'); - }); + assert.equal(rows.length, 2); - test('_computeIdentity', () => { - assert.equal( - element._computeIdentity(ids[0].identity), 'username:john'); - assert.equal(element._computeIdentity(ids[2].identity), ''); - }); + const nameCells = rows.map(row => + row.querySelectorAll('td')[1].textContent + ); - test('filterIdentities', () => { - assert.isFalse(element.filterIdentities(ids[0])); + assert.equal(nameCells[0], 'gerrit@example.com'); + assert.equal(nameCells[1], 'gerrit2@example.com'); + }); - assert.isTrue(element.filterIdentities(ids[1])); - }); + test('_computeIdentity', () => { + assert.equal( + element._computeIdentity(ids[0].identity), 'username:john'); + assert.equal(element._computeIdentity(ids[2].identity), ''); + }); - test('delete id', done => { - element._idName = 'mailto:gerrit2@example.com'; - const loadDataStub = sandbox.stub(element, 'loadData'); - element._handleDeleteItemConfirm().then(() => { - assert.isTrue(loadDataStub.called); - done(); - }); - }); + test('filterIdentities', () => { + assert.isFalse(element.filterIdentities(ids[0])); - test('_handleDeleteItem opens modal', () => { - const deleteBtn = - Polymer.dom(element.root).querySelector('.deleteButton'); - const deleteItem = sandbox.stub(element, '_handleDeleteItem'); - MockInteractions.tap(deleteBtn); - assert.isTrue(deleteItem.called); - }); + assert.isTrue(element.filterIdentities(ids[1])); + }); - test('_computeShowLinkAnotherIdentity', () => { - let serverConfig; - - serverConfig = { - auth: { - git_basic_auth_policy: 'OAUTH', - }, - }; - assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'OpenID', - }, - }; - assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'HTTP_LDAP', - }, - }; - assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'LDAP', - }, - }; - assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'HTTP', - }, - }; - assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); - - serverConfig = {}; - assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); - }); - - test('_showLinkAnotherIdentity', () => { - element.serverConfig = { - auth: { - git_basic_auth_policy: 'OAUTH', - }, - }; - - assert.isTrue(element._showLinkAnotherIdentity); - - element.serverConfig = { - auth: { - git_basic_auth_policy: 'LDAP', - }, - }; - - assert.isFalse(element._showLinkAnotherIdentity); + test('delete id', done => { + element._idName = 'mailto:gerrit2@example.com'; + const loadDataStub = sandbox.stub(element, 'loadData'); + element._handleDeleteItemConfirm().then(() => { + assert.isTrue(loadDataStub.called); + done(); }); }); + + test('_handleDeleteItem opens modal', () => { + const deleteBtn = + dom(element.root).querySelector('.deleteButton'); + const deleteItem = sandbox.stub(element, '_handleDeleteItem'); + MockInteractions.tap(deleteBtn); + assert.isTrue(deleteItem.called); + }); + + test('_computeShowLinkAnotherIdentity', () => { + let serverConfig; + + serverConfig = { + auth: { + git_basic_auth_policy: 'OAUTH', + }, + }; + assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'OpenID', + }, + }; + assert.isTrue(element._computeShowLinkAnotherIdentity(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'HTTP_LDAP', + }, + }; + assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'LDAP', + }, + }; + assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'HTTP', + }, + }; + assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); + + serverConfig = {}; + assert.isFalse(element._computeShowLinkAnotherIdentity(serverConfig)); + }); + + test('_showLinkAnotherIdentity', () => { + element.serverConfig = { + auth: { + git_basic_auth_policy: 'OAUTH', + }, + }; + + assert.isTrue(element._showLinkAnotherIdentity); + + element.serverConfig = { + auth: { + git_basic_auth_policy: 'LDAP', + }, + }; + + assert.isFalse(element._showLinkAnotherIdentity); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js index 0ee232b..42982fd 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor.js
@@ -14,68 +14,80 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrMenuEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-menu-editor'; } +import '@polymer/iron-input/iron-input.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import '../../../styles/gr-form-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-menu-editor_html.js'; - static get properties() { - return { - menuItems: Array, - _newName: String, - _newUrl: String, - }; - } +/** @extends Polymer.Element */ +class GrMenuEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _handleMoveUpButton(e) { - const index = Number(Polymer.dom(e).localTarget.dataset.index); - if (index === 0) { return; } - const row = this.menuItems[index]; - const prev = this.menuItems[index - 1]; - this.splice('menuItems', index - 1, 2, row, prev); - } + static get is() { return 'gr-menu-editor'; } - _handleMoveDownButton(e) { - const index = Number(Polymer.dom(e).localTarget.dataset.index); - if (index === this.menuItems.length - 1) { return; } - const row = this.menuItems[index]; - const next = this.menuItems[index + 1]; - this.splice('menuItems', index, 2, next, row); - } - - _handleDeleteButton(e) { - const index = Number(Polymer.dom(e).localTarget.dataset.index); - this.splice('menuItems', index, 1); - } - - _handleAddButton() { - if (this._computeAddDisabled(this._newName, this._newUrl)) { return; } - - this.splice('menuItems', this.menuItems.length, 0, { - name: this._newName, - url: this._newUrl, - target: '_blank', - }); - - this._newName = ''; - this._newUrl = ''; - } - - _computeAddDisabled(newName, newUrl) { - return !newName.length || !newUrl.length; - } - - _handleInputKeydown(e) { - if (e.keyCode === 13) { - e.stopPropagation(); - this._handleAddButton(); - } - } + static get properties() { + return { + menuItems: Array, + _newName: String, + _newUrl: String, + }; } - customElements.define(GrMenuEditor.is, GrMenuEditor); -})(); + _handleMoveUpButton(e) { + const index = Number(dom(e).localTarget.dataset.index); + if (index === 0) { return; } + const row = this.menuItems[index]; + const prev = this.menuItems[index - 1]; + this.splice('menuItems', index - 1, 2, row, prev); + } + + _handleMoveDownButton(e) { + const index = Number(dom(e).localTarget.dataset.index); + if (index === this.menuItems.length - 1) { return; } + const row = this.menuItems[index]; + const next = this.menuItems[index + 1]; + this.splice('menuItems', index, 2, next, row); + } + + _handleDeleteButton(e) { + const index = Number(dom(e).localTarget.dataset.index); + this.splice('menuItems', index, 1); + } + + _handleAddButton() { + if (this._computeAddDisabled(this._newName, this._newUrl)) { return; } + + this.splice('menuItems', this.menuItems.length, 0, { + name: this._newName, + url: this._newUrl, + target: '_blank', + }); + + this._newName = ''; + this._newUrl = ''; + } + + _computeAddDisabled(newName, newUrl) { + return !newName.length || !newUrl.length; + } + + _handleInputKeydown(e) { + if (e.keyCode === 13) { + e.stopPropagation(); + this._handleAddButton(); + } + } +} + +customElements.define(GrMenuEditor.is, GrMenuEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js index 46fc165..58b654f 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> - -<dom-module id="gr-menu-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .buttonColumn { width: 2em; @@ -61,25 +53,13 @@ <td>[[item.name]]</td> <td class="urlCell">[[item.url]]</td> <td class="buttonColumn"> - <gr-button - link - data-index$="[[index]]" - on-click="_handleMoveUpButton" - class="moveUpButton">↑</gr-button> + <gr-button link="" data-index\$="[[index]]" on-click="_handleMoveUpButton" class="moveUpButton">↑</gr-button> </td> <td class="buttonColumn"> - <gr-button - link - data-index$="[[index]]" - on-click="_handleMoveDownButton" - class="moveDownButton">↓</gr-button> + <gr-button link="" data-index\$="[[index]]" on-click="_handleMoveDownButton" class="moveDownButton">↓</gr-button> </td> <td> - <gr-button - link - data-index$="[[index]]" - on-click="_handleDeleteButton" - class="remove-button">Delete</gr-button> + <gr-button link="" data-index\$="[[index]]" on-click="_handleDeleteButton" class="remove-button">Delete</gr-button> </td> </tr> </template> @@ -87,43 +67,22 @@ <tfoot> <tr> <th> - <iron-input - placeholder="New Title" - on-keydown="_handleInputKeydown" - bind-value="{{_newName}}"> - <input - is="iron-input" - placeholder="New Title" - on-keydown="_handleInputKeydown" - bind-value="{{_newName}}"> + <iron-input placeholder="New Title" on-keydown="_handleInputKeydown" bind-value="{{_newName}}"> + <input is="iron-input" placeholder="New Title" on-keydown="_handleInputKeydown" bind-value="{{_newName}}"> </iron-input> </th> <th> - <iron-input - class="newUrlInput" - placeholder="New URL" - on-keydown="_handleInputKeydown" - bind-value="{{_newUrl}}"> - <input - class="newUrlInput" - is="iron-input" - placeholder="New URL" - on-keydown="_handleInputKeydown" - bind-value="{{_newUrl}}"> + <iron-input class="newUrlInput" placeholder="New URL" on-keydown="_handleInputKeydown" bind-value="{{_newUrl}}"> + <input class="newUrlInput" is="iron-input" placeholder="New URL" on-keydown="_handleInputKeydown" bind-value="{{_newUrl}}"> </iron-input> </th> <th></th> <th></th> <th> - <gr-button - link - disabled$="[[_computeAddDisabled(_newName, _newUrl)]]" - on-click="_handleAddButton">Add</gr-button> + <gr-button link="" disabled\$="[[_computeAddDisabled(_newName, _newUrl)]]" on-click="_handleAddButton">Add</gr-button> </th> </tr> </tfoot> </table> </div> - </template> - <script src="gr-menu-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html index a5f2074..930255c 100644 --- a/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-menu-editor/gr-menu-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-menu-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-menu-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-menu-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,146 +40,149 @@ </template> </test-fixture> -<script> - suite('gr-menu-editor tests', async () => { - await readyToTest(); - let element; - let menu; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-menu-editor.js'; +import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-menu-editor tests', () => { + let element; + let menu; - function assertMenuNamesEqual(element, expected) { - const names = element.menuItems.map(i => i.name); - assert.equal(names.length, expected.length); - for (let i = 0; i < names.length; i++) { - assert.equal(names[i], expected[i]); - } + function assertMenuNamesEqual(element, expected) { + const names = element.menuItems.map(i => i.name); + assert.equal(names.length, expected.length); + for (let i = 0; i < names.length; i++) { + assert.equal(names[i], expected[i]); + } + } + + // Click the up/down button (according to direction) for the index'th row. + // The index of the first row is 0, corresponding to the array. + function move(element, index, direction) { + const selector = 'tr:nth-child(' + (index + 1) + ') .move' + + direction + 'Button'; + const button = + element.shadowRoot + .querySelector('tbody').querySelector(selector) + .shadowRoot + .querySelector('paper-button'); + MockInteractions.tap(button); + } + + setup(done => { + element = fixture('basic'); + menu = [ + {url: '/first/url', name: 'first name', target: '_blank'}, + {url: '/second/url', name: 'second name', target: '_blank'}, + {url: '/third/url', name: 'third name', target: '_blank'}, + ]; + element.set('menuItems', menu); + flush$0(); + flush(done); + }); + + test('renders', () => { + const rows = element.shadowRoot + .querySelector('tbody').querySelectorAll('tr'); + let tds; + + assert.equal(rows.length, menu.length); + for (let i = 0; i < menu.length; i++) { + tds = rows[i].querySelectorAll('td'); + assert.equal(tds[0].textContent, menu[i].name); + assert.equal(tds[1].textContent, menu[i].url); } - // Click the up/down button (according to direction) for the index'th row. - // The index of the first row is 0, corresponding to the array. - function move(element, index, direction) { - const selector = 'tr:nth-child(' + (index + 1) + ') .move' + - direction + 'Button'; - const button = - element.shadowRoot - .querySelector('tbody').querySelector(selector) - .shadowRoot - .querySelector('paper-button'); - MockInteractions.tap(button); - } + assert.isTrue(element._computeAddDisabled(element._newName, + element._newUrl)); + }); - setup(done => { - element = fixture('basic'); - menu = [ - {url: '/first/url', name: 'first name', target: '_blank'}, - {url: '/second/url', name: 'second name', target: '_blank'}, - {url: '/third/url', name: 'third name', target: '_blank'}, - ]; - element.set('menuItems', menu); - Polymer.dom.flush(); - flush(done); - }); + test('_computeAddDisabled', () => { + assert.isTrue(element._computeAddDisabled('', '')); + assert.isTrue(element._computeAddDisabled('name', '')); + assert.isTrue(element._computeAddDisabled('', 'url')); + assert.isFalse(element._computeAddDisabled('name', 'url')); + }); - test('renders', () => { - const rows = element.shadowRoot - .querySelector('tbody').querySelectorAll('tr'); - let tds; + test('add a new menu item', () => { + const newName = 'new name'; + const newUrl = 'new url'; - assert.equal(rows.length, menu.length); - for (let i = 0; i < menu.length; i++) { - tds = rows[i].querySelectorAll('td'); - assert.equal(tds[0].textContent, menu[i].name); - assert.equal(tds[1].textContent, menu[i].url); - } + element._newName = newName; + element._newUrl = newUrl; + assert.isFalse(element._computeAddDisabled(element._newName, + element._newUrl)); - assert.isTrue(element._computeAddDisabled(element._newName, - element._newUrl)); - }); + const originalMenuLength = element.menuItems.length; - test('_computeAddDisabled', () => { - assert.isTrue(element._computeAddDisabled('', '')); - assert.isTrue(element._computeAddDisabled('name', '')); - assert.isTrue(element._computeAddDisabled('', 'url')); - assert.isFalse(element._computeAddDisabled('name', 'url')); - }); + element._handleAddButton(); - test('add a new menu item', () => { - const newName = 'new name'; - const newUrl = 'new url'; + assert.equal(element.menuItems.length, originalMenuLength + 1); + assert.equal(element.menuItems[element.menuItems.length - 1].name, + newName); + assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl); + }); - element._newName = newName; - element._newUrl = newUrl; - assert.isFalse(element._computeAddDisabled(element._newName, - element._newUrl)); + test('move items down', () => { + assertMenuNamesEqual(element, + ['first name', 'second name', 'third name']); - const originalMenuLength = element.menuItems.length; + // Move the middle item down + move(element, 1, 'Down'); + assertMenuNamesEqual(element, + ['first name', 'third name', 'second name']); - element._handleAddButton(); + // Moving the bottom item down is a no-op. + move(element, 2, 'Down'); + assertMenuNamesEqual(element, + ['first name', 'third name', 'second name']); + }); - assert.equal(element.menuItems.length, originalMenuLength + 1); - assert.equal(element.menuItems[element.menuItems.length - 1].name, - newName); - assert.equal(element.menuItems[element.menuItems.length - 1].url, newUrl); - }); + test('move items up', () => { + assertMenuNamesEqual(element, + ['first name', 'second name', 'third name']); - test('move items down', () => { - assertMenuNamesEqual(element, - ['first name', 'second name', 'third name']); + // Move the last item up twice to be the first. + move(element, 2, 'Up'); + move(element, 1, 'Up'); + assertMenuNamesEqual(element, + ['third name', 'first name', 'second name']); - // Move the middle item down - move(element, 1, 'Down'); - assertMenuNamesEqual(element, - ['first name', 'third name', 'second name']); + // Moving the top item up is a no-op. + move(element, 0, 'Up'); + assertMenuNamesEqual(element, + ['third name', 'first name', 'second name']); + }); - // Moving the bottom item down is a no-op. - move(element, 2, 'Down'); - assertMenuNamesEqual(element, - ['first name', 'third name', 'second name']); - }); + test('remove item', () => { + assertMenuNamesEqual(element, + ['first name', 'second name', 'third name']); - test('move items up', () => { - assertMenuNamesEqual(element, - ['first name', 'second name', 'third name']); + // Tap the delete button for the middle item. + MockInteractions.tap(element.shadowRoot + .querySelector('tbody') + .querySelector('tr:nth-child(2) .remove-button') + .shadowRoot + .querySelector('paper-button')); - // Move the last item up twice to be the first. - move(element, 2, 'Up'); - move(element, 1, 'Up'); - assertMenuNamesEqual(element, - ['third name', 'first name', 'second name']); + assertMenuNamesEqual(element, ['first name', 'third name']); - // Moving the top item up is a no-op. - move(element, 0, 'Up'); - assertMenuNamesEqual(element, - ['third name', 'first name', 'second name']); - }); - - test('remove item', () => { - assertMenuNamesEqual(element, - ['first name', 'second name', 'third name']); - - // Tap the delete button for the middle item. + // Delete remaining items. + for (let i = 0; i < 2; i++) { MockInteractions.tap(element.shadowRoot .querySelector('tbody') - .querySelector('tr:nth-child(2) .remove-button') + .querySelector('tr:first-child .remove-button') .shadowRoot .querySelector('paper-button')); + } + assertMenuNamesEqual(element, []); - assertMenuNamesEqual(element, ['first name', 'third name']); - - // Delete remaining items. - for (let i = 0; i < 2; i++) { - MockInteractions.tap(element.shadowRoot - .querySelector('tbody') - .querySelector('tr:first-child .remove-button') - .shadowRoot - .querySelector('paper-button')); - } - assertMenuNamesEqual(element, []); - - // Add item to empty menu. - element._newName = 'new name'; - element._newUrl = 'new url'; - element._handleAddButton(); - assertMenuNamesEqual(element, ['new name']); - }); + // Add item to empty menu. + element._newName = 'new name'; + element._newUrl = 'new url'; + element._handleAddButton(); + assertMenuNamesEqual(element, ['new name']); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js index 4bb98d0..c20800f 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog.js
@@ -14,142 +14,155 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '@polymer/iron-input/iron-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/gr-form-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-registration-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrRegistrationDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-registration-dialog'; } + /** + * Fired when account details are changed. + * + * @event account-detail-update + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the close button is pressed. + * + * @event close */ - class GrRegistrationDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-registration-dialog'; } - /** - * Fired when account details are changed. - * - * @event account-detail-update - */ - /** - * Fired when the close button is pressed. - * - * @event close - */ - - static get properties() { - return { - settingsUrl: String, - /** @type {?} */ - _account: { - type: Object, - value: () => { - // Prepopulate possibly undefined fields with values to trigger - // computed bindings. - return {email: null, name: null, username: null}; - }, + static get properties() { + return { + settingsUrl: String, + /** @type {?} */ + _account: { + type: Object, + value: () => { + // Prepopulate possibly undefined fields with values to trigger + // computed bindings. + return {email: null, name: null, username: null}; }, - _usernameMutable: { - type: Boolean, - computed: '_computeUsernameMutable(_serverConfig, _account.username)', - }, - _loading: { - type: Boolean, - value: true, - observer: '_loadingChanged', - }, - _saving: { - type: Boolean, - value: false, - }, - _serverConfig: Object, - }; - } - - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'dialog'); - } - - loadData() { - this._loading = true; - - const loadAccount = this.$.restAPI.getAccount().then(account => { - // Using Object.assign here allows preservation of the default values - // supplied in the value generating function of this._account, unless - // they are overridden by properties in the account from the response. - this._account = Object.assign({}, this._account, account); - }); - - const loadConfig = this.$.restAPI.getConfig().then(config => { - this._serverConfig = config; - }); - - return Promise.all([loadAccount, loadConfig]).then(() => { - this._loading = false; - }); - } - - _save() { - this._saving = true; - const promises = [ - this.$.restAPI.setAccountName(this.$.name.value), - this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''), - ]; - - if (this._usernameMutable) { - promises.push(this.$.restAPI.setAccountUsername(this.$.username.value)); - } - - return Promise.all(promises).then(() => { - this._saving = false; - this.fire('account-detail-update'); - }); - } - - _handleSave(e) { - e.preventDefault(); - this._save().then(this.close.bind(this)); - } - - _handleClose(e) { - e.preventDefault(); - this.close(); - } - - close() { - this._saving = true; // disable buttons indefinitely - this.fire('close'); - } - - _computeSaveDisabled(name, email, saving) { - return !name || !email || saving; - } - - _computeUsernameMutable(config, username) { - // Polymer 2: check for undefined - if ([ - config, - username, - ].some(arg => arg === undefined)) { - return undefined; - } - - return config.auth.editable_account_fields.includes('USER_NAME') && - !username; - } - - _computeUsernameClass(usernameMutable) { - return usernameMutable ? '' : 'hide'; - } - - _loadingChanged() { - this.classList.toggle('loading', this._loading); - } + }, + _usernameMutable: { + type: Boolean, + computed: '_computeUsernameMutable(_serverConfig, _account.username)', + }, + _loading: { + type: Boolean, + value: true, + observer: '_loadingChanged', + }, + _saving: { + type: Boolean, + value: false, + }, + _serverConfig: Object, + }; } - customElements.define(GrRegistrationDialog.is, GrRegistrationDialog); -})(); + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } + + loadData() { + this._loading = true; + + const loadAccount = this.$.restAPI.getAccount().then(account => { + // Using Object.assign here allows preservation of the default values + // supplied in the value generating function of this._account, unless + // they are overridden by properties in the account from the response. + this._account = Object.assign({}, this._account, account); + }); + + const loadConfig = this.$.restAPI.getConfig().then(config => { + this._serverConfig = config; + }); + + return Promise.all([loadAccount, loadConfig]).then(() => { + this._loading = false; + }); + } + + _save() { + this._saving = true; + const promises = [ + this.$.restAPI.setAccountName(this.$.name.value), + this.$.restAPI.setPreferredAccountEmail(this.$.email.value || ''), + ]; + + if (this._usernameMutable) { + promises.push(this.$.restAPI.setAccountUsername(this.$.username.value)); + } + + return Promise.all(promises).then(() => { + this._saving = false; + this.fire('account-detail-update'); + }); + } + + _handleSave(e) { + e.preventDefault(); + this._save().then(this.close.bind(this)); + } + + _handleClose(e) { + e.preventDefault(); + this.close(); + } + + close() { + this._saving = true; // disable buttons indefinitely + this.fire('close'); + } + + _computeSaveDisabled(name, email, saving) { + return !name || !email || saving; + } + + _computeUsernameMutable(config, username) { + // Polymer 2: check for undefined + if ([ + config, + username, + ].some(arg => arg === undefined)) { + return undefined; + } + + return config.auth.editable_account_fields.includes('USER_NAME') && + !username; + } + + _computeUsernameClass(usernameMutable) { + return usernameMutable ? '' : 'hide'; + } + + _loadingChanged() { + this.classList.toggle('loading', this._loading); + } +} + +customElements.define(GrRegistrationDialog.is, GrRegistrationDialog);
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js index c289a49..737e6d5 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-registration-dialog"> - <template> +export const htmlTemplate = html` <style include="gr-form-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -85,31 +76,19 @@ <hr> <section> <div class="title">Full Name</div> - <iron-input - bind-value="{{_account.name}}"> - <input - is="iron-input" - id="name" - bind-value="{{_account.name}}" - disabled="[[_saving]]"> + <iron-input bind-value="{{_account.name}}"> + <input is="iron-input" id="name" bind-value="{{_account.name}}" disabled="[[_saving]]"> </iron-input> </section> - <section class$="[[_computeUsernameClass(_usernameMutable)]]"> + <section class\$="[[_computeUsernameClass(_usernameMutable)]]"> <div class="title">Username</div> - <iron-input - bind-value="{{_account.username}}"> - <input - is="iron-input" - id="username" - bind-value="{{_account.username}}" - disabled="[[_saving]]"> + <iron-input bind-value="{{_account.username}}"> + <input is="iron-input" id="username" bind-value="{{_account.username}}" disabled="[[_saving]]"> </iron-input> </section> <section> <div class="title">Preferred Email</div> - <select - id="email" - disabled="[[_saving]]"> + <select id="email" disabled="[[_saving]]"> <option value="[[_account.email]]">[[_account.email]]</option> <template is="dom-repeat" items="[[_account.secondary_emails]]"> <option value="[[item]]">[[item]]</option> @@ -119,24 +98,13 @@ <hr> <p> More configuration options for Gerrit may be found in the - <a on-click="close" href$="[[settingsUrl]]">settings</a>. + <a on-click="close" href\$="[[settingsUrl]]">settings</a>. </p> </main> <footer> - <gr-button - id="closeButton" - link - disabled="[[_saving]]" - on-click="_handleClose">Close</gr-button> - <gr-button - id="saveButton" - primary - link - disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]" - on-click="_handleSave">Save</gr-button> + <gr-button id="closeButton" link="" disabled="[[_saving]]" on-click="_handleClose">Close</gr-button> + <gr-button id="saveButton" primary="" link="" disabled="[[_computeSaveDisabled(_account.name, _account.email, _saving)]]" on-click="_handleSave">Save</gr-button> </footer> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-registration-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html index a3be75c..9aaaaa7 100644 --- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html +++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-registration-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-registration-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-registration-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-registration-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -41,149 +46,151 @@ </template> </test-fixture> -<script> - suite('gr-registration-dialog tests', async () => { - await readyToTest(); - let element; - let account; - let sandbox; - let _listeners; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-registration-dialog.js'; +suite('gr-registration-dialog tests', () => { + let element; + let account; + let sandbox; + let _listeners; - setup(() => { - sandbox = sinon.sandbox.create(); - _listeners = {}; + setup(() => { + sandbox = sinon.sandbox.create(); + _listeners = {}; - account = { - name: 'name', - username: null, - email: 'email', - secondary_emails: [ - 'email2', - 'email3', - ], - }; + account = { + name: 'name', + username: null, + email: 'email', + secondary_emails: [ + 'email2', + 'email3', + ], + }; - stub('gr-rest-api-interface', { - getAccount() { - return Promise.resolve(account); - }, - setAccountName(name) { - account.name = name; - return Promise.resolve(); - }, - setAccountUsername(username) { - account.username = username; - return Promise.resolve(); - }, - setPreferredAccountEmail(email) { - account.email = email; - return Promise.resolve(); - }, - getConfig() { - return Promise.resolve( - {auth: {editable_account_fields: ['USER_NAME']}}); - }, - }); - - element = fixture('basic'); - - return element.loadData(); + stub('gr-rest-api-interface', { + getAccount() { + return Promise.resolve(account); + }, + setAccountName(name) { + account.name = name; + return Promise.resolve(); + }, + setAccountUsername(username) { + account.username = username; + return Promise.resolve(); + }, + setPreferredAccountEmail(email) { + account.email = email; + return Promise.resolve(); + }, + getConfig() { + return Promise.resolve( + {auth: {editable_account_fields: ['USER_NAME']}}); + }, }); - teardown(() => { - sandbox.restore(); - for (const eventType in _listeners) { - if (_listeners.hasOwnProperty(eventType)) { - element.removeEventListener(eventType, _listeners[eventType]); - } + element = fixture('basic'); + + return element.loadData(); + }); + + teardown(() => { + sandbox.restore(); + for (const eventType in _listeners) { + if (_listeners.hasOwnProperty(eventType)) { + element.removeEventListener(eventType, _listeners[eventType]); } - }); - - function listen(eventType) { - return new Promise(resolve => { - _listeners[eventType] = function() { resolve(); }; - element.addEventListener(eventType, _listeners[eventType]); - }); } + }); - function save(opt_action) { - const promise = listen('account-detail-update'); - if (opt_action) { - opt_action(); - } else { - MockInteractions.tap(element.$.saveButton); - } - return promise; + function listen(eventType) { + return new Promise(resolve => { + _listeners[eventType] = function() { resolve(); }; + element.addEventListener(eventType, _listeners[eventType]); + }); + } + + function save(opt_action) { + const promise = listen('account-detail-update'); + if (opt_action) { + opt_action(); + } else { + MockInteractions.tap(element.$.saveButton); } + return promise; + } - function close(opt_action) { - const promise = listen('close'); - if (opt_action) { - opt_action(); - } else { - MockInteractions.tap(element.$.closeButton); - } - return promise; + function close(opt_action) { + const promise = listen('close'); + if (opt_action) { + opt_action(); + } else { + MockInteractions.tap(element.$.closeButton); } + return promise; + } - test('fires the close event on close', done => { - close().then(done); - }); + test('fires the close event on close', done => { + close().then(done); + }); - test('fires the close event on save', done => { - close(() => { - MockInteractions.tap(element.$.saveButton); - }).then(done); - }); + test('fires the close event on save', done => { + close(() => { + MockInteractions.tap(element.$.saveButton); + }).then(done); + }); - test('saves account details', done => { - flush(() => { - element.$.name.value = 'new name'; - element.$.username.value = 'new username'; - element.$.email.value = 'email3'; + test('saves account details', done => { + flush(() => { + element.$.name.value = 'new name'; + element.$.username.value = 'new username'; + element.$.email.value = 'email3'; - // Nothing should be committed yet. - assert.equal(account.name, 'name'); - assert.isNotOk(account.username); - assert.equal(account.email, 'email'); + // Nothing should be committed yet. + assert.equal(account.name, 'name'); + assert.isNotOk(account.username); + assert.equal(account.email, 'email'); - // Save and verify new values are committed. - save() - .then(() => { - assert.equal(account.name, 'new name'); - assert.equal(account.username, 'new username'); - assert.equal(account.email, 'email3'); - }) - .then(done); - }); - }); - - test('email select properly populated', done => { - element._account = {email: 'foo', secondary_emails: ['bar', 'baz']}; - flush(() => { - assert.equal(element.$.email.value, 'foo'); - done(); - }); - }); - - test('save btn disabled', () => { - const compute = element._computeSaveDisabled; - assert.isTrue(compute('', '', false)); - assert.isTrue(compute('', 'test', false)); - assert.isTrue(compute('test', '', false)); - assert.isTrue(compute('test', 'test', true)); - assert.isFalse(compute('test', 'test', false)); - }); - - test('_computeUsernameMutable', () => { - assert.isTrue(element._computeUsernameMutable( - {auth: {editable_account_fields: ['USER_NAME']}}, null)); - assert.isFalse(element._computeUsernameMutable( - {auth: {editable_account_fields: ['USER_NAME']}}, 'abc')); - assert.isFalse(element._computeUsernameMutable( - {auth: {editable_account_fields: []}}, null)); - assert.isFalse(element._computeUsernameMutable( - {auth: {editable_account_fields: []}}, 'abc')); + // Save and verify new values are committed. + save() + .then(() => { + assert.equal(account.name, 'new name'); + assert.equal(account.username, 'new username'); + assert.equal(account.email, 'email3'); + }) + .then(done); }); }); + + test('email select properly populated', done => { + element._account = {email: 'foo', secondary_emails: ['bar', 'baz']}; + flush(() => { + assert.equal(element.$.email.value, 'foo'); + done(); + }); + }); + + test('save btn disabled', () => { + const compute = element._computeSaveDisabled; + assert.isTrue(compute('', '', false)); + assert.isTrue(compute('', 'test', false)); + assert.isTrue(compute('test', '', false)); + assert.isTrue(compute('test', 'test', true)); + assert.isFalse(compute('test', 'test', false)); + }); + + test('_computeUsernameMutable', () => { + assert.isTrue(element._computeUsernameMutable( + {auth: {editable_account_fields: ['USER_NAME']}}, null)); + assert.isFalse(element._computeUsernameMutable( + {auth: {editable_account_fields: ['USER_NAME']}}, 'abc')); + assert.isFalse(element._computeUsernameMutable( + {auth: {editable_account_fields: []}}, null)); + assert.isFalse(element._computeUsernameMutable( + {auth: {editable_account_fields: []}}, 'abc')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js index bae1f38..3884a15 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item.js
@@ -14,22 +14,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-settings-item_html.js'; - /** @extends Polymer.Element */ - class GrSettingsItem extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-settings-item'; } +/** @extends Polymer.Element */ +class GrSettingsItem extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - static get properties() { - return { - anchor: String, - title: String, - }; - } + static get is() { return 'gr-settings-item'; } + + static get properties() { + return { + anchor: String, + title: String, + }; } +} - customElements.define(GrSettingsItem.is, GrSettingsItem); -})(); +customElements.define(GrSettingsItem.is, GrSettingsItem);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js index 937ee79..accb8c8 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-item_html.js
@@ -1,24 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-settings-item"> - <template> +export const htmlTemplate = html` <style> :host { display: block; @@ -27,6 +25,4 @@ </style> <h2 id="[[anchor]]">[[title]]</h2> <slot></slot> - </template> - <script src="gr-settings-item.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js index d5a7eb7..5b11516 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item.js
@@ -14,22 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrSettingsMenuItem extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-settings-menu-item'; } +import '../../../styles/gr-page-nav-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-settings-menu-item_html.js'; - static get properties() { - return { - href: String, - title: String, - }; - } +/** @extends Polymer.Element */ +class GrSettingsMenuItem extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-settings-menu-item'; } + + static get properties() { + return { + href: String, + title: String, + }; } +} - customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem); -})(); +customElements.define(GrSettingsMenuItem.is, GrSettingsMenuItem);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js index c356e80..5cb129f 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-menu-item_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/gr-page-nav-styles.html"> - -<dom-module id="gr-settings-menu-item"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -27,8 +24,6 @@ /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> <div class="navStyles"> - <li><a href$="[[href]]">[[title]]</a></li> + <li><a href\$="[[href]]">[[title]]</a></li> </div> - </template> - <script src="gr-settings-menu-item.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js index 78bad8c..733fa56 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -14,456 +14,489 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const PREFS_SECTION_FIELDS = [ - 'changes_per_page', - 'date_format', - 'time_format', - 'email_strategy', - 'diff_view', - 'publish_comments_on_push', - '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 '@polymer/iron-input/iron-input.js'; +import '../../../behaviors/docs-url-behavior/docs-url-behavior.js'; +import '@polymer/paper-toggle-button/paper-toggle-button.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/gr-menu-page-styles.js'; +import '../../../styles/gr-page-nav-styles.js'; +import '../../../styles/shared-styles.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../gr-change-table-editor/gr-change-table-editor.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-date-formatter/gr-date-formatter.js'; +import '../../shared/gr-diff-preferences/gr-diff-preferences.js'; +import '../../shared/gr-page-nav/gr-page-nav.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../shared/gr-select/gr-select.js'; +import '../gr-account-info/gr-account-info.js'; +import '../gr-agreements-list/gr-agreements-list.js'; +import '../gr-edit-preferences/gr-edit-preferences.js'; +import '../gr-email-editor/gr-email-editor.js'; +import '../gr-gpg-editor/gr-gpg-editor.js'; +import '../gr-group-list/gr-group-list.js'; +import '../gr-http-password/gr-http-password.js'; +import '../gr-identities/gr-identities.js'; +import '../gr-menu-editor/gr-menu-editor.js'; +import '../gr-ssh-editor/gr-ssh-editor.js'; +import '../gr-watched-projects-editor/gr-watched-projects-editor.js'; +import '../../../scripts/util.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-settings-view_html.js'; - const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' + - 'Documentation'; - const GERRIT_DOCS_FILTER_PATH = '/user-notify.html'; - const ABSOLUTE_URL_PATTERN = /^https?:/; - const TRAILING_SLASH_PATTERN = /\/$/; +const PREFS_SECTION_FIELDS = [ + 'changes_per_page', + 'date_format', + 'time_format', + 'email_strategy', + 'diff_view', + 'publish_comments_on_push', + 'work_in_progress_by_default', + 'default_base_for_merges', + 'signed_off_by', + 'email_format', + 'size_bar_in_change_table', + 'relative_date_in_change_table', +]; - const RELOAD_MESSAGE = 'Reloading...'; +const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' + + 'Documentation'; +const GERRIT_DOCS_FILTER_PATH = '/user-notify.html'; +const ABSOLUTE_URL_PATTERN = /^https?:/; +const TRAILING_SLASH_PATTERN = /\/$/; - const HTTP_AUTH = [ - 'HTTP', - 'HTTP_LDAP', - ]; +const RELOAD_MESSAGE = 'Reloading...'; + +const HTTP_AUTH = [ + 'HTTP', + 'HTTP_LDAP', +]; + +/** + * @appliesMixin Gerrit.DocsUrlMixin + * @appliesMixin Gerrit.ChangeTableMixin + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrSettingsView extends mixinBehaviors( [ + Gerrit.DocsUrlBehavior, + Gerrit.ChangeTableBehavior, + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-settings-view'; } + /** + * Fired when the title of the page should change. + * + * @event title-change + */ /** - * @appliesMixin Gerrit.DocsUrlMixin - * @appliesMixin Gerrit.ChangeTableMixin - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired with email confirmation text, or when the page reloads. + * + * @event show-alert */ - class GrSettingsView extends Polymer.mixinBehaviors( [ - Gerrit.DocsUrlBehavior, - Gerrit.ChangeTableBehavior, - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-settings-view'; } - /** - * Fired when the title of the page should change. - * - * @event title-change - */ - /** - * Fired with email confirmation text, or when the page reloads. - * - * @event show-alert - */ + static get properties() { + return { + prefs: { + type: Object, + value() { return {}; }, + }, + params: { + type: Object, + value() { return {}; }, + }, + _accountInfoChanged: Boolean, + _changeTableColumnsNotDisplayed: Array, + /** @type {?} */ + _localPrefs: { + type: Object, + value() { return {}; }, + }, + _localChangeTableColumns: { + type: Array, + value() { return []; }, + }, + _localMenu: { + type: Array, + value() { return []; }, + }, + _loading: { + type: Boolean, + value: true, + }, + _changeTableChanged: { + type: Boolean, + value: false, + }, + _prefsChanged: { + type: Boolean, + value: false, + }, + /** @type {?} */ + _diffPrefsChanged: Boolean, + /** @type {?} */ + _editPrefsChanged: Boolean, + _menuChanged: { + type: Boolean, + value: false, + }, + _watchedProjectsChanged: { + type: Boolean, + value: false, + }, + _keysChanged: { + type: Boolean, + value: false, + }, + _gpgKeysChanged: { + type: Boolean, + value: false, + }, + _newEmail: String, + _addingEmail: { + type: Boolean, + value: false, + }, + _lastSentVerificationEmail: { + type: String, + value: null, + }, + /** @type {?} */ + _serverConfig: Object, + /** @type {?string} */ + _docsBaseUrl: String, + _emailsChanged: Boolean, - static get properties() { - return { - prefs: { - type: Object, - value() { return {}; }, - }, - params: { - type: Object, - value() { return {}; }, - }, - _accountInfoChanged: Boolean, - _changeTableColumnsNotDisplayed: Array, - /** @type {?} */ - _localPrefs: { - type: Object, - value() { return {}; }, - }, - _localChangeTableColumns: { - type: Array, - value() { return []; }, - }, - _localMenu: { - type: Array, - value() { return []; }, - }, - _loading: { - type: Boolean, - value: true, - }, - _changeTableChanged: { - type: Boolean, - value: false, - }, - _prefsChanged: { - type: Boolean, - value: false, - }, - /** @type {?} */ - _diffPrefsChanged: Boolean, - /** @type {?} */ - _editPrefsChanged: Boolean, - _menuChanged: { - type: Boolean, - value: false, - }, - _watchedProjectsChanged: { - type: Boolean, - value: false, - }, - _keysChanged: { - type: Boolean, - value: false, - }, - _gpgKeysChanged: { - type: Boolean, - value: false, - }, - _newEmail: String, - _addingEmail: { - type: Boolean, - value: false, - }, - _lastSentVerificationEmail: { - type: String, - value: null, - }, - /** @type {?} */ - _serverConfig: Object, - /** @type {?string} */ - _docsBaseUrl: String, - _emailsChanged: Boolean, + /** + * For testing purposes. + */ + _loadingPromise: Object, - /** - * For testing purposes. - */ - _loadingPromise: Object, + _showNumber: Boolean, - _showNumber: Boolean, + _isDark: { + type: Boolean, + value: false, + }, + }; + } - _isDark: { - type: Boolean, - value: false, - }, - }; - } + static get observers() { + return [ + '_handlePrefsChanged(_localPrefs.*)', + '_handleMenuChanged(_localMenu.splices)', + '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)', + ]; + } - static get observers() { - return [ - '_handlePrefsChanged(_localPrefs.*)', - '_handleMenuChanged(_localMenu.splices)', - '_handleChangeTableChanged(_localChangeTableColumns, _showNumber)', - ]; - } + /** @override */ + attached() { + super.attached(); + // Polymer 2: anchor tag won't work on shadow DOM + // we need to manually calling scrollIntoView when hash changed + this.listen(window, 'location-change', '_handleLocationChange'); + this.fire('title-change', {title: 'Settings'}); - /** @override */ - attached() { - super.attached(); - // Polymer 2: anchor tag won't work on shadow DOM - // we need to manually calling scrollIntoView when hash changed - this.listen(window, 'location-change', '_handleLocationChange'); - this.fire('title-change', {title: 'Settings'}); + this._isDark = !!window.localStorage.getItem('dark-theme'); - this._isDark = !!window.localStorage.getItem('dark-theme'); + const promises = [ + this.$.accountInfo.loadData(), + this.$.watchedProjectsEditor.loadData(), + this.$.groupList.loadData(), + this.$.identities.loadData(), + this.$.editPrefs.loadData(), + this.$.diffPrefs.loadData(), + ]; - const promises = [ - this.$.accountInfo.loadData(), - this.$.watchedProjectsEditor.loadData(), - this.$.groupList.loadData(), - this.$.identities.loadData(), - this.$.editPrefs.loadData(), - this.$.diffPrefs.loadData(), - ]; - - promises.push(this.$.restAPI.getPreferences().then(prefs => { - this.prefs = prefs; - this._showNumber = !!prefs.legacycid_in_change_table; - this._copyPrefs('_localPrefs', 'prefs'); - this._cloneMenu(prefs.my); - this._cloneChangeTableColumns(); - })); - - promises.push(this.$.restAPI.getConfig().then(config => { - this._serverConfig = config; - const configPromises = []; - - if (this._serverConfig && this._serverConfig.sshd) { - configPromises.push(this.$.sshEditor.loadData()); - } - - if (this._serverConfig && - this._serverConfig.receive && - this._serverConfig.receive.enable_signed_push) { - configPromises.push(this.$.gpgEditor.loadData()); - } - - configPromises.push( - this.getDocsBaseUrl(config, this.$.restAPI) - .then(baseUrl => { this._docsBaseUrl = baseUrl; })); - - return Promise.all(configPromises); - })); - - if (this.params.emailToken) { - promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then( - message => { - if (message) { - this.fire('show-alert', {message}); - } - this.$.emailEditor.loadData(); - })); - } else { - promises.push(this.$.emailEditor.loadData()); - } - - this._loadingPromise = Promise.all(promises).then(() => { - this._loading = false; - - // Handle anchor tag for initial load - this._handleLocationChange(); - }); - } - - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'location-change', '_handleLocationChange'); - } - - _handleLocationChange() { - // Handle anchor tag after dom attached - const urlHash = window.location.hash; - if (urlHash) { - // Use shadowRoot for Polymer 2 - const elem = (this.shadowRoot || document).querySelector(urlHash); - if (elem) { - elem.scrollIntoView(); - } - } - } - - reloadAccountDetail() { - Promise.all([ - this.$.accountInfo.loadData(), - this.$.emailEditor.loadData(), - ]); - } - - _isLoading() { - return this._loading || this._loading === undefined; - } - - _copyPrefs(to, from) { - for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) { - this.set([to, PREFS_SECTION_FIELDS[i]], - this[from][PREFS_SECTION_FIELDS[i]]); - } - } - - _cloneMenu(prefs) { - const menu = []; - for (const item of prefs) { - menu.push({ - name: item.name, - url: item.url, - target: item.target, - }); - } - this._localMenu = menu; - } - - _cloneChangeTableColumns() { - let columns = this.getVisibleColumns(this.prefs.change_table); - - if (columns.length === 0) { - columns = this.columnNames; - this._changeTableColumnsNotDisplayed = []; - } else { - this._changeTableColumnsNotDisplayed = this.getComplementColumns( - this.prefs.change_table); - } - this._localChangeTableColumns = columns; - } - - _formatChangeTableColumns(changeTableArray) { - return changeTableArray.map(item => { - return {column: item}; - }); - } - - _handleChangeTableChanged() { - if (this._isLoading()) { return; } - this._changeTableChanged = true; - } - - _handlePrefsChanged(prefs) { - 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); - } - - _handleWorkInProgressByDefault() { - this.set('_localPrefs.work_in_progress_by_default', - this.$.workInProgressByDefault.checked); - } - - _handleInsertSignedOff() { - this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked); - } - - _handleMenuChanged() { - if (this._isLoading()) { return; } - this._menuChanged = true; - } - - _handleSaveAccountInfo() { - this.$.accountInfo.save(); - } - - _handleSavePreferences() { - this._copyPrefs('prefs', '_localPrefs'); - - return this.$.restAPI.savePreferences(this.prefs).then(() => { - this._prefsChanged = false; - }); - } - - _handleSaveChangeTable() { - this.set('prefs.change_table', this._localChangeTableColumns); - this.set('prefs.legacycid_in_change_table', this._showNumber); + promises.push(this.$.restAPI.getPreferences().then(prefs => { + this.prefs = prefs; + this._showNumber = !!prefs.legacycid_in_change_table; + this._copyPrefs('_localPrefs', 'prefs'); + this._cloneMenu(prefs.my); this._cloneChangeTableColumns(); - return this.$.restAPI.savePreferences(this.prefs).then(() => { - this._changeTableChanged = false; - }); - } + })); - _handleSaveDiffPreferences() { - this.$.diffPrefs.save(); - } + promises.push(this.$.restAPI.getConfig().then(config => { + this._serverConfig = config; + const configPromises = []; - _handleSaveEditPreferences() { - this.$.editPrefs.save(); - } - - _handleSaveMenu() { - this.set('prefs.my', this._localMenu); - this._cloneMenu(this.prefs.my); - return this.$.restAPI.savePreferences(this.prefs).then(() => { - this._menuChanged = false; - }); - } - - _handleResetMenuButton() { - return this.$.restAPI.getDefaultPreferences().then(data => { - if (data && data.my) { - this._cloneMenu(data.my); - } - }); - } - - _handleSaveWatchedProjects() { - this.$.watchedProjectsEditor.save(); - } - - _computeHeaderClass(changed) { - return changed ? 'edited' : ''; - } - - _handleSaveEmails() { - this.$.emailEditor.save(); - } - - _handleNewEmailKeydown(e) { - if (e.keyCode === 13) { // Enter - e.stopPropagation(); - this._handleAddEmailButton(); - } - } - - _isNewEmailValid(newEmail) { - return newEmail && newEmail.includes('@'); - } - - _computeAddEmailButtonEnabled(newEmail, addingEmail) { - return this._isNewEmailValid(newEmail) && !addingEmail; - } - - _handleAddEmailButton() { - if (!this._isNewEmailValid(this._newEmail)) { return; } - - this._addingEmail = true; - this.$.restAPI.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 = ''; - }); - } - - _getFilterDocsLink(docsBaseUrl) { - let base = docsBaseUrl; - if (!base || !ABSOLUTE_URL_PATTERN.test(base)) { - base = GERRIT_DOCS_BASE_URL; + if (this._serverConfig && this._serverConfig.sshd) { + configPromises.push(this.$.sshEditor.loadData()); } - // Remove any trailing slash, since it is in the GERRIT_DOCS_FILTER_PATH. - base = base.replace(TRAILING_SLASH_PATTERN, ''); - - return base + GERRIT_DOCS_FILTER_PATH; - } - - _handleToggleDark() { - if (this._isDark) { - window.localStorage.removeItem('dark-theme'); - } else { - window.localStorage.setItem('dark-theme', 'true'); - } - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: RELOAD_MESSAGE}, - bubbles: true, - composed: true, - })); - this.async(() => { - window.location.reload(); - }, 1); - } - - _showHttpAuth(config) { - if (config && config.auth && - config.auth.git_basic_auth_policy) { - return HTTP_AUTH.includes( - config.auth.git_basic_auth_policy.toUpperCase()); + if (this._serverConfig && + this._serverConfig.receive && + this._serverConfig.receive.enable_signed_push) { + configPromises.push(this.$.gpgEditor.loadData()); } - return false; + configPromises.push( + this.getDocsBaseUrl(config, this.$.restAPI) + .then(baseUrl => { this._docsBaseUrl = baseUrl; })); + + return Promise.all(configPromises); + })); + + if (this.params.emailToken) { + promises.push(this.$.restAPI.confirmEmail(this.params.emailToken).then( + message => { + if (message) { + this.fire('show-alert', {message}); + } + this.$.emailEditor.loadData(); + })); + } else { + promises.push(this.$.emailEditor.loadData()); + } + + this._loadingPromise = Promise.all(promises).then(() => { + this._loading = false; + + // Handle anchor tag for initial load + this._handleLocationChange(); + }); + } + + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'location-change', '_handleLocationChange'); + } + + _handleLocationChange() { + // Handle anchor tag after dom attached + const urlHash = window.location.hash; + if (urlHash) { + // Use shadowRoot for Polymer 2 + const elem = (this.shadowRoot || document).querySelector(urlHash); + if (elem) { + elem.scrollIntoView(); + } } } - customElements.define(GrSettingsView.is, GrSettingsView); -})(); + reloadAccountDetail() { + Promise.all([ + this.$.accountInfo.loadData(), + this.$.emailEditor.loadData(), + ]); + } + + _isLoading() { + return this._loading || this._loading === undefined; + } + + _copyPrefs(to, from) { + for (let i = 0; i < PREFS_SECTION_FIELDS.length; i++) { + this.set([to, PREFS_SECTION_FIELDS[i]], + this[from][PREFS_SECTION_FIELDS[i]]); + } + } + + _cloneMenu(prefs) { + const menu = []; + for (const item of prefs) { + menu.push({ + name: item.name, + url: item.url, + target: item.target, + }); + } + this._localMenu = menu; + } + + _cloneChangeTableColumns() { + let columns = this.getVisibleColumns(this.prefs.change_table); + + if (columns.length === 0) { + columns = this.columnNames; + this._changeTableColumnsNotDisplayed = []; + } else { + this._changeTableColumnsNotDisplayed = this.getComplementColumns( + this.prefs.change_table); + } + this._localChangeTableColumns = columns; + } + + _formatChangeTableColumns(changeTableArray) { + return changeTableArray.map(item => { + return {column: item}; + }); + } + + _handleChangeTableChanged() { + if (this._isLoading()) { return; } + this._changeTableChanged = true; + } + + _handlePrefsChanged(prefs) { + 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); + } + + _handleWorkInProgressByDefault() { + this.set('_localPrefs.work_in_progress_by_default', + this.$.workInProgressByDefault.checked); + } + + _handleInsertSignedOff() { + this.set('_localPrefs.signed_off_by', this.$.insertSignedOff.checked); + } + + _handleMenuChanged() { + if (this._isLoading()) { return; } + this._menuChanged = true; + } + + _handleSaveAccountInfo() { + this.$.accountInfo.save(); + } + + _handleSavePreferences() { + this._copyPrefs('prefs', '_localPrefs'); + + return this.$.restAPI.savePreferences(this.prefs).then(() => { + this._prefsChanged = false; + }); + } + + _handleSaveChangeTable() { + this.set('prefs.change_table', this._localChangeTableColumns); + this.set('prefs.legacycid_in_change_table', this._showNumber); + this._cloneChangeTableColumns(); + return this.$.restAPI.savePreferences(this.prefs).then(() => { + this._changeTableChanged = false; + }); + } + + _handleSaveDiffPreferences() { + this.$.diffPrefs.save(); + } + + _handleSaveEditPreferences() { + this.$.editPrefs.save(); + } + + _handleSaveMenu() { + this.set('prefs.my', this._localMenu); + this._cloneMenu(this.prefs.my); + return this.$.restAPI.savePreferences(this.prefs).then(() => { + this._menuChanged = false; + }); + } + + _handleResetMenuButton() { + return this.$.restAPI.getDefaultPreferences().then(data => { + if (data && data.my) { + this._cloneMenu(data.my); + } + }); + } + + _handleSaveWatchedProjects() { + this.$.watchedProjectsEditor.save(); + } + + _computeHeaderClass(changed) { + return changed ? 'edited' : ''; + } + + _handleSaveEmails() { + this.$.emailEditor.save(); + } + + _handleNewEmailKeydown(e) { + if (e.keyCode === 13) { // Enter + e.stopPropagation(); + this._handleAddEmailButton(); + } + } + + _isNewEmailValid(newEmail) { + return newEmail && newEmail.includes('@'); + } + + _computeAddEmailButtonEnabled(newEmail, addingEmail) { + return this._isNewEmailValid(newEmail) && !addingEmail; + } + + _handleAddEmailButton() { + if (!this._isNewEmailValid(this._newEmail)) { return; } + + this._addingEmail = true; + this.$.restAPI.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 = ''; + }); + } + + _getFilterDocsLink(docsBaseUrl) { + let base = docsBaseUrl; + if (!base || !ABSOLUTE_URL_PATTERN.test(base)) { + base = GERRIT_DOCS_BASE_URL; + } + + // Remove any trailing slash, since it is in the GERRIT_DOCS_FILTER_PATH. + base = base.replace(TRAILING_SLASH_PATTERN, ''); + + return base + GERRIT_DOCS_FILTER_PATH; + } + + _handleToggleDark() { + if (this._isDark) { + window.localStorage.removeItem('dark-theme'); + } else { + window.localStorage.setItem('dark-theme', 'true'); + } + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message: RELOAD_MESSAGE}, + bubbles: true, + composed: true, + })); + this.async(() => { + window.location.reload(); + }, 1); + } + + _showHttpAuth(config) { + if (config && config.auth && + config.auth.git_basic_auth_policy) { + return HTTP_AUTH.includes( + config.auth.git_basic_auth_policy.toUpperCase()); + } + + return false; + } +} + +customElements.define(GrSettingsView.is, GrSettingsView);
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js index 475a8e2..0f03ec1 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_html.js
@@ -1,53 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> - -<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html"> -<link rel="import" href="/bower_components/paper-toggle-button/paper-toggle-button.html"> - -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/gr-menu-page-styles.html"> -<link rel="import" href="../../../styles/gr-page-nav-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../settings/gr-change-table-editor/gr-change-table-editor.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-diff-preferences/gr-diff-preferences.html"> -<link rel="import" href="../../shared/gr-page-nav/gr-page-nav.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> -<link rel="import" href="../gr-account-info/gr-account-info.html"> -<link rel="import" href="../gr-agreements-list/gr-agreements-list.html"> -<link rel="import" href="../gr-edit-preferences/gr-edit-preferences.html"> -<link rel="import" href="../gr-email-editor/gr-email-editor.html"> -<link rel="import" href="../gr-gpg-editor/gr-gpg-editor.html"> -<link rel="import" href="../gr-group-list/gr-group-list.html"> -<link rel="import" href="../gr-http-password/gr-http-password.html"> -<link rel="import" href="../gr-identities/gr-identities.html"> -<link rel="import" href="../gr-menu-editor/gr-menu-editor.html"> -<link rel="import" href="../gr-ssh-editor/gr-ssh-editor.html"> -<link rel="import" href="../gr-watched-projects-editor/gr-watched-projects-editor.html"> -<script src="../../../scripts/util.js"></script> - -<dom-module id="gr-settings-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { color: var(--primary-text-color); @@ -84,8 +53,8 @@ <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> + <div class="loading" hidden\$="[[!_loading]]">Loading...</div> + <div hidden\$="[[_loading]]" hidden=""> <gr-page-nav class="navStyles"> <ul> <li><a href="#Profile">Profile</a></li> @@ -99,10 +68,10 @@ <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]"> <li><a href="#HTTPCredentials">HTTP Credentials</a></li> </template> - <li hidden$="[[!_serverConfig.sshd]]"><a href="#SSHKeys"> + <li hidden\$="[[!_serverConfig.sshd]]"><a href="#SSHKeys"> SSH Keys </a></li> - <li hidden$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys"> + <li hidden\$="[[!_serverConfig.receive.enable_signed_push]]"><a href="#GPGKeys"> GPG Keys </a></li> <li><a href="#Groups">Groups</a></li> @@ -121,9 +90,7 @@ <h1>User Settings</h1> <section class="darkToggle"> <div class="toggle"> - <paper-toggle-button - checked="[[_isDark]]" - on-change="_handleToggleDark"></paper-toggle-button> + <paper-toggle-button checked="[[_isDark]]" on-change="_handleToggleDark"></paper-toggle-button> <div>Dark theme (alpha)</div> </div> <p> @@ -132,26 +99,17 @@ feedback via the link in the app footer is strongly encouraged! </p> </section> - <h2 - id="Profile" - class$="[[_computeHeaderClass(_accountInfoChanged)]]">Profile</h2> + <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> + <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> + <h2 id="Preferences" class\$="[[_computeHeaderClass(_prefsChanged)]]">Preferences</h2> <fieldset id="preferences"> <section> <span class="title">Changes per page</span> <span class="value"> - <gr-select - bind-value="{{_localPrefs.changes_per_page}}"> + <gr-select bind-value="{{_localPrefs.changes_per_page}}"> <select> <option value="10">10 rows per page</option> <option value="25">25 rows per page</option> @@ -164,8 +122,7 @@ <section> <span class="title">Date/time format</span> <span class="value"> - <gr-select - bind-value="{{_localPrefs.date_format}}"> + <gr-select bind-value="{{_localPrefs.date_format}}"> <select> <option value="STD">Jun 3 ; Jun 3, 2016</option> <option value="US">06/03 ; 06/03/16</option> @@ -174,8 +131,7 @@ <option value="UK">03/06 ; 03/06/2016</option> </select> </gr-select> - <gr-select - bind-value="{{_localPrefs.time_format}}"> + <gr-select bind-value="{{_localPrefs.time_format}}"> <select> <option value="HHMM_12">4:10 PM</option> <option value="HHMM_24">16:10</option> @@ -186,8 +142,7 @@ <section> <span class="title">Email notifications</span> <span class="value"> - <gr-select - bind-value="{{_localPrefs.email_strategy}}"> + <gr-select bind-value="{{_localPrefs.email_strategy}}"> <select> <option value="CC_ON_OWN_COMMENTS">Every comment</option> <option value="ENABLED">Only comments left by others</option> @@ -196,11 +151,10 @@ </gr-select> </span> </section> - <section hidden$="[[!_localPrefs.email_format]]"> + <section hidden\$="[[!_localPrefs.email_format]]"> <span class="title">Email format</span> <span class="value"> - <gr-select - bind-value="{{_localPrefs.email_format}}"> + <gr-select bind-value="{{_localPrefs.email_format}}"> <select> <option value="HTML_PLAINTEXT">HTML and plaintext</option> <option value="PLAINTEXT">Plaintext only</option> @@ -208,11 +162,10 @@ </gr-select> </span> </section> - <section hidden$="[[!_localPrefs.default_base_for_merges]]"> + <section hidden\$="[[!_localPrefs.default_base_for_merges]]"> <span class="title">Default Base For Merges</span> <span class="value"> - <gr-select - bind-value="{{_localPrefs.default_base_for_merges}}"> + <gr-select bind-value="{{_localPrefs.default_base_for_merges}}"> <select> <option value="AUTO_MERGE">Auto Merge</option> <option value="FIRST_PARENT">First Parent</option> @@ -223,18 +176,13 @@ <section> <span class="title">Show Relative Dates In Changes Table</span> <span class="value"> - <input - id="relativeDateInChangeTable" - type="checkbox" - checked$="[[_localPrefs.relative_date_in_change_table]]" - on-change="_handleRelativeDateInChangeTable"> + <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="{{_localPrefs.diff_view}}"> + <gr-select bind-value="{{_localPrefs.diff_view}}"> <select> <option value="SIDE_BY_SIDE">Side by side</option> <option value="UNIFIED_DIFF">Unified diff</option> @@ -245,31 +193,19 @@ <section> <span class="title">Show size bars in file list</span> <span class="value"> - <input - id="showSizeBarsInFileList" - type="checkbox" - checked$="[[_localPrefs.size_bar_in_change_table]]" - on-change="_handleShowSizeBarsInFileListChanged"> + <input id="showSizeBarsInFileList" type="checkbox" checked\$="[[_localPrefs.size_bar_in_change_table]]" on-change="_handleShowSizeBarsInFileListChanged"> </span> </section> <section> <span class="title">Publish comments on push</span> <span class="value"> - <input - id="publishCommentsOnPush" - type="checkbox" - checked$="[[_localPrefs.publish_comments_on_push]]" - on-change="_handlePublishCommentsOnPushChanged"> + <input id="publishCommentsOnPush" type="checkbox" checked\$="[[_localPrefs.publish_comments_on_push]]" on-change="_handlePublishCommentsOnPushChanged"> </span> </section> <section> <span class="title">Set new changes to "work in progress" by default</span> <span class="value"> - <input - id="workInProgressByDefault" - type="checkbox" - checked$="[[_localPrefs.work_in_progress_by_default]]" - on-change="_handleWorkInProgressByDefault"> + <input id="workInProgressByDefault" type="checkbox" checked\$="[[_localPrefs.work_in_progress_by_default]]" on-change="_handleWorkInProgressByDefault"> </span> </section> <section> @@ -277,132 +213,69 @@ Insert Signed-off-by Footer For Inline Edit Changes </span> <span class="value"> - <input - id="insertSignedOff" - type="checkbox" - checked$="[[_localPrefs.signed_off_by]]" - on-change="_handleInsertSignedOff"> + <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> + <gr-button id="savePrefs" on-click="_handleSavePreferences" disabled="[[!_prefsChanged]]">Save changes</gr-button> </fieldset> - <h2 - id="DiffPreferences" - class$="[[_computeHeaderClass(_diffPrefsChanged)]]"> + <h2 id="DiffPreferences" class\$="[[_computeHeaderClass(_diffPrefsChanged)]]"> Diff Preferences </h2> <fieldset id="diffPreferences"> - <gr-diff-preferences - id="diffPrefs" - has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences> - <gr-button - id="saveDiffPrefs" - on-click="_handleSaveDiffPreferences" - disabled$="[[!_diffPrefsChanged]]">Save changes</gr-button> + <gr-diff-preferences id="diffPrefs" has-unsaved-changes="{{_diffPrefsChanged}}"></gr-diff-preferences> + <gr-button id="saveDiffPrefs" on-click="_handleSaveDiffPreferences" disabled\$="[[!_diffPrefsChanged]]">Save changes</gr-button> </fieldset> - <h2 - id="EditPreferences" - class$="[[_computeHeaderClass(_editPrefsChanged)]]"> + <h2 id="EditPreferences" class\$="[[_computeHeaderClass(_editPrefsChanged)]]"> Edit Preferences </h2> <fieldset id="editPreferences"> - <gr-edit-preferences - id="editPrefs" - has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences> - <gr-button - id="saveEditPrefs" - on-click="_handleSaveEditPreferences" - disabled$="[[!_editPrefsChanged]]">Save changes</gr-button> + <gr-edit-preferences id="editPrefs" has-unsaved-changes="{{_editPrefsChanged}}"></gr-edit-preferences> + <gr-button id="saveEditPrefs" on-click="_handleSaveEditPreferences" disabled\$="[[!_editPrefsChanged]]">Save changes</gr-button> </fieldset> - <h2 id="Menu" class$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2> + <h2 id="Menu" class\$="[[_computeHeaderClass(_menuChanged)]]">Menu</h2> <fieldset id="menu"> - <gr-menu-editor - menu-items="{{_localMenu}}"></gr-menu-editor> - <gr-button - id="saveMenu" - on-click="_handleSaveMenu" - disabled="[[!_menuChanged]]">Save changes</gr-button> - <gr-button - id="resetMenu" - link - on-click="_handleResetMenuButton">Reset</gr-button> + <gr-menu-editor menu-items="{{_localMenu}}"></gr-menu-editor> + <gr-button id="saveMenu" on-click="_handleSaveMenu" disabled="[[!_menuChanged]]">Save changes</gr-button> + <gr-button id="resetMenu" link="" on-click="_handleResetMenuButton">Reset</gr-button> </fieldset> - <h2 id="ChangeTableColumns" - class$="[[_computeHeaderClass(_changeTableChanged)]]"> + <h2 id="ChangeTableColumns" class\$="[[_computeHeaderClass(_changeTableChanged)]]"> Change Table Columns </h2> <fieldset id="changeTableColumns"> - <gr-change-table-editor - show-number="{{_showNumber}}" - displayed-columns="{{_localChangeTableColumns}}"> + <gr-change-table-editor show-number="{{_showNumber}}" displayed-columns="{{_localChangeTableColumns}}"> </gr-change-table-editor> - <gr-button - id="saveChangeTable" - on-click="_handleSaveChangeTable" - disabled="[[!_changeTableChanged]]">Save changes</gr-button> + <gr-button id="saveChangeTable" on-click="_handleSaveChangeTable" disabled="[[!_changeTableChanged]]">Save changes</gr-button> </fieldset> - <h2 - id="Notifications" - class$="[[_computeHeaderClass(_watchedProjectsChanged)]]"> + <h2 id="Notifications" class\$="[[_computeHeaderClass(_watchedProjectsChanged)]]"> Notifications </h2> <fieldset id="watchedProjects"> - <gr-watched-projects-editor - has-unsaved-changes="{{_watchedProjectsChanged}}" - id="watchedProjectsEditor"></gr-watched-projects-editor> - <gr-button - on-click="_handleSaveWatchedProjects" - disabled$="[[!_watchedProjectsChanged]]" - id="_handleSaveWatchedProjects">Save changes</gr-button> + <gr-watched-projects-editor has-unsaved-changes="{{_watchedProjectsChanged}}" 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)]]"> + <h2 id="EmailAddresses" class\$="[[_computeHeaderClass(_emailsChanged)]]"> Email Addresses </h2> <fieldset id="email"> - <gr-email-editor - id="emailEditor" - has-unsaved-changes="{{_emailsChanged}}"></gr-email-editor> - <gr-button - on-click="_handleSaveEmails" - disabled$="[[!_emailsChanged]]">Save changes</gr-button> + <gr-email-editor id="emailEditor" has-unsaved-changes="{{_emailsChanged}}"></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" - bind-value="{{_newEmail}}" - is="iron-input" - type="text" - disabled="[[_addingEmail]]" - on-keydown="_handleNewEmailKeydown" - placeholder="email@example.com"> + <iron-input class="newEmailInput" bind-value="{{_newEmail}}" type="text" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com"> + <input class="newEmailInput" bind-value="{{_newEmail}}" is="iron-input" type="text" disabled="[[_addingEmail]]" on-keydown="_handleNewEmailKeydown" placeholder="email@example.com"> </iron-input> </span> </section> - <section - id="verificationSentMessage" - hidden$="[[!_lastSentVerificationEmail]]"> + <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> + <gr-button disabled="[[!_computeAddEmailButtonEnabled(_newEmail, _addingEmail)]]" on-click="_handleAddEmailButton">Send verification</gr-button> </fieldset> <template is="dom-if" if="[[_showHttpAuth(_serverConfig)]]"> <div> @@ -412,21 +285,13 @@ </fieldset> </div> </template> - <div hidden$="[[!_serverConfig.sshd]]"> - <h2 - id="SSHKeys" - class$="[[_computeHeaderClass(_keysChanged)]]">SSH keys</h2> - <gr-ssh-editor - id="sshEditor" - has-unsaved-changes="{{_keysChanged}}"></gr-ssh-editor> + <div hidden\$="[[!_serverConfig.sshd]]"> + <h2 id="SSHKeys" class\$="[[_computeHeaderClass(_keysChanged)]]">SSH keys</h2> + <gr-ssh-editor id="sshEditor" has-unsaved-changes="{{_keysChanged}}"></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}}"></gr-gpg-editor> + <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}}"></gr-gpg-editor> </div> <h2 id="Groups">Groups</h2> <fieldset> @@ -451,9 +316,7 @@ <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> + <a href\$="[[_getFilterDocsLink(_docsBaseUrl)]]" target="_blank" rel="nofollow">Gerrit documentation</a> for the complete set of footers. </p> <table> @@ -517,6 +380,4 @@ </main> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-settings-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html index cd268c6..a014c96 100644 --- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html +++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-settings-view.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-settings-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-settings-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -41,488 +46,491 @@ </template> </test-fixture> -<script> - suite('gr-settings-view tests', async () => { - await readyToTest(); - let element; - let account; - let preferences; - let config; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-settings-view.js'; +import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-settings-view tests', () => { + let element; + let account; + let preferences; + let config; + let sandbox; - function valueOf(title, fieldsetid) { - const sections = element.$[fieldsetid].querySelectorAll('section'); - let titleEl; - for (let i = 0; i < sections.length; i++) { - titleEl = sections[i].querySelector('.title'); - if (titleEl.textContent.trim() === title) { - return sections[i].querySelector('.value'); - } + function valueOf(title, fieldsetid) { + const sections = element.$[fieldsetid].querySelectorAll('section'); + let titleEl; + for (let i = 0; i < sections.length; i++) { + titleEl = sections[i].querySelector('.title'); + if (titleEl.textContent.trim() === title) { + return sections[i].querySelector('.value'); } } + } - // Because deepEqual isn't behaving in Safari. - function assertMenusEqual(actual, expected) { - assert.equal(actual.length, expected.length); - for (let i = 0; i < actual.length; i++) { - assert.equal(actual[i].name, expected[i].name); - assert.equal(actual[i].url, expected[i].url); - } + // Because deepEqual isn't behaving in Safari. + function assertMenusEqual(actual, expected) { + assert.equal(actual.length, expected.length); + for (let i = 0; i < actual.length; i++) { + assert.equal(actual[i].name, expected[i].name); + assert.equal(actual[i].url, expected[i].url); } + } - function stubAddAccountEmail(statusCode) { - return sandbox.stub(element.$.restAPI, 'addAccountEmail', - () => Promise.resolve({status: statusCode})); - } + function stubAddAccountEmail(statusCode) { + return sandbox.stub(element.$.restAPI, 'addAccountEmail', + () => Promise.resolve({status: statusCode})); + } - setup(done => { - sandbox = sinon.sandbox.create(); - account = { - _account_id: 123, - name: 'user name', - email: 'user@email', - username: 'user username', - registered: '2000-01-01 00:00:00.000000000', - }; - preferences = { - changes_per_page: 25, - date_format: 'UK', - time_format: 'HHMM_12', - diff_view: 'UNIFIED_DIFF', - email_strategy: 'ENABLED', - email_format: 'HTML_PLAINTEXT', - default_base_for_merges: 'FIRST_PARENT', - relative_date_in_change_table: false, - size_bar_in_change_table: true, + setup(done => { + sandbox = sinon.sandbox.create(); + account = { + _account_id: 123, + name: 'user name', + email: 'user@email', + username: 'user username', + registered: '2000-01-01 00:00:00.000000000', + }; + preferences = { + changes_per_page: 25, + date_format: 'UK', + time_format: 'HHMM_12', + diff_view: 'UNIFIED_DIFF', + email_strategy: 'ENABLED', + email_format: 'HTML_PLAINTEXT', + default_base_for_merges: 'FIRST_PARENT', + relative_date_in_change_table: false, + size_bar_in_change_table: true, - my: [ - {url: '/first/url', name: 'first name', target: '_blank'}, - {url: '/second/url', name: 'second name', target: '_blank'}, - ], - change_table: [], - }; - config = {auth: {editable_account_fields: []}}; + my: [ + {url: '/first/url', name: 'first name', target: '_blank'}, + {url: '/second/url', name: 'second name', target: '_blank'}, + ], + change_table: [], + }; + config = {auth: {editable_account_fields: []}}; - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(true); }, - getAccount() { return Promise.resolve(account); }, - getPreferences() { return Promise.resolve(preferences); }, - getWatchedProjects() { - return Promise.resolve([]); - }, - getAccountEmails() { return Promise.resolve(); }, - getConfig() { return Promise.resolve(config); }, - getAccountGroups() { return Promise.resolve([]); }, - }); - element = fixture('basic'); - - // Allow the element to render. - element._loadingPromise.then(done); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(true); }, + getAccount() { return Promise.resolve(account); }, + getPreferences() { return Promise.resolve(preferences); }, + getWatchedProjects() { + return Promise.resolve([]); + }, + getAccountEmails() { return Promise.resolve(); }, + getConfig() { return Promise.resolve(config); }, + getAccountGroups() { return Promise.resolve([]); }, }); + element = fixture('basic'); - teardown(() => { - sandbox.restore(); - }); + // Allow the element to render. + element._loadingPromise.then(done); + }); - test('calls the title-change event', () => { - const titleChangedStub = sandbox.stub(); + teardown(() => { + sandbox.restore(); + }); - // Create a new view. - const newElement = document.createElement('gr-settings-view'); - newElement.addEventListener('title-change', titleChangedStub); + test('calls the title-change event', () => { + const titleChangedStub = sandbox.stub(); - // Attach it to the fixture. - const blank = fixture('blank'); - blank.appendChild(newElement); + // Create a new view. + const newElement = document.createElement('gr-settings-view'); + newElement.addEventListener('title-change', titleChangedStub); - Polymer.dom.flush(); + // Attach it to the fixture. + const blank = fixture('blank'); + blank.appendChild(newElement); - assert.isTrue(titleChangedStub.called); - assert.equal(titleChangedStub.getCall(0).args[0].detail.title, - 'Settings'); - }); + flush(); - test('user preferences', done => { - // Rendered with the expected preferences selected. - assert.equal(valueOf('Changes per page', 'preferences') - .firstElementChild.bindValue, preferences.changes_per_page); - assert.equal(valueOf('Date/time format', 'preferences') - .firstElementChild.bindValue, preferences.date_format); - assert.equal(valueOf('Date/time format', 'preferences') - .lastElementChild.bindValue, preferences.time_format); - assert.equal(valueOf('Email notifications', 'preferences') - .firstElementChild.bindValue, preferences.email_strategy); - assert.equal(valueOf('Email format', 'preferences') - .firstElementChild.bindValue, preferences.email_format); - assert.equal(valueOf('Default Base For Merges', 'preferences') - .firstElementChild.bindValue, preferences.default_base_for_merges); - assert.equal( - valueOf('Show Relative Dates In Changes Table', 'preferences') - .firstElementChild.checked, false); - assert.equal(valueOf('Diff view', 'preferences') - .firstElementChild.bindValue, preferences.diff_view); - assert.equal(valueOf('Show size bars in file list', 'preferences') - .firstElementChild.checked, true); - assert.equal(valueOf('Publish comments on push', 'preferences') - .firstElementChild.checked, false); - assert.equal(valueOf( - 'Set new changes to "work in progress" by default', 'preferences') - .firstElementChild.checked, false); - assert.equal(valueOf( - 'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences') - .firstElementChild.checked, false); + assert.isTrue(titleChangedStub.called); + assert.equal(titleChangedStub.getCall(0).args[0].detail.title, + 'Settings'); + }); - assert.isFalse(element._prefsChanged); - assert.isFalse(element._menuChanged); + test('user preferences', done => { + // Rendered with the expected preferences selected. + assert.equal(valueOf('Changes per page', 'preferences') + .firstElementChild.bindValue, preferences.changes_per_page); + assert.equal(valueOf('Date/time format', 'preferences') + .firstElementChild.bindValue, preferences.date_format); + assert.equal(valueOf('Date/time format', 'preferences') + .lastElementChild.bindValue, preferences.time_format); + assert.equal(valueOf('Email notifications', 'preferences') + .firstElementChild.bindValue, preferences.email_strategy); + assert.equal(valueOf('Email format', 'preferences') + .firstElementChild.bindValue, preferences.email_format); + assert.equal(valueOf('Default Base For Merges', 'preferences') + .firstElementChild.bindValue, preferences.default_base_for_merges); + assert.equal( + valueOf('Show Relative Dates In Changes Table', 'preferences') + .firstElementChild.checked, false); + assert.equal(valueOf('Diff view', 'preferences') + .firstElementChild.bindValue, preferences.diff_view); + assert.equal(valueOf('Show size bars in file list', 'preferences') + .firstElementChild.checked, true); + assert.equal(valueOf('Publish comments on push', 'preferences') + .firstElementChild.checked, false); + assert.equal(valueOf( + 'Set new changes to "work in progress" by default', 'preferences') + .firstElementChild.checked, false); + assert.equal(valueOf( + 'Insert Signed-off-by Footer For Inline Edit Changes', 'preferences') + .firstElementChild.checked, false); - // Change the diff view element. - const diffSelect = valueOf('Diff view', 'preferences').firstElementChild; - diffSelect.bindValue = 'SIDE_BY_SIDE'; + assert.isFalse(element._prefsChanged); + assert.isFalse(element._menuChanged); - const publishOnPush = - valueOf('Publish comments on push', 'preferences').firstElementChild; - diffSelect.fire('change'); + // Change the diff view element. + const diffSelect = valueOf('Diff view', 'preferences').firstElementChild; + diffSelect.bindValue = 'SIDE_BY_SIDE'; - MockInteractions.tap(publishOnPush); - - assert.isTrue(element._prefsChanged); - assert.isFalse(element._menuChanged); - - stub('gr-rest-api-interface', { - savePreferences(prefs) { - assert.equal(prefs.diff_view, 'SIDE_BY_SIDE'); - assertMenusEqual(prefs.my, preferences.my); - assert.equal(prefs.publish_comments_on_push, true); - return Promise.resolve(); - }, - }); - - // Save the change. - element._handleSavePreferences().then(() => { - assert.isFalse(element._prefsChanged); - assert.isFalse(element._menuChanged); - done(); - }); - }); - - test('publish comments on push', done => { - const publishCommentsOnPush = + const publishOnPush = valueOf('Publish comments on push', 'preferences').firstElementChild; - MockInteractions.tap(publishCommentsOnPush); + diffSelect.fire('change'); - assert.isFalse(element._menuChanged); - assert.isTrue(element._prefsChanged); + MockInteractions.tap(publishOnPush); - stub('gr-rest-api-interface', { - savePreferences(prefs) { - assert.equal(prefs.publish_comments_on_push, true); - return Promise.resolve(); - }, - }); + assert.isTrue(element._prefsChanged); + assert.isFalse(element._menuChanged); - // Save the change. - element._handleSavePreferences().then(() => { - assert.isFalse(element._prefsChanged); - assert.isFalse(element._menuChanged); - done(); - }); + stub('gr-rest-api-interface', { + savePreferences(prefs) { + assert.equal(prefs.diff_view, 'SIDE_BY_SIDE'); + assertMenusEqual(prefs.my, preferences.my); + assert.equal(prefs.publish_comments_on_push, true); + return Promise.resolve(); + }, }); - test('set new changes work-in-progress', done => { - const newChangesWorkInProgress = - valueOf('Set new changes to "work in progress" by default', - 'preferences').firstElementChild; - MockInteractions.tap(newChangesWorkInProgress); - + // Save the change. + element._handleSavePreferences().then(() => { + assert.isFalse(element._prefsChanged); assert.isFalse(element._menuChanged); - assert.isTrue(element._prefsChanged); + done(); + }); + }); - stub('gr-rest-api-interface', { - savePreferences(prefs) { - assert.equal(prefs.work_in_progress_by_default, true); - return Promise.resolve(); - }, - }); + test('publish comments on push', done => { + const publishCommentsOnPush = + valueOf('Publish comments on push', 'preferences').firstElementChild; + MockInteractions.tap(publishCommentsOnPush); - // Save the change. - element._handleSavePreferences().then(() => { - assert.isFalse(element._prefsChanged); - assert.isFalse(element._menuChanged); - done(); - }); + assert.isFalse(element._menuChanged); + assert.isTrue(element._prefsChanged); + + stub('gr-rest-api-interface', { + savePreferences(prefs) { + assert.equal(prefs.publish_comments_on_push, true); + return Promise.resolve(); + }, }); - test('menu', done => { + // Save the change. + element._handleSavePreferences().then(() => { + assert.isFalse(element._prefsChanged); + assert.isFalse(element._menuChanged); + done(); + }); + }); + + test('set new changes work-in-progress', done => { + const newChangesWorkInProgress = + valueOf('Set new changes to "work in progress" by default', + 'preferences').firstElementChild; + MockInteractions.tap(newChangesWorkInProgress); + + assert.isFalse(element._menuChanged); + assert.isTrue(element._prefsChanged); + + stub('gr-rest-api-interface', { + savePreferences(prefs) { + assert.equal(prefs.work_in_progress_by_default, true); + return Promise.resolve(); + }, + }); + + // Save the change. + element._handleSavePreferences().then(() => { + assert.isFalse(element._prefsChanged); + assert.isFalse(element._menuChanged); + done(); + }); + }); + + test('menu', done => { + assert.isFalse(element._menuChanged); + assert.isFalse(element._prefsChanged); + + assertMenusEqual(element._localMenu, preferences.my); + + const menu = element.$.menu.firstElementChild; + let tableRows = dom(menu.root).querySelectorAll('tbody tr'); + assert.equal(tableRows.length, preferences.my.length); + + // Add a menu item: + element.splice('_localMenu', 1, 0, {name: 'foo', url: 'bar', target: ''}); + flush(); + + tableRows = dom(menu.root).querySelectorAll('tbody tr'); + assert.equal(tableRows.length, preferences.my.length + 1); + + assert.isTrue(element._menuChanged); + assert.isFalse(element._prefsChanged); + + stub('gr-rest-api-interface', { + savePreferences(prefs) { + assertMenusEqual(prefs.my, element._localMenu); + return Promise.resolve(); + }, + }); + + element._handleSaveMenu().then(() => { assert.isFalse(element._menuChanged); assert.isFalse(element._prefsChanged); - - assertMenusEqual(element._localMenu, preferences.my); - - const menu = element.$.menu.firstElementChild; - let tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr'); - assert.equal(tableRows.length, preferences.my.length); - - // Add a menu item: - element.splice('_localMenu', 1, 0, {name: 'foo', url: 'bar', target: ''}); - Polymer.dom.flush(); - - tableRows = Polymer.dom(menu.root).querySelectorAll('tbody tr'); - assert.equal(tableRows.length, preferences.my.length + 1); - - assert.isTrue(element._menuChanged); - assert.isFalse(element._prefsChanged); - - stub('gr-rest-api-interface', { - savePreferences(prefs) { - assertMenusEqual(prefs.my, element._localMenu); - return Promise.resolve(); - }, - }); - - element._handleSaveMenu().then(() => { - assert.isFalse(element._menuChanged); - assert.isFalse(element._prefsChanged); - assertMenusEqual(element.prefs.my, element._localMenu); - done(); - }); + assertMenusEqual(element.prefs.my, element._localMenu); + done(); }); + }); - test('add email validation', () => { - assert.isFalse(element._isNewEmailValid('invalid email')); - assert.isTrue(element._isNewEmailValid('vaguely@valid.email')); + test('add email validation', () => { + 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)); + assert.isFalse( + element._computeAddEmailButtonEnabled('invalid email'), true); + assert.isFalse( + element._computeAddEmailButtonEnabled('vaguely@valid.email', true)); + assert.isTrue( + element._computeAddEmailButtonEnabled('vaguely@valid.email', false)); + }); + + test('add email does not save invalid', () => { + const addEmailStub = stubAddAccountEmail(201); + + assert.isFalse(element._addingEmail); + assert.isNotOk(element._lastSentVerificationEmail); + element._newEmail = 'invalid email'; + + element._handleAddEmailButton(); + + assert.isFalse(element._addingEmail); + assert.isFalse(addEmailStub.called); + assert.isNotOk(element._lastSentVerificationEmail); + + assert.isFalse(addEmailStub.called); + }); + + test('add email does save valid', done => { + const addEmailStub = stubAddAccountEmail(201); + + assert.isFalse(element._addingEmail); + assert.isNotOk(element._lastSentVerificationEmail); + element._newEmail = 'valid@email.com'; + + element._handleAddEmailButton(); + + assert.isTrue(element._addingEmail); + assert.isTrue(addEmailStub.called); + + assert.isTrue(addEmailStub.called); + addEmailStub.lastCall.returnValue.then(() => { + assert.isOk(element._lastSentVerificationEmail); + done(); }); + }); - test('add email does not save invalid', () => { - const addEmailStub = stubAddAccountEmail(201); + test('add email does not set last-email if error', done => { + const addEmailStub = stubAddAccountEmail(500); - assert.isFalse(element._addingEmail); + assert.isNotOk(element._lastSentVerificationEmail); + element._newEmail = 'valid@email.com'; + + element._handleAddEmailButton(); + + assert.isTrue(addEmailStub.called); + addEmailStub.lastCall.returnValue.then(() => { assert.isNotOk(element._lastSentVerificationEmail); - element._newEmail = 'invalid email'; - - element._handleAddEmailButton(); - - assert.isFalse(element._addingEmail); - assert.isFalse(addEmailStub.called); - assert.isNotOk(element._lastSentVerificationEmail); - - assert.isFalse(addEmailStub.called); + done(); }); + }); - test('add email does save valid', done => { - const addEmailStub = stubAddAccountEmail(201); + test('emails are loaded without emailToken', () => { + sandbox.stub(element.$.emailEditor, 'loadData'); + element.params = {}; + element.attached(); + assert.isTrue(element.$.emailEditor.loadData.calledOnce); + }); - assert.isFalse(element._addingEmail); - assert.isNotOk(element._lastSentVerificationEmail); - element._newEmail = 'valid@email.com'; + test('_handleSaveChangeTable', () => { + let newColumns = ['Owner', 'Project', 'Branch']; + element._localChangeTableColumns = newColumns.slice(0); + element._showNumber = false; + const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns'); + element._handleSaveChangeTable(); + assert.isTrue(cloneStub.calledOnce); + assert.deepEqual(element.prefs.change_table, newColumns); + assert.isNotOk(element.prefs.legacycid_in_change_table); - element._handleAddEmailButton(); + newColumns = ['Size']; + element._localChangeTableColumns = newColumns; + element._showNumber = true; + element._handleSaveChangeTable(); + assert.isTrue(cloneStub.calledTwice); + assert.deepEqual(element.prefs.change_table, newColumns); + assert.isTrue(element.prefs.legacycid_in_change_table); + }); - assert.isTrue(element._addingEmail); - assert.isTrue(addEmailStub.called); - - assert.isTrue(addEmailStub.called); - addEmailStub.lastCall.returnValue.then(() => { - assert.isOk(element._lastSentVerificationEmail); - done(); - }); - }); - - test('add email does not set last-email if error', done => { - const addEmailStub = stubAddAccountEmail(500); - - assert.isNotOk(element._lastSentVerificationEmail); - element._newEmail = 'valid@email.com'; - - element._handleAddEmailButton(); - - assert.isTrue(addEmailStub.called); - addEmailStub.lastCall.returnValue.then(() => { - assert.isNotOk(element._lastSentVerificationEmail); - done(); - }); - }); - - test('emails are loaded without emailToken', () => { - sandbox.stub(element.$.emailEditor, 'loadData'); - element.params = {}; - element.attached(); - assert.isTrue(element.$.emailEditor.loadData.calledOnce); - }); - - test('_handleSaveChangeTable', () => { - let newColumns = ['Owner', 'Project', 'Branch']; - element._localChangeTableColumns = newColumns.slice(0); - element._showNumber = false; - const cloneStub = sandbox.stub(element, '_cloneChangeTableColumns'); - element._handleSaveChangeTable(); - assert.isTrue(cloneStub.calledOnce); - 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(); - assert.isTrue(cloneStub.calledTwice); - assert.deepEqual(element.prefs.change_table, newColumns); - assert.isTrue(element.prefs.legacycid_in_change_table); - }); - - test('reset menu item back to default', done => { - const originalMenu = { - my: [ - {url: '/first/url', name: 'first name', target: '_blank'}, - {url: '/second/url', name: 'second name', target: '_blank'}, - {url: '/third/url', name: 'third name', target: '_blank'}, - ], - }; - - stub('gr-rest-api-interface', { - getDefaultPreferences() { return Promise.resolve(originalMenu); }, - }); - - const updatedMenu = [ + test('reset menu item back to default', done => { + const originalMenu = { + my: [ {url: '/first/url', name: 'first name', target: '_blank'}, {url: '/second/url', name: 'second name', target: '_blank'}, {url: '/third/url', name: 'third name', target: '_blank'}, - {url: '/fourth/url', name: 'fourth name', target: '_blank'}, - ]; + ], + }; - element.set('_localMenu', updatedMenu); - - element._handleResetMenuButton().then(() => { - assertMenusEqual(element._localMenu, originalMenu.my); - done(); - }); + stub('gr-rest-api-interface', { + getDefaultPreferences() { return Promise.resolve(originalMenu); }, }); - test('test that reset button is called', () => { - const overlayOpen = sandbox.stub(element, '_handleResetMenuButton'); + const updatedMenu = [ + {url: '/first/url', name: 'first name', target: '_blank'}, + {url: '/second/url', name: 'second name', target: '_blank'}, + {url: '/third/url', name: 'third name', target: '_blank'}, + {url: '/fourth/url', name: 'fourth name', target: '_blank'}, + ]; - MockInteractions.tap(element.$.resetMenu); + element.set('_localMenu', updatedMenu); - assert.isTrue(overlayOpen.called); - }); - - test('_showHttpAuth', () => { - let serverConfig; - - serverConfig = { - auth: { - git_basic_auth_policy: 'HTTP', - }, - }; - - assert.isTrue(element._showHttpAuth(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'HTTP_LDAP', - }, - }; - - assert.isTrue(element._showHttpAuth(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'LDAP', - }, - }; - - assert.isFalse(element._showHttpAuth(serverConfig)); - - serverConfig = { - auth: { - git_basic_auth_policy: 'OAUTH', - }, - }; - - assert.isFalse(element._showHttpAuth(serverConfig)); - - serverConfig = {}; - - assert.isFalse(element._showHttpAuth(serverConfig)); - }); - - suite('_getFilterDocsLink', () => { - test('with http: docs base URL', () => { - const base = 'http://example.com/'; - 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); - 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); - assert.equal(result, 'https://example.com/user-notify.html'); - }); - - test('without docs base URL', () => { - const result = element._getFilterDocsLink(null); - assert.equal(result, 'https://gerrit-review.googlesource.com/' + - 'Documentation/user-notify.html'); - }); - - test('ignores non HTTP links', () => { - const base = 'javascript://alert("evil");'; - const result = element._getFilterDocsLink(base); - assert.equal(result, 'https://gerrit-review.googlesource.com/' + - 'Documentation/user-notify.html'); - }); - }); - - suite('when email verification token is provided', () => { - let resolveConfirm; - - setup(() => { - sandbox.stub(element.$.emailEditor, 'loadData'); - sandbox.stub( - element.$.restAPI, - 'confirmEmail', - () => new Promise(resolve => { resolveConfirm = resolve; })); - element.params = {emailToken: 'foo'}; - element.attached(); - }); - - test('it is used to confirm email via rest API', () => { - assert.isTrue(element.$.restAPI.confirmEmail.calledOnce); - assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo')); - }); - - test('emails are not loaded initially', () => { - assert.isFalse(element.$.emailEditor.loadData.called); - }); - - test('user emails are loaded after email confirmed', done => { - element._loadingPromise.then(() => { - assert.isTrue(element.$.emailEditor.loadData.calledOnce); - done(); - }); - resolveConfirm(); - }); - - test('show-alert is fired when email is confirmed', done => { - sandbox.spy(element, 'fire'); - element._loadingPromise.then(() => { - assert.isTrue( - element.fire.calledWith('show-alert', {message: 'bar'})); - done(); - }); - resolveConfirm('bar'); - }); + element._handleResetMenuButton().then(() => { + assertMenusEqual(element._localMenu, originalMenu.my); + done(); }); }); + + test('test that reset button is called', () => { + const overlayOpen = sandbox.stub(element, '_handleResetMenuButton'); + + MockInteractions.tap(element.$.resetMenu); + + assert.isTrue(overlayOpen.called); + }); + + test('_showHttpAuth', () => { + let serverConfig; + + serverConfig = { + auth: { + git_basic_auth_policy: 'HTTP', + }, + }; + + assert.isTrue(element._showHttpAuth(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'HTTP_LDAP', + }, + }; + + assert.isTrue(element._showHttpAuth(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'LDAP', + }, + }; + + assert.isFalse(element._showHttpAuth(serverConfig)); + + serverConfig = { + auth: { + git_basic_auth_policy: 'OAUTH', + }, + }; + + assert.isFalse(element._showHttpAuth(serverConfig)); + + serverConfig = {}; + + assert.isFalse(element._showHttpAuth(serverConfig)); + }); + + suite('_getFilterDocsLink', () => { + test('with http: docs base URL', () => { + const base = 'http://example.com/'; + 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); + 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); + assert.equal(result, 'https://example.com/user-notify.html'); + }); + + test('without docs base URL', () => { + const result = element._getFilterDocsLink(null); + assert.equal(result, 'https://gerrit-review.googlesource.com/' + + 'Documentation/user-notify.html'); + }); + + test('ignores non HTTP links', () => { + const base = 'javascript://alert("evil");'; + const result = element._getFilterDocsLink(base); + assert.equal(result, 'https://gerrit-review.googlesource.com/' + + 'Documentation/user-notify.html'); + }); + }); + + suite('when email verification token is provided', () => { + let resolveConfirm; + + setup(() => { + sandbox.stub(element.$.emailEditor, 'loadData'); + sandbox.stub( + element.$.restAPI, + 'confirmEmail', + () => new Promise(resolve => { resolveConfirm = resolve; })); + element.params = {emailToken: 'foo'}; + element.attached(); + }); + + test('it is used to confirm email via rest API', () => { + assert.isTrue(element.$.restAPI.confirmEmail.calledOnce); + assert.isTrue(element.$.restAPI.confirmEmail.calledWith('foo')); + }); + + test('emails are not loaded initially', () => { + assert.isFalse(element.$.emailEditor.loadData.called); + }); + + test('user emails are loaded after email confirmed', done => { + element._loadingPromise.then(() => { + assert.isTrue(element.$.emailEditor.loadData.calledOnce); + done(); + }); + resolveConfirm(); + }); + + test('show-alert is fired when email is confirmed', done => { + sandbox.spy(element, 'fire'); + element._loadingPromise.then(() => { + assert.isTrue( + element.fire.calledWith('show-alert', {message: 'bar'})); + done(); + }); + resolveConfirm('bar'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js index 44fb48c..814eb7a 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor.js
@@ -14,95 +14,108 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrSshEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-ssh-editor'; } +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../styles/gr-form-styles.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-copy-clipboard/gr-copy-clipboard.js'; +import '../../shared/gr-overlay/gr-overlay.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-ssh-editor_html.js'; - static get properties() { - return { - hasUnsavedChanges: { - type: Boolean, - value: false, - notify: true, - }, - _keys: Array, - /** @type {?} */ - _keyToView: Object, - _newKey: { - type: String, - value: '', - }, - _keysToRemove: { - type: Array, - value() { return []; }, - }, - }; - } +/** @extends Polymer.Element */ +class GrSshEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - loadData() { - return this.$.restAPI.getAccountSSHKeys().then(keys => { - this._keys = keys; - }); - } + static get is() { return 'gr-ssh-editor'; } - save() { - const promises = this._keysToRemove.map(key => { - this.$.restAPI.deleteAccountSSHKey(key.seq); - }); - - return Promise.all(promises).then(() => { - this._keysToRemove = []; - this.hasUnsavedChanges = false; - }); - } - - _getStatusLabel(isValid) { - return isValid ? 'Valid' : 'Invalid'; - } - - _showKey(e) { - const el = Polymer.dom(e).localTarget; - const index = parseInt(el.getAttribute('data-index'), 10); - this._keyToView = this._keys[index]; - this.$.viewKeyOverlay.open(); - } - - _closeOverlay() { - this.$.viewKeyOverlay.close(); - } - - _handleDeleteKey(e) { - const el = Polymer.dom(e).localTarget; - const index = parseInt(el.getAttribute('data-index'), 10); - this.push('_keysToRemove', this._keys[index]); - this.splice('_keys', index, 1); - this.hasUnsavedChanges = true; - } - - _handleAddKey() { - this.$.addButton.disabled = true; - this.$.newKey.disabled = true; - return this.$.restAPI.addAccountSSHKey(this._newKey.trim()) - .then(key => { - this.$.newKey.disabled = false; - this._newKey = ''; - this.push('_keys', key); - }) - .catch(() => { - this.$.addButton.disabled = false; - this.$.newKey.disabled = false; - }); - } - - _computeAddButtonDisabled(newKey) { - return !newKey.length; - } + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + value: false, + notify: true, + }, + _keys: Array, + /** @type {?} */ + _keyToView: Object, + _newKey: { + type: String, + value: '', + }, + _keysToRemove: { + type: Array, + value() { return []; }, + }, + }; } - customElements.define(GrSshEditor.is, GrSshEditor); -})(); + loadData() { + return this.$.restAPI.getAccountSSHKeys().then(keys => { + this._keys = keys; + }); + } + + save() { + const promises = this._keysToRemove.map(key => { + this.$.restAPI.deleteAccountSSHKey(key.seq); + }); + + return Promise.all(promises).then(() => { + this._keysToRemove = []; + this.hasUnsavedChanges = false; + }); + } + + _getStatusLabel(isValid) { + return isValid ? 'Valid' : 'Invalid'; + } + + _showKey(e) { + const el = dom(e).localTarget; + const index = parseInt(el.getAttribute('data-index'), 10); + this._keyToView = this._keys[index]; + this.$.viewKeyOverlay.open(); + } + + _closeOverlay() { + this.$.viewKeyOverlay.close(); + } + + _handleDeleteKey(e) { + const el = dom(e).localTarget; + const index = parseInt(el.getAttribute('data-index'), 10); + this.push('_keysToRemove', this._keys[index]); + this.splice('_keys', index, 1); + this.hasUnsavedChanges = true; + } + + _handleAddKey() { + this.$.addButton.disabled = true; + this.$.newKey.disabled = true; + return this.$.restAPI.addAccountSSHKey(this._newKey.trim()) + .then(key => { + this.$.newKey.disabled = false; + this._newKey = ''; + this.push('_keys', key); + }) + .catch(() => { + this.$.addButton.disabled = false; + this.$.newKey.disabled = false; + }); + } + + _computeAddButtonDisabled(newKey) { + return !newKey.length; + } +} + +customElements.define(GrSshEditor.is, GrSshEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js index dd02ccd..3cefb90e 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-ssh-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -79,31 +70,20 @@ <td class="commentColumn">[[key.comment]]</td> <td>[[_getStatusLabel(key.valid)]]</td> <td> - <gr-button - link - on-click="_showKey" - data-index$="[[index]]" - link>Click to View</gr-button> + <gr-button link="" on-click="_showKey" data-index\$="[[index]]">Click to View</gr-button> </td> <td> - <gr-copy-clipboard - has-tooltip - button-title="Copy SSH public key to clipboard" - hide-input - text="[[key.ssh_public_key]]"> + <gr-copy-clipboard has-tooltip="" button-title="Copy SSH public key to clipboard" hide-input="" text="[[key.ssh_public_key]]"> </gr-copy-clipboard> </td> <td> - <gr-button - link - data-index$="[[index]]" - on-click="_handleDeleteKey">Delete</gr-button> + <gr-button link="" data-index\$="[[index]]" on-click="_handleDeleteKey">Delete</gr-button> </td> </tr> </template> </tbody> </table> - <gr-overlay id="viewKeyOverlay" with-backdrop> + <gr-overlay id="viewKeyOverlay" with-backdrop=""> <fieldset> <section> <span class="title">Algorithm</span> @@ -118,33 +98,19 @@ <span class="value">[[_keyToView.comment]]</span> </section> </fieldset> - <gr-button - class="closeButton" - on-click="_closeOverlay">Close</gr-button> + <gr-button class="closeButton" on-click="_closeOverlay">Close</gr-button> </gr-overlay> - <gr-button - on-click="save" - disabled$="[[!hasUnsavedChanges]]">Save changes</gr-button> + <gr-button on-click="save" disabled\$="[[!hasUnsavedChanges]]">Save changes</gr-button> </fieldset> <fieldset> <section> <span class="title">New SSH key</span> <span class="value"> - <iron-autogrow-textarea - id="newKey" - autocomplete="on" - bind-value="{{_newKey}}" - placeholder="New SSH Key"></iron-autogrow-textarea> + <iron-autogrow-textarea id="newKey" autocomplete="on" bind-value="{{_newKey}}" placeholder="New SSH Key"></iron-autogrow-textarea> </span> </section> - <gr-button - id="addButton" - link - disabled$="[[_computeAddButtonDisabled(_newKey)]]" - on-click="_handleAddKey">Add new SSH key</gr-button> + <gr-button id="addButton" link="" disabled\$="[[_computeAddButtonDisabled(_newKey)]]" on-click="_handleAddKey">Add new SSH key</gr-button> </fieldset> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-ssh-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html index 82d427d..4312d9a 100644 --- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-ssh-editor</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-ssh-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-ssh-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-ssh-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,149 +40,152 @@ </template> </test-fixture> -<script> - suite('gr-ssh-editor tests', async () => { - await readyToTest(); - let element; - let keys; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-ssh-editor.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-ssh-editor tests', () => { + let element; + let keys; - setup(done => { - keys = [{ - seq: 1, - ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one', - encoded_key: '<key 1>', - algorithm: 'ssh-rsa', - comment: 'comment-one@machine-one', - valid: true, - }, { - seq: 2, - ssh_public_key: 'ssh-rsa <key 2> comment-two@machine-two', - encoded_key: '<key 2>', - algorithm: 'ssh-rsa', - comment: 'comment-two@machine-two', - valid: true, - }]; + setup(done => { + keys = [{ + seq: 1, + ssh_public_key: 'ssh-rsa <key 1> comment-one@machine-one', + encoded_key: '<key 1>', + algorithm: 'ssh-rsa', + comment: 'comment-one@machine-one', + valid: true, + }, { + seq: 2, + ssh_public_key: 'ssh-rsa <key 2> comment-two@machine-two', + encoded_key: '<key 2>', + algorithm: 'ssh-rsa', + comment: 'comment-two@machine-two', + valid: true, + }]; - stub('gr-rest-api-interface', { - getAccountSSHKeys() { return Promise.resolve(keys); }, - }); - - element = fixture('basic'); - - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getAccountSSHKeys() { return Promise.resolve(keys); }, }); - test('renders', () => { - const rows = Polymer.dom(element.root).querySelectorAll('tbody tr'); + element = fixture('basic'); - assert.equal(rows.length, 2); + element.loadData().then(() => { flush(done); }); + }); - let cells = rows[0].querySelectorAll('td'); - assert.equal(cells[0].textContent, keys[0].comment); + test('renders', () => { + const rows = dom(element.root).querySelectorAll('tbody tr'); - cells = rows[1].querySelectorAll('td'); - assert.equal(cells[0].textContent, keys[1].comment); - }); + assert.equal(rows.length, 2); - test('remove key', done => { - const lastKey = keys[1]; + let cells = rows[0].querySelectorAll('td'); + assert.equal(cells[0].textContent, keys[0].comment); - const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey', - () => Promise.resolve()); + cells = rows[1].querySelectorAll('td'); + assert.equal(cells[0].textContent, keys[1].comment); + }); + test('remove key', done => { + const lastKey = keys[1]; + + const saveStub = sinon.stub(element.$.restAPI, 'deleteAccountSSHKey', + () => Promise.resolve()); + + assert.equal(element._keysToRemove.length, 0); + assert.isFalse(element.hasUnsavedChanges); + + // Get the delete button for the last row. + const button = dom(element.root).querySelector( + 'tbody tr:last-of-type td:nth-child(5) gr-button'); + + MockInteractions.tap(button); + + assert.equal(element._keys.length, 1); + assert.equal(element._keysToRemove.length, 1); + assert.equal(element._keysToRemove[0], lastKey); + assert.isTrue(element.hasUnsavedChanges); + assert.isFalse(saveStub.called); + + element.save().then(() => { + assert.isTrue(saveStub.called); + assert.equal(saveStub.lastCall.args[0], lastKey.seq); assert.equal(element._keysToRemove.length, 0); assert.isFalse(element.hasUnsavedChanges); - - // Get the delete button for the last row. - const button = Polymer.dom(element.root).querySelector( - 'tbody tr:last-of-type td:nth-child(5) gr-button'); - - MockInteractions.tap(button); - - assert.equal(element._keys.length, 1); - assert.equal(element._keysToRemove.length, 1); - assert.equal(element._keysToRemove[0], lastKey); - assert.isTrue(element.hasUnsavedChanges); - assert.isFalse(saveStub.called); - - element.save().then(() => { - assert.isTrue(saveStub.called); - assert.equal(saveStub.lastCall.args[0], lastKey.seq); - assert.equal(element._keysToRemove.length, 0); - assert.isFalse(element.hasUnsavedChanges); - done(); - }); - }); - - test('show key', () => { - const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open'); - - // Get the show button for the last row. - const button = Polymer.dom(element.root).querySelector( - 'tbody tr:last-of-type td:nth-child(3) gr-button'); - - MockInteractions.tap(button); - - assert.equal(element._keyToView, keys[1]); - assert.isTrue(openSpy.called); - }); - - test('add key', done => { - const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three'; - const newKeyObject = { - seq: 3, - ssh_public_key: newKeyString, - encoded_key: '<key 3>', - algorithm: 'ssh-rsa', - comment: 'comment-three@machine-three', - valid: true, - }; - - const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', - () => Promise.resolve(newKeyObject)); - - element._newKey = newKeyString; - - assert.isFalse(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - - element._handleAddKey().then(() => { - assert.isTrue(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - assert.equal(element._keys.length, 3); - done(); - }); - - assert.isTrue(element.$.addButton.disabled); - assert.isTrue(element.$.newKey.disabled); - - assert.isTrue(addStub.called); - assert.equal(addStub.lastCall.args[0], newKeyString); - }); - - test('add invalid key', done => { - const newKeyString = 'not even close to valid'; - - const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', - () => Promise.reject(new Error('error'))); - - element._newKey = newKeyString; - - assert.isFalse(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - - element._handleAddKey().then(() => { - assert.isFalse(element.$.addButton.disabled); - assert.isFalse(element.$.newKey.disabled); - assert.equal(element._keys.length, 2); - done(); - }); - - assert.isTrue(element.$.addButton.disabled); - assert.isTrue(element.$.newKey.disabled); - - assert.isTrue(addStub.called); - assert.equal(addStub.lastCall.args[0], newKeyString); + done(); }); }); + + test('show key', () => { + const openSpy = sinon.spy(element.$.viewKeyOverlay, 'open'); + + // Get the show button for the last row. + const button = dom(element.root).querySelector( + 'tbody tr:last-of-type td:nth-child(3) gr-button'); + + MockInteractions.tap(button); + + assert.equal(element._keyToView, keys[1]); + assert.isTrue(openSpy.called); + }); + + test('add key', done => { + const newKeyString = 'ssh-rsa <key 3> comment-three@machine-three'; + const newKeyObject = { + seq: 3, + ssh_public_key: newKeyString, + encoded_key: '<key 3>', + algorithm: 'ssh-rsa', + comment: 'comment-three@machine-three', + valid: true, + }; + + const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', + () => Promise.resolve(newKeyObject)); + + element._newKey = newKeyString; + + assert.isFalse(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + + element._handleAddKey().then(() => { + assert.isTrue(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + assert.equal(element._keys.length, 3); + done(); + }); + + assert.isTrue(element.$.addButton.disabled); + assert.isTrue(element.$.newKey.disabled); + + assert.isTrue(addStub.called); + assert.equal(addStub.lastCall.args[0], newKeyString); + }); + + test('add invalid key', done => { + const newKeyString = 'not even close to valid'; + + const addStub = sinon.stub(element.$.restAPI, 'addAccountSSHKey', + () => Promise.reject(new Error('error'))); + + element._newKey = newKeyString; + + assert.isFalse(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + + element._handleAddKey().then(() => { + assert.isFalse(element.$.addButton.disabled); + assert.isFalse(element.$.newKey.disabled); + assert.equal(element._keys.length, 2); + done(); + }); + + assert.isTrue(element.$.addButton.disabled); + assert.isTrue(element.$.newKey.disabled); + + assert.isTrue(addStub.called); + assert.equal(addStub.lastCall.args[0], newKeyString); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js index df115ca..b8960e8 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor.js
@@ -14,169 +14,181 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const NOTIFICATION_TYPES = [ - {name: 'Changes', key: 'notify_new_changes'}, - {name: 'Patches', key: 'notify_new_patch_sets'}, - {name: 'Comments', key: 'notify_all_comments'}, - {name: 'Submits', key: 'notify_submitted_changes'}, - {name: 'Abandons', key: 'notify_abandoned_changes'}, - ]; +import '@polymer/iron-input/iron-input.js'; +import '../../shared/gr-autocomplete/gr-autocomplete.js'; +import '../../shared/gr-button/gr-button.js'; +import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/gr-form-styles.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-watched-projects-editor_html.js'; - /** @extends Polymer.Element */ - class GrWatchedProjectsEditor extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-watched-projects-editor'; } +const NOTIFICATION_TYPES = [ + {name: 'Changes', key: 'notify_new_changes'}, + {name: 'Patches', key: 'notify_new_patch_sets'}, + {name: 'Comments', key: 'notify_all_comments'}, + {name: 'Submits', key: 'notify_submitted_changes'}, + {name: 'Abandons', key: 'notify_abandoned_changes'}, +]; - static get properties() { - return { - hasUnsavedChanges: { - type: Boolean, - value: false, - notify: true, +/** @extends Polymer.Element */ +class GrWatchedProjectsEditor extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-watched-projects-editor'; } + + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + value: false, + notify: true, + }, + + _projects: Array, + _projectsToRemove: { + type: Array, + value() { return []; }, + }, + _query: { + type: Function, + value() { + return this._getProjectSuggestions.bind(this); }, - - _projects: Array, - _projectsToRemove: { - type: Array, - value() { return []; }, - }, - _query: { - type: Function, - value() { - return this._getProjectSuggestions.bind(this); - }, - }, - }; - } - - loadData() { - return this.$.restAPI.getWatchedProjects().then(projs => { - this._projects = projs; - }); - } - - save() { - let deletePromise; - if (this._projectsToRemove.length) { - deletePromise = this.$.restAPI.deleteWatchedProjects( - this._projectsToRemove); - } else { - deletePromise = Promise.resolve(); - } - - return deletePromise - .then(() => this.$.restAPI.saveWatchedProjects(this._projects)) - .then(projects => { - this._projects = projects; - this._projectsToRemove = []; - this.hasUnsavedChanges = false; - }); - } - - _getTypes() { - return NOTIFICATION_TYPES; - } - - _getTypeCount() { - return this._getTypes().length; - } - - _computeCheckboxChecked(project, key) { - return project.hasOwnProperty(key); - } - - _getProjectSuggestions(input) { - return this.$.restAPI.getSuggestedProjects(input) - .then(response => { - const projects = []; - for (const key in response) { - if (!response.hasOwnProperty(key)) { continue; } - projects.push({ - name: key, - value: response[key], - }); - } - return projects; - }); - } - - _handleRemoveProject(e) { - const el = Polymer.dom(e).localTarget; - const index = parseInt(el.getAttribute('data-index'), 10); - const project = this._projects[index]; - this.splice('_projects', index, 1); - this.push('_projectsToRemove', project); - this.hasUnsavedChanges = true; - } - - _canAddProject(project, text, filter) { - if ((!project || !project.id) && !text) { return false; } - - // This will only be used if not using the auto complete - if (!project && text) { return true; } - - // Check if the project with filter is already in the list. Compare - // filters using == to coalesce null and undefined. - for (let i = 0; i < this._projects.length; i++) { - if (this._projects[i].project === project.id && - this._projects[i].filter == filter) { - return false; - } - } - - return true; - } - - _getNewProjectIndex(name, filter) { - let i; - for (i = 0; i < this._projects.length; i++) { - if (this._projects[i].project > name || - (this._projects[i].project === name && - this._projects[i].filter > filter)) { - break; - } - } - return i; - } - - _handleAddProject() { - const newProject = this.$.newProject.value; - const newProjectName = this.$.newProject.text; - const filter = this.$.newFilter.value || null; - - if (!this._canAddProject(newProject, newProjectName, filter)) { return; } - - const insertIndex = this._getNewProjectIndex(newProjectName, filter); - - this.splice('_projects', insertIndex, 0, { - project: newProjectName, - filter, - _is_local: true, - }); - - this.$.newProject.clear(); - this.$.newFilter.bindValue = ''; - this.hasUnsavedChanges = true; - } - - _handleCheckboxChange(e) { - const el = Polymer.dom(e).localTarget; - const index = parseInt(el.getAttribute('data-index'), 10); - const key = el.getAttribute('data-key'); - const checked = el.checked; - this.set(['_projects', index, key], !!checked); - this.hasUnsavedChanges = true; - } - - _handleNotifCellClick(e) { - const checkbox = Polymer.dom(e.target).querySelector('input'); - if (checkbox) { checkbox.click(); } - } + }, + }; } - customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor); -})(); + loadData() { + return this.$.restAPI.getWatchedProjects().then(projs => { + this._projects = projs; + }); + } + + save() { + let deletePromise; + if (this._projectsToRemove.length) { + deletePromise = this.$.restAPI.deleteWatchedProjects( + this._projectsToRemove); + } else { + deletePromise = Promise.resolve(); + } + + return deletePromise + .then(() => this.$.restAPI.saveWatchedProjects(this._projects)) + .then(projects => { + this._projects = projects; + this._projectsToRemove = []; + this.hasUnsavedChanges = false; + }); + } + + _getTypes() { + return NOTIFICATION_TYPES; + } + + _getTypeCount() { + return this._getTypes().length; + } + + _computeCheckboxChecked(project, key) { + return project.hasOwnProperty(key); + } + + _getProjectSuggestions(input) { + return this.$.restAPI.getSuggestedProjects(input) + .then(response => { + const projects = []; + for (const key in response) { + if (!response.hasOwnProperty(key)) { continue; } + projects.push({ + name: key, + value: response[key], + }); + } + return projects; + }); + } + + _handleRemoveProject(e) { + const el = dom(e).localTarget; + const index = parseInt(el.getAttribute('data-index'), 10); + const project = this._projects[index]; + this.splice('_projects', index, 1); + this.push('_projectsToRemove', project); + this.hasUnsavedChanges = true; + } + + _canAddProject(project, text, filter) { + if ((!project || !project.id) && !text) { return false; } + + // This will only be used if not using the auto complete + if (!project && text) { return true; } + + // Check if the project with filter is already in the list. Compare + // filters using == to coalesce null and undefined. + for (let i = 0; i < this._projects.length; i++) { + if (this._projects[i].project === project.id && + this._projects[i].filter == filter) { + return false; + } + } + + return true; + } + + _getNewProjectIndex(name, filter) { + let i; + for (i = 0; i < this._projects.length; i++) { + if (this._projects[i].project > name || + (this._projects[i].project === name && + this._projects[i].filter > filter)) { + break; + } + } + return i; + } + + _handleAddProject() { + const newProject = this.$.newProject.value; + const newProjectName = this.$.newProject.text; + const filter = this.$.newFilter.value || null; + + if (!this._canAddProject(newProject, newProjectName, filter)) { return; } + + const insertIndex = this._getNewProjectIndex(newProjectName, filter); + + this.splice('_projects', insertIndex, 0, { + project: newProjectName, + filter, + _is_local: true, + }); + + this.$.newProject.clear(); + this.$.newFilter.bindValue = ''; + this.hasUnsavedChanges = true; + } + + _handleCheckboxChange(e) { + const el = dom(e).localTarget; + const index = parseInt(el.getAttribute('data-index'), 10); + const key = el.getAttribute('data-key'); + const checked = el.checked; + this.set(['_projects', index, key], !!checked); + this.hasUnsavedChanges = true; + } + + _handleNotifCellClick(e) { + const checkbox = dom(e.target).querySelector('input'); + if (checkbox) { checkbox.click(); } + } +} + +customElements.define(GrWatchedProjectsEditor.is, GrWatchedProjectsEditor);
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js index b1ecb2e..bc381e0 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/gr-form-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-watched-projects-editor"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -60,11 +53,7 @@ </tr> </thead> <tbody> - <template - is="dom-repeat" - items="[[_projects]]" - as="project" - index-as="projectIndex"> + <template is="dom-repeat" items="[[_projects]]" as="project" index-as="projectIndex"> <tr> <td> [[project.project]] @@ -72,24 +61,13 @@ <div class="projectFilter">[[project.filter]]</div> </template> </td> - <template - is="dom-repeat" - items="[[_getTypes()]]" - as="type"> + <template is="dom-repeat" items="[[_getTypes()]]" as="type"> <td class="notifControl" on-click="_handleNotifCellClick"> - <input - type="checkbox" - data-index$="[[projectIndex]]" - data-key$="[[type.key]]" - on-change="_handleCheckboxChange" - checked$="[[_computeCheckboxChecked(project, type.key)]]"> + <input type="checkbox" data-index\$="[[projectIndex]]" data-key\$="[[type.key]]" on-change="_handleCheckboxChange" checked\$="[[_computeCheckboxChecked(project, type.key)]]"> </td> </template> <td> - <gr-button - link - data-index$="[[projectIndex]]" - on-click="_handleRemoveProject">Delete</gr-button> + <gr-button link="" data-index\$="[[projectIndex]]" on-click="_handleRemoveProject">Delete</gr-button> </td> </tr> </template> @@ -97,33 +75,19 @@ <tfoot> <tr> <th> - <gr-autocomplete - id="newProject" - query="[[_query]]" - threshold="1" - allow-non-suggested-values - tab-complete - placeholder="Repo"></gr-autocomplete> + <gr-autocomplete id="newProject" query="[[_query]]" threshold="1" allow-non-suggested-values="" tab-complete="" placeholder="Repo"></gr-autocomplete> </th> - <th colspan$="[[_getTypeCount()]]"> - <iron-input - class="newFilterInput" - placeholder="branch:name, or other search expression"> - <input - id="newFilter" - class="newFilterInput" - is="iron-input" - placeholder="branch:name, or other search expression"> + <th colspan\$="[[_getTypeCount()]]"> + <iron-input class="newFilterInput" placeholder="branch:name, or other search expression"> + <input id="newFilter" class="newFilterInput" is="iron-input" placeholder="branch:name, or other search expression"> </iron-input> </th> <th> - <gr-button link on-click="_handleAddProject">Add</gr-button> + <gr-button link="" on-click="_handleAddProject">Add</gr-button> </th> </tr> </tfoot> </table> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-watched-projects-editor.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html index c96d6a0..a02afcb 100644 --- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html +++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-settings-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-watched-projects-editor.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-watched-projects-editor.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-watched-projects-editor.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,184 +40,186 @@ </template> </test-fixture> -<script> - suite('gr-watched-projects-editor tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-watched-projects-editor.js'; +suite('gr-watched-projects-editor tests', () => { + let element; - setup(done => { - const projects = [ - { - project: 'project a', - notify_submitted_changes: true, - notify_abandoned_changes: true, - }, { - project: 'project b', - filter: 'filter 1', - notify_new_changes: true, - }, { - project: 'project b', - filter: 'filter 2', - }, { - project: 'project c', - notify_new_changes: true, - notify_new_patch_sets: true, - notify_all_comments: true, - }, - ]; + setup(done => { + const projects = [ + { + project: 'project a', + notify_submitted_changes: true, + notify_abandoned_changes: true, + }, { + project: 'project b', + filter: 'filter 1', + notify_new_changes: true, + }, { + project: 'project b', + filter: 'filter 2', + }, { + project: 'project c', + notify_new_changes: true, + notify_new_patch_sets: true, + notify_all_comments: true, + }, + ]; - stub('gr-rest-api-interface', { - getSuggestedProjects(input) { - if (input.startsWith('th')) { - return Promise.resolve({'the project': { - id: 'the project', - state: 'ACTIVE', - web_links: [], - }}); - } else { - return Promise.resolve({}); - } - }, - getWatchedProjects() { - return Promise.resolve(projects); - }, - }); - - element = fixture('basic'); - - element.loadData().then(() => { flush(done); }); + stub('gr-rest-api-interface', { + getSuggestedProjects(input) { + if (input.startsWith('th')) { + return Promise.resolve({'the project': { + id: 'the project', + state: 'ACTIVE', + web_links: [], + }}); + } else { + return Promise.resolve({}); + } + }, + getWatchedProjects() { + return Promise.resolve(projects); + }, }); - test('renders', () => { - const rows = element.shadowRoot - .querySelector('table').querySelectorAll('tbody tr'); - assert.equal(rows.length, 4); + element = fixture('basic'); - function getKeysOfRow(row) { - const boxes = rows[row].querySelectorAll('input[checked]'); - return Array.prototype.map.call(boxes, - e => e.getAttribute('data-key')); - } + element.loadData().then(() => { flush(done); }); + }); - let checkedKeys = getKeysOfRow(0); - assert.equal(checkedKeys.length, 2); - assert.equal(checkedKeys[0], 'notify_submitted_changes'); - assert.equal(checkedKeys[1], 'notify_abandoned_changes'); + test('renders', () => { + const rows = element.shadowRoot + .querySelector('table').querySelectorAll('tbody tr'); + assert.equal(rows.length, 4); - checkedKeys = getKeysOfRow(1); - assert.equal(checkedKeys.length, 1); - assert.equal(checkedKeys[0], 'notify_new_changes'); + function getKeysOfRow(row) { + const boxes = rows[row].querySelectorAll('input[checked]'); + return Array.prototype.map.call(boxes, + e => e.getAttribute('data-key')); + } - checkedKeys = getKeysOfRow(2); - assert.equal(checkedKeys.length, 0); + let checkedKeys = getKeysOfRow(0); + assert.equal(checkedKeys.length, 2); + assert.equal(checkedKeys[0], 'notify_submitted_changes'); + assert.equal(checkedKeys[1], 'notify_abandoned_changes'); - checkedKeys = getKeysOfRow(3); - assert.equal(checkedKeys.length, 3); - assert.equal(checkedKeys[0], 'notify_new_changes'); - assert.equal(checkedKeys[1], 'notify_new_patch_sets'); - assert.equal(checkedKeys[2], 'notify_all_comments'); - }); + checkedKeys = getKeysOfRow(1); + assert.equal(checkedKeys.length, 1); + assert.equal(checkedKeys[0], 'notify_new_changes'); - test('_getProjectSuggestions empty', done => { - element._getProjectSuggestions('nonexistent').then(projects => { - assert.equal(projects.length, 0); - done(); - }); - }); + checkedKeys = getKeysOfRow(2); + assert.equal(checkedKeys.length, 0); - test('_getProjectSuggestions non-empty', done => { - element._getProjectSuggestions('the project').then(projects => { - assert.equal(projects.length, 1); - assert.equal(projects[0].name, 'the project'); - done(); - }); - }); + checkedKeys = getKeysOfRow(3); + assert.equal(checkedKeys.length, 3); + assert.equal(checkedKeys[0], 'notify_new_changes'); + assert.equal(checkedKeys[1], 'notify_new_patch_sets'); + assert.equal(checkedKeys[2], 'notify_all_comments'); + }); - test('_getProjectSuggestions non-empty with two letter project', done => { - element._getProjectSuggestions('th').then(projects => { - assert.equal(projects.length, 1); - assert.equal(projects[0].name, 'the project'); - done(); - }); - }); - - test('_canAddProject', () => { - assert.isFalse(element._canAddProject(null, null, null)); - assert.isFalse(element._canAddProject({}, null, null)); - - // Can add a project that is not in the list. - assert.isTrue(element._canAddProject({id: 'project d'}, null, null)); - assert.isTrue(element._canAddProject({id: 'project d'}, null, 'filter 3')); - - // Cannot add a project that is in the list with no filter. - assert.isFalse(element._canAddProject({id: 'project a'}, null, null)); - - // Can add a project that is in the list if the filter differs. - assert.isTrue(element._canAddProject({id: 'project a'}, null, 'filter 4')); - - // Cannot add a project that is in the list with the same filter. - assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 1')); - assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 2')); - - // Can add a project that is in the list using a new filter. - assert.isTrue(element._canAddProject({id: 'project b'}, null, 'filter 3')); - - // Can add a project that is not added by the auto complete - assert.isTrue(element._canAddProject(null, 'test', null)); - }); - - test('_getNewProjectIndex', () => { - // Projects are sorted in ASCII order. - assert.equal(element._getNewProjectIndex('project A', 'filter'), 0); - assert.equal(element._getNewProjectIndex('project a', 'filter'), 1); - - // Projects are sorted by filter when the names are equal - assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1); - assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2); - assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3); - - // Projects with filters follow those without - assert.equal(element._getNewProjectIndex('project c', 'filter'), 4); - }); - - test('_handleAddProject', () => { - element.$.newProject.value = {id: 'project d'}; - element.$.newProject.setText('project d'); - element.$.newFilter.bindValue = ''; - - element._handleAddProject(); - - assert.equal(element._projects.length, 5); - assert.equal(element._projects[4].project, 'project d'); - assert.isNotOk(element._projects[4].filter); - assert.isTrue(element._projects[4]._is_local); - }); - - test('_handleAddProject with invalid inputs', () => { - element.$.newProject.value = {id: 'project b'}; - element.$.newProject.setText('project b'); - element.$.newFilter.bindValue = 'filter 1'; - element.$.newFilter.value = 'filter 1'; - - element._handleAddProject(); - - assert.equal(element._projects.length, 4); - }); - - test('_handleRemoveProject', () => { - assert.equal(element._projectsToRemove, 0); - const button = element.shadowRoot - .querySelector('table tbody tr:nth-child(2) gr-button'); - MockInteractions.tap(button); - - flushAsynchronousOperations(); - - const rows = element.shadowRoot - .querySelector('table tbody').querySelectorAll('tr'); - assert.equal(rows.length, 3); - - assert.equal(element._projectsToRemove.length, 1); - assert.equal(element._projectsToRemove[0].project, 'project b'); + test('_getProjectSuggestions empty', done => { + element._getProjectSuggestions('nonexistent').then(projects => { + assert.equal(projects.length, 0); + done(); }); }); + + test('_getProjectSuggestions non-empty', done => { + element._getProjectSuggestions('the project').then(projects => { + assert.equal(projects.length, 1); + assert.equal(projects[0].name, 'the project'); + done(); + }); + }); + + test('_getProjectSuggestions non-empty with two letter project', done => { + element._getProjectSuggestions('th').then(projects => { + assert.equal(projects.length, 1); + assert.equal(projects[0].name, 'the project'); + done(); + }); + }); + + test('_canAddProject', () => { + assert.isFalse(element._canAddProject(null, null, null)); + assert.isFalse(element._canAddProject({}, null, null)); + + // Can add a project that is not in the list. + assert.isTrue(element._canAddProject({id: 'project d'}, null, null)); + assert.isTrue(element._canAddProject({id: 'project d'}, null, 'filter 3')); + + // Cannot add a project that is in the list with no filter. + assert.isFalse(element._canAddProject({id: 'project a'}, null, null)); + + // Can add a project that is in the list if the filter differs. + assert.isTrue(element._canAddProject({id: 'project a'}, null, 'filter 4')); + + // Cannot add a project that is in the list with the same filter. + assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 1')); + assert.isFalse(element._canAddProject({id: 'project b'}, null, 'filter 2')); + + // Can add a project that is in the list using a new filter. + assert.isTrue(element._canAddProject({id: 'project b'}, null, 'filter 3')); + + // Can add a project that is not added by the auto complete + assert.isTrue(element._canAddProject(null, 'test', null)); + }); + + test('_getNewProjectIndex', () => { + // Projects are sorted in ASCII order. + assert.equal(element._getNewProjectIndex('project A', 'filter'), 0); + assert.equal(element._getNewProjectIndex('project a', 'filter'), 1); + + // Projects are sorted by filter when the names are equal + assert.equal(element._getNewProjectIndex('project b', 'filter 0'), 1); + assert.equal(element._getNewProjectIndex('project b', 'filter 1.5'), 2); + assert.equal(element._getNewProjectIndex('project b', 'filter 3'), 3); + + // Projects with filters follow those without + assert.equal(element._getNewProjectIndex('project c', 'filter'), 4); + }); + + test('_handleAddProject', () => { + element.$.newProject.value = {id: 'project d'}; + element.$.newProject.setText('project d'); + element.$.newFilter.bindValue = ''; + + element._handleAddProject(); + + assert.equal(element._projects.length, 5); + assert.equal(element._projects[4].project, 'project d'); + assert.isNotOk(element._projects[4].filter); + assert.isTrue(element._projects[4]._is_local); + }); + + test('_handleAddProject with invalid inputs', () => { + element.$.newProject.value = {id: 'project b'}; + element.$.newProject.setText('project b'); + element.$.newFilter.bindValue = 'filter 1'; + element.$.newFilter.value = 'filter 1'; + + element._handleAddProject(); + + assert.equal(element._projects.length, 4); + }); + + test('_handleRemoveProject', () => { + assert.equal(element._projectsToRemove, 0); + const button = element.shadowRoot + .querySelector('table tbody tr:nth-child(2) gr-button'); + MockInteractions.tap(button); + + flushAsynchronousOperations(); + + const rows = element.shadowRoot + .querySelector('table tbody').querySelectorAll('tr'); + assert.equal(rows.length, 3); + + assert.equal(element._projectsToRemove.length, 1); + assert.equal(element._projectsToRemove[0].project, 'project b'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js index 8cd2021..4ac540d 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js +++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip.js
@@ -14,80 +14,92 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-account-link/gr-account-link.js'; +import '../gr-button/gr-button.js'; +import '../gr-icons/gr-icons.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-chip_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrAccountChip extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-account-chip'; } + /** + * Fired to indicate a key was pressed while this chip was focused. + * + * @event account-chip-keydown + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired to indicate this chip should be removed, i.e. when the x button is + * clicked or when the remove function is called. + * + * @event remove */ - class GrAccountChip extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-chip'; } - /** - * Fired to indicate a key was pressed while this chip was focused. - * - * @event account-chip-keydown - */ - /** - * Fired to indicate this chip should be removed, i.e. when the x button is - * clicked or when the remove function is called. - * - * @event remove - */ - - static get properties() { - return { - account: Object, - additionalText: String, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - removable: { - type: Boolean, - value: false, - }, - showAvatar: { - type: Boolean, - reflectToAttribute: true, - }, - transparentBackground: { - type: Boolean, - value: false, - }, - }; - } - - /** @override */ - ready() { - super.ready(); - this._getHasAvatars().then(hasAvatars => { - this.showAvatar = hasAvatars; - }); - } - - _getBackgroundClass(transparent) { - return transparent ? 'transparentBackground' : ''; - } - - _handleRemoveTap(e) { - e.preventDefault(); - this.fire('remove', {account: this.account}); - } - - _getHasAvatars() { - return this.$.restAPI.getConfig() - .then(cfg => Promise.resolve(!!( - cfg && cfg.plugin && cfg.plugin.has_avatars - ))); - } + static get properties() { + return { + account: Object, + additionalText: String, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + removable: { + type: Boolean, + value: false, + }, + showAvatar: { + type: Boolean, + reflectToAttribute: true, + }, + transparentBackground: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrAccountChip.is, GrAccountChip); -})(); + /** @override */ + ready() { + super.ready(); + this._getHasAvatars().then(hasAvatars => { + this.showAvatar = hasAvatars; + }); + } + + _getBackgroundClass(transparent) { + return transparent ? 'transparentBackground' : ''; + } + + _handleRemoveTap(e) { + e.preventDefault(); + this.fire('remove', {account: this.account}); + } + + _getHasAvatars() { + return this.$.restAPI.getConfig() + .then(cfg => Promise.resolve(!!( + cfg && cfg.plugin && cfg.plugin.has_avatars + ))); + } +} + +customElements.define(GrAccountChip.is, GrAccountChip);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js index 7e2d872..7f219e5 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js +++ b/polygerrit-ui/app/elements/shared/gr-account-chip/gr-account-chip_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../gr-account-link/gr-account-link.html"> -<link rel="import" href="../gr-button/gr-button.html"> -<link rel="import" href="../gr-icons/gr-icons.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-account-chip"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -88,23 +80,12 @@ width: 1.2rem; } </style> - <div class$="container [[_getBackgroundClass(transparentBackground)]]"> - <gr-account-link account="[[account]]" - additional-text="[[additionalText]]"> + <div class\$="container [[_getBackgroundClass(transparentBackground)]]"> + <gr-account-link account="[[account]]" additional-text="[[additionalText]]"> </gr-account-link> - <gr-button - id="remove" - link - hidden$="[[!removable]]" - hidden - tabindex="-1" - aria-label="Remove" - class$="remove [[_getBackgroundClass(transparentBackground)]]" - on-click="_handleRemoveTap"> + <gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" tabindex="-1" aria-label="Remove" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap"> <iron-icon icon="gr-icons:close"></iron-icon> </gr-button> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-account-chip.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js index d2a111a..49e984c 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js +++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry.js
@@ -14,96 +14,105 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-autocomplete/gr-autocomplete.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-entry_html.js'; + +/** + * gr-account-entry is an element for entering account + * and/or group with autocomplete support. + * + * @extends Polymer.Element + */ +class GrAccountEntry extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-account-entry'; } + /** + * Fired when an account is entered. + * + * @event add + */ /** - * gr-account-entry is an element for entering account - * and/or group with autocomplete support. + * When allowAnyInput is true, account-text-changed is fired when input text + * changed. This is needed so that the reply dialog's save button can be + * enabled for arbitrary cc's, which don't need a 'commit'. * - * @extends Polymer.Element + * @event account-text-changed */ - class GrAccountEntry extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-account-entry'; } - /** - * Fired when an account is entered. - * - * @event add - */ - /** - * When allowAnyInput is true, account-text-changed is fired when input text - * changed. This is needed so that the reply dialog's save button can be - * enabled for arbitrary cc's, which don't need a 'commit'. - * - * @event account-text-changed - */ + static get properties() { + return { + allowAnyInput: Boolean, + borderless: Boolean, + placeholder: String, - static get properties() { - return { - allowAnyInput: Boolean, - borderless: Boolean, - placeholder: String, + // suggestFrom = 0 to enable default suggestions. + suggestFrom: { + type: Number, + value: 0, + }, - // suggestFrom = 0 to enable default suggestions. - suggestFrom: { - type: Number, - value: 0, + /** @type {!function(string): !Promise<Array<{name, value}>>} */ + querySuggestions: { + type: Function, + notify: true, + value() { + return input => Promise.resolve([]); }, + }, - /** @type {!function(string): !Promise<Array<{name, value}>>} */ - querySuggestions: { - type: Function, - notify: true, - value() { - return input => Promise.resolve([]); - }, - }, + _config: Object, + /** The value of the autocomplete entry. */ + _inputText: { + type: String, + observer: '_inputTextChanged', + }, - _config: Object, - /** The value of the autocomplete entry. */ - _inputText: { - type: String, - observer: '_inputTextChanged', - }, - - }; - } - - get focusStart() { - return this.$.input.focusStart; - } - - focus() { - this.$.input.focus(); - } - - clear() { - this.$.input.clear(); - } - - setText(text) { - this.$.input.setText(text); - } - - getText() { - return this.$.input.text; - } - - _handleInputCommit(e) { - this.fire('add', {value: e.detail.value}); - this.$.input.focus(); - } - - _inputTextChanged(text) { - if (text.length && this.allowAnyInput) { - this.dispatchEvent(new CustomEvent( - 'account-text-changed', {bubbles: true, composed: true})); - } - } + }; } - customElements.define(GrAccountEntry.is, GrAccountEntry); -})(); + get focusStart() { + return this.$.input.focusStart; + } + + focus() { + this.$.input.focus(); + } + + clear() { + this.$.input.clear(); + } + + setText(text) { + this.$.input.setText(text); + } + + getText() { + return this.$.input.text; + } + + _handleInputCommit(e) { + this.fire('add', {value: e.detail.value}); + this.$.input.focus(); + } + + _inputTextChanged(text) { + if (text.length && this.allowAnyInput) { + this.dispatchEvent(new CustomEvent( + 'account-text-changed', {bubbles: true, composed: true})); + } + } +} + +customElements.define(GrAccountEntry.is, GrAccountEntry);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js index 992ea8407..281526d 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js +++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-account-entry"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> gr-autocomplete { display: inline-block; @@ -30,19 +24,6 @@ overflow: hidden; } </style> - <gr-autocomplete - id="input" - borderless="[[borderless]]" - placeholder="[[placeholder]]" - threshold="[[suggestFrom]]" - query="[[querySuggestions]]" - allow-non-suggested-values="[[allowAnyInput]]" - on-commit="_handleInputCommit" - clear-on-commit - warn-uncommitted - text="{{_inputText}}" - vertical-offset="24"> + <gr-autocomplete id="input" borderless="[[borderless]]" placeholder="[[placeholder]]" threshold="[[suggestFrom]]" query="[[querySuggestions]]" allow-non-suggested-values="[[allowAnyInput]]" on-commit="_handleInputCommit" clear-on-commit="" warn-uncommitted="" text="{{_inputText}}" vertical-offset="24"> </gr-autocomplete> - </template> - <script src="gr-account-entry.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html index 51310eb..7ef07d5 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-entry/gr-account-entry_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-entry</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-account-entry.html"> +<script type="module" src="./gr-account-entry.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-account-entry.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,78 +43,81 @@ </template> </test-fixture> -<script> - suite('gr-account-entry tests', async () => { - await readyToTest(); - let sandbox; - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-account-entry.js'; +suite('gr-account-entry tests', () => { + let sandbox; + let element; - const suggestion1 = { - email: 'email1@example.com', - _account_id: 1, - some_property: 'value', - }; - const suggestion2 = { - email: 'email2@example.com', - _account_id: 2, - }; - const suggestion3 = { - email: 'email25@example.com', - _account_id: 25, - some_other_property: 'other value', - }; + const suggestion1 = { + email: 'email1@example.com', + _account_id: 1, + some_property: 'value', + }; + const suggestion2 = { + email: 'email2@example.com', + _account_id: 2, + }; + const suggestion3 = { + email: 'email25@example.com', + _account_id: 25, + some_other_property: 'other value', + }; - setup(done => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - return flush(done); - }); + setup(done => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + return flush(done); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - suite('stubbed values for querySuggestions', () => { - setup(() => { - element.querySuggestions = input => Promise.resolve([ - suggestion1, - suggestion2, - suggestion3, - ]); - }); - }); - - test('account-text-changed fired when input text changed and allowAnyInput', - () => { - // Spy on query, as that is called when _updateSuggestions proceeds. - const changeStub = sandbox.stub(); - element.allowAnyInput = true; - element.querySuggestions = input => Promise.resolve([]); - element.addEventListener('account-text-changed', changeStub); - element.$.input.text = 'a'; - assert.isTrue(changeStub.calledOnce); - element.$.input.text = 'ab'; - assert.isTrue(changeStub.calledTwice); - }); - - test('account-text-changed not fired when input text changed without ' + - 'allowAnyInput', () => { - // Spy on query, as that is called when _updateSuggestions proceeds. - const changeStub = sandbox.stub(); - element.querySuggestions = input => Promise.resolve([]); - element.addEventListener('account-text-changed', changeStub); - element.$.input.text = 'a'; - assert.isFalse(changeStub.called); - }); - - test('setText', () => { - // Spy on query, as that is called when _updateSuggestions proceeds. - const suggestSpy = sandbox.spy(element.$.input, 'query'); - element.setText('test text'); - flushAsynchronousOperations(); - - assert.equal(element.$.input.$.input.value, 'test text'); - assert.isFalse(suggestSpy.called); + suite('stubbed values for querySuggestions', () => { + setup(() => { + element.querySuggestions = input => Promise.resolve([ + suggestion1, + suggestion2, + suggestion3, + ]); }); }); + + test('account-text-changed fired when input text changed and allowAnyInput', + () => { + // Spy on query, as that is called when _updateSuggestions proceeds. + const changeStub = sandbox.stub(); + element.allowAnyInput = true; + element.querySuggestions = input => Promise.resolve([]); + element.addEventListener('account-text-changed', changeStub); + element.$.input.text = 'a'; + assert.isTrue(changeStub.calledOnce); + element.$.input.text = 'ab'; + assert.isTrue(changeStub.calledTwice); + }); + + test('account-text-changed not fired when input text changed without ' + + 'allowAnyInput', () => { + // Spy on query, as that is called when _updateSuggestions proceeds. + const changeStub = sandbox.stub(); + element.querySuggestions = input => Promise.resolve([]); + element.addEventListener('account-text-changed', changeStub); + element.$.input.text = 'a'; + assert.isFalse(changeStub.called); + }); + + test('setText', () => { + // Spy on query, as that is called when _updateSuggestions proceeds. + const suggestSpy = sandbox.spy(element.$.input, 'query'); + element.setText('test text'); + flushAsynchronousOperations(); + + assert.equal(element.$.input.$.input.value, 'test text'); + assert.isFalse(suggestSpy.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js index 34c4cb6..ba65e03 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label.js
@@ -14,121 +14,134 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.js'; - /** - * @appliesMixin Gerrit.DisplayNameMixin - * @appliesMixin Gerrit.TooltipMixin - * @extends Polymer.Element - */ - class GrAccountLabel extends Polymer.mixinBehaviors( [ - Gerrit.DisplayNameBehavior, - Gerrit.TooltipBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-label'; } +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '../../../styles/shared-styles.js'; +import '../gr-avatar/gr-avatar.js'; +import '../gr-limited-text/gr-limited-text.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../scripts/util.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-label_html.js'; - static get properties() { - return { - /** - * @type {{ name: string, status: string }} - */ - account: Object, - avatarImageSize: { - type: Number, - value: 32, - }, - title: { - type: String, - reflectToAttribute: true, - computed: '_computeAccountTitle(account, additionalText)', - }, - additionalText: String, - hasTooltip: { - type: Boolean, - reflectToAttribute: true, - computed: '_computeHasTooltip(account)', - }, - hideAvatar: { - type: Boolean, - value: false, - }, - _serverConfig: { - type: Object, - value: null, - }, - }; - } +/** + * @appliesMixin Gerrit.DisplayNameMixin + * @appliesMixin Gerrit.TooltipMixin + * @extends Polymer.Element + */ +class GrAccountLabel extends mixinBehaviors( [ + Gerrit.DisplayNameBehavior, + Gerrit.TooltipBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - ready() { - super.ready(); - if (!this.additionalText) { this.additionalText = ''; } - this.$.restAPI.getConfig() - .then(config => { this._serverConfig = config; }); - } + static get is() { return 'gr-account-label'; } - _computeName(account, config) { - return this.getUserName(config, account, false); - } - - _computeStatusTextLength(account, config) { - // 35 as the max length of the name + status - return Math.max(10, 35 - this._computeName(account, config).length); - } - - _computeAccountTitle(account, tooltip) { - // Polymer 2: check for undefined - if ([ - account, - tooltip, - ].some(arg => arg === undefined)) { - return undefined; - } - - if (!account) { return; } - let result = ''; - if (this._computeName(account, this._serverConfig)) { - result += this._computeName(account, this._serverConfig); - } - if (account.email) { - result += ` <${account.email}>`; - } - if (this.additionalText) { - result += ` ${this.additionalText}`; - } - - // Show status in the label tooltip instead of - // in a separate tooltip on status - if (account.status) { - result += ` (${account.status})`; - } - - return result; - } - - _computeShowEmailClass(account) { - if (!account || account.name || !account.email) { return ''; } - return 'showEmail'; - } - - _computeEmailStr(account) { - if (!account || !account.email) { - return ''; - } - if (account.name) { - return '(' + account.email + ')'; - } - return account.email; - } - - _computeHasTooltip(account) { - // If an account has loaded to fire this method, then set to true. - return !!account; - } + static get properties() { + return { + /** + * @type {{ name: string, status: string }} + */ + account: Object, + avatarImageSize: { + type: Number, + value: 32, + }, + title: { + type: String, + reflectToAttribute: true, + computed: '_computeAccountTitle(account, additionalText)', + }, + additionalText: String, + hasTooltip: { + type: Boolean, + reflectToAttribute: true, + computed: '_computeHasTooltip(account)', + }, + hideAvatar: { + type: Boolean, + value: false, + }, + _serverConfig: { + type: Object, + value: null, + }, + }; } - customElements.define(GrAccountLabel.is, GrAccountLabel); -})(); + /** @override */ + ready() { + super.ready(); + if (!this.additionalText) { this.additionalText = ''; } + this.$.restAPI.getConfig() + .then(config => { this._serverConfig = config; }); + } + + _computeName(account, config) { + return this.getUserName(config, account, false); + } + + _computeStatusTextLength(account, config) { + // 35 as the max length of the name + status + return Math.max(10, 35 - this._computeName(account, config).length); + } + + _computeAccountTitle(account, tooltip) { + // Polymer 2: check for undefined + if ([ + account, + tooltip, + ].some(arg => arg === undefined)) { + return undefined; + } + + if (!account) { return; } + let result = ''; + if (this._computeName(account, this._serverConfig)) { + result += this._computeName(account, this._serverConfig); + } + if (account.email) { + result += ` <${account.email}>`; + } + if (this.additionalText) { + result += ` ${this.additionalText}`; + } + + // Show status in the label tooltip instead of + // in a separate tooltip on status + if (account.status) { + result += ` (${account.status})`; + } + + return result; + } + + _computeShowEmailClass(account) { + if (!account || account.name || !account.email) { return ''; } + return 'showEmail'; + } + + _computeEmailStr(account) { + if (!account || !account.email) { + return ''; + } + if (account.name) { + return '(' + account.email + ')'; + } + return account.email; + } + + _computeHasTooltip(account) { + // If an account has loaded to fire this method, then set to true. + return !!account; + } +} + +customElements.define(GrAccountLabel.is, GrAccountLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js index fcd9ccd..e9d0e5d 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/gr-display-name-behavior/gr-display-name-behavior.html"> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-avatar/gr-avatar.html"> -<link rel="import" href="../gr-limited-text/gr-limited-text.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> -<script src="../../../scripts/util.js"></script> - -<dom-module id="gr-account-label"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: inline; @@ -54,25 +45,19 @@ </style> <span> <template is="dom-if" if="[[!hideAvatar]]"> - <gr-avatar account="[[account]]" - image-size="[[avatarImageSize]]"></gr-avatar> + <gr-avatar account="[[account]]" image-size="[[avatarImageSize]]"></gr-avatar> </template> - <span class$="text [[_computeShowEmailClass(account)]]"> + <span class\$="text [[_computeShowEmailClass(account)]]"> <span class="name"> [[_computeName(account, _serverConfig)]]</span> <span class="email"> [[_computeEmailStr(account)]] </span> <template is="dom-if" if="[[account.status]]"> - (<gr-limited-text - disable-tooltip="true" - limit="[[_computeStatusTextLength(account, _serverConfig)]]" - text="[[account.status]]"> + (<gr-limited-text disable-tooltip="true" limit="[[_computeStatusTextLength(account, _serverConfig)]]" text="[[account.status]]"> </gr-limited-text>) </template> </span> </span> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-account-label.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html index f5a9b8d..db742e6 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-label</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-account-label.html"> +<script type="module" src="./gr-account-label.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-account-label.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,17 +43,124 @@ </template> </test-fixture> -<script> - suite('gr-account-label tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-account-label.js'; +suite('gr-account-label tests', () => { + let element; + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getLoggedIn() { return Promise.resolve(false); }, + }); + element = fixture('basic'); + element._config = { + user: { + anonymous_coward_name: 'Anonymous Coward', + }, + }; + }); + + test('null guard', () => { + assert.doesNotThrow(() => { + element.account = null; + }); + }); + + test('missing email', () => { + assert.equal('', element._computeEmailStr({name: 'foo'})); + }); + + test('computed fields', () => { + assert.equal( + element._computeAccountTitle({ + name: 'Andrew Bonventre', + email: 'andybons+gerrit@gmail.com', + }, /* additionalText= */ ''), + 'Andrew Bonventre <andybons+gerrit@gmail.com>'); + + assert.equal( + element._computeAccountTitle({ + name: 'Andrew Bonventre', + }, /* additionalText= */ ''), + 'Andrew Bonventre'); + + assert.equal( + element._computeAccountTitle({ + email: 'andybons+gerrit@gmail.com', + }, /* additionalText= */ ''), + 'Anonymous <andybons+gerrit@gmail.com>'); + + assert.equal(element._computeShowEmailClass( + { + name: 'Andrew Bonventre', + email: 'andybons+gerrit@gmail.com', + }, /* additionalText= */ ''), ''); + + assert.equal(element._computeShowEmailClass( + { + email: 'andybons+gerrit@gmail.com', + }, /* additionalText= */ ''), 'showEmail'); + + assert.equal(element._computeShowEmailClass( + {name: 'Andrew Bonventre'}, + /* additionalText= */ '' + ), + ''); + + assert.equal(element._computeShowEmailClass(undefined), ''); + + assert.equal( + element._computeEmailStr({name: 'test', email: 'test'}), '(test)'); + assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test'); + }); + + suite('_computeName', () => { + test('not showing anonymous', () => { + const account = {name: 'Wyatt'}; + assert.deepEqual(element._computeName(account, null), 'Wyatt'); + }); + + test('showing anonymous but no config', () => { + const account = {}; + assert.deepEqual(element._computeName(account, null), + 'Anonymous'); + }); + + test('test for Anonymous Coward user and replace with Anonymous', () => { + const config = { + user: { + anonymous_coward_name: 'Anonymous Coward', + }, + }; + const account = {}; + assert.deepEqual(element._computeName(account, config), + 'Anonymous'); + }); + + test('test for anonymous_coward_name', () => { + const config = { + user: { + anonymous_coward_name: 'TestAnon', + }, + }; + const account = {}; + assert.deepEqual(element._computeName(account, config), + 'TestAnon'); + }); + }); + + suite('status in tooltip', () => { setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getLoggedIn() { return Promise.resolve(false); }, - }); element = fixture('basic'); + element.account = { + name: 'test', + email: 'test@google.com', + status: 'OOO until Aug 10th', + }; element._config = { user: { anonymous_coward_name: 'Anonymous Coward', @@ -55,133 +168,29 @@ }; }); - test('null guard', () => { - assert.doesNotThrow(() => { - element.account = null; - }); + test('tooltip should contain status text', () => { + assert.deepEqual(element.title, + 'test <test@google.com> (OOO until Aug 10th)'); }); - test('missing email', () => { - assert.equal('', element._computeEmailStr({name: 'foo'})); + test('status text should not have tooltip', () => { + flushAsynchronousOperations(); + assert.deepEqual(element.shadowRoot + .querySelector('gr-limited-text').title, ''); }); - test('computed fields', () => { - assert.equal( - element._computeAccountTitle({ - name: 'Andrew Bonventre', - email: 'andybons+gerrit@gmail.com', - }, /* additionalText= */ ''), - 'Andrew Bonventre <andybons+gerrit@gmail.com>'); - - assert.equal( - element._computeAccountTitle({ - name: 'Andrew Bonventre', - }, /* additionalText= */ ''), - 'Andrew Bonventre'); - - assert.equal( - element._computeAccountTitle({ - email: 'andybons+gerrit@gmail.com', - }, /* additionalText= */ ''), - 'Anonymous <andybons+gerrit@gmail.com>'); - - assert.equal(element._computeShowEmailClass( - { - name: 'Andrew Bonventre', - email: 'andybons+gerrit@gmail.com', - }, /* additionalText= */ ''), ''); - - assert.equal(element._computeShowEmailClass( - { - email: 'andybons+gerrit@gmail.com', - }, /* additionalText= */ ''), 'showEmail'); - - assert.equal(element._computeShowEmailClass( - {name: 'Andrew Bonventre'}, - /* additionalText= */ '' - ), - ''); - - assert.equal(element._computeShowEmailClass(undefined), ''); - - assert.equal( - element._computeEmailStr({name: 'test', email: 'test'}), '(test)'); - assert.equal(element._computeEmailStr({email: 'test'}, ''), 'test'); - }); - - suite('_computeName', () => { - test('not showing anonymous', () => { - const account = {name: 'Wyatt'}; - assert.deepEqual(element._computeName(account, null), 'Wyatt'); - }); - - test('showing anonymous but no config', () => { - const account = {}; - assert.deepEqual(element._computeName(account, null), - 'Anonymous'); - }); - - test('test for Anonymous Coward user and replace with Anonymous', () => { - const config = { - user: { - anonymous_coward_name: 'Anonymous Coward', - }, - }; - const account = {}; - assert.deepEqual(element._computeName(account, config), - 'Anonymous'); - }); - - test('test for anonymous_coward_name', () => { - const config = { - user: { - anonymous_coward_name: 'TestAnon', - }, - }; - const account = {}; - assert.deepEqual(element._computeName(account, config), - 'TestAnon'); - }); - }); - - suite('status in tooltip', () => { - setup(() => { - element = fixture('basic'); - element.account = { - name: 'test', - email: 'test@google.com', - status: 'OOO until Aug 10th', - }; - element._config = { - user: { - anonymous_coward_name: 'Anonymous Coward', - }, - }; - }); - - test('tooltip should contain status text', () => { - assert.deepEqual(element.title, - 'test <test@google.com> (OOO until Aug 10th)'); - }); - - test('status text should not have tooltip', () => { - flushAsynchronousOperations(); - assert.deepEqual(element.shadowRoot - .querySelector('gr-limited-text').title, ''); - }); - - test('status text should honor the name length and total length', () => { - assert.deepEqual( - element._computeStatusTextLength(element.account, element._config), - 31 - ); - assert.deepEqual( - element._computeStatusTextLength({ - name: 'a very long long long long name', - }, element._config), - 10 - ); - }); + test('status text should honor the name length and total length', () => { + assert.deepEqual( + element._computeStatusTextLength(element.account, element._config), + 31 + ); + assert.deepEqual( + element._computeStatusTextLength({ + name: 'a very long long long long name', + }, element._config), + 10 + ); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js index b0ce04c..4a38427 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,38 +14,48 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @extends Polymer.Element - */ - class GrAccountLink extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-link'; } +import '../../../scripts/bundled-polymer.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../gr-account-label/gr-account-label.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-link_html.js'; - static get properties() { - return { - additionalText: String, - account: Object, - avatarImageSize: { - type: Number, - value: 32, - }, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @extends Polymer.Element + */ +class GrAccountLink extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _computeOwnerLink(account) { - if (!account) { return; } - return Gerrit.Nav.getUrlForOwner( - account.email || account.username || account.name || - account._account_id); - } + static get is() { return 'gr-account-link'; } + + static get properties() { + return { + additionalText: String, + account: Object, + avatarImageSize: { + type: Number, + value: 32, + }, + }; } - customElements.define(GrAccountLink.is, GrAccountLink); -})(); + _computeOwnerLink(account) { + if (!account) { return; } + return Gerrit.Nav.getUrlForOwner( + account.email || account.username || account.name || + account._account_id); + } +} + +customElements.define(GrAccountLink.is, GrAccountLink);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js index d3575b2..4ea343e 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../gr-account-label/gr-account-label.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-account-link"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: inline-block; @@ -38,12 +32,8 @@ } </style> <span> - <a href$="[[_computeOwnerLink(account)]]" tabindex="-1"> - <gr-account-label account="[[account]]" - additional-text="[[additionalText]]" - avatar-image-size="[[avatarImageSize]]"></gr-account-label> + <a href\$="[[_computeOwnerLink(account)]]" tabindex="-1"> + <gr-account-label account="[[account]]" additional-text="[[additionalText]]" avatar-image-size="[[avatarImageSize]]"></gr-account-label> </a> </span> - </template> - <script src="gr-account-link.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html index c648661..ff89a78 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-link</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-account-link.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-account-link.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-link.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,48 +40,50 @@ </template> </test-fixture> -<script> - suite('gr-account-link tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-link.js'; +suite('gr-account-link tests', () => { + let element; + let sandbox; - setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - sandbox = sinon.sandbox.create(); + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, }); - - teardown(() => { - sandbox.restore(); - }); - - test('computed fields', () => { - const url = 'test/url'; - const urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForOwner').returns(url); - const account = { - email: 'email', - username: 'username', - name: 'name', - _account_id: '_account_id', - }; - assert.isNotOk(element._computeOwnerLink()); - assert.equal(element._computeOwnerLink(account), url); - assert.isTrue(urlStub.lastCall.calledWithExactly('email')); - - delete account.email; - assert.equal(element._computeOwnerLink(account), url); - assert.isTrue(urlStub.lastCall.calledWithExactly('username')); - - delete account.username; - assert.equal(element._computeOwnerLink(account), url); - assert.isTrue(urlStub.lastCall.calledWithExactly('name')); - - delete account.name; - assert.equal(element._computeOwnerLink(account), url); - assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id')); - }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('computed fields', () => { + const url = 'test/url'; + const urlStub = sandbox.stub(Gerrit.Nav, 'getUrlForOwner').returns(url); + const account = { + email: 'email', + username: 'username', + name: 'name', + _account_id: '_account_id', + }; + assert.isNotOk(element._computeOwnerLink()); + assert.equal(element._computeOwnerLink(account), url); + assert.isTrue(urlStub.lastCall.calledWithExactly('email')); + + delete account.email; + assert.equal(element._computeOwnerLink(account), url); + assert.isTrue(urlStub.lastCall.calledWithExactly('username')); + + delete account.username; + assert.equal(element._computeOwnerLink(account), url); + assert.isTrue(urlStub.lastCall.calledWithExactly('name')); + + delete account.name; + assert.equal(element._computeOwnerLink(account), url); + assert.isTrue(urlStub.lastCall.calledWithExactly('_account_id')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js index 7955d50..2e8f768 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list.js
@@ -14,342 +14,353 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const VALID_EMAIL_ALERT = 'Please input a valid email.'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-account-chip/gr-account-chip.js'; +import '../gr-account-entry/gr-account-entry.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-account-list_html.js'; +const VALID_EMAIL_ALERT = 'Please input a valid email.'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrAccountList extends mixinBehaviors( [ + // Used in the tests for gr-account-list and other elements tests. + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-account-list'; } /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when user inputs an invalid email address. + * + * @event show-alert */ - class GrAccountList extends Polymer.mixinBehaviors( [ - // Used in the tests for gr-account-list and other elements tests. - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-account-list'; } - /** - * Fired when user inputs an invalid email address. - * - * @event show-alert - */ - static get properties() { - return { - accounts: { - type: Array, - value() { return []; }, - notify: true, - }, - change: Object, - filter: Function, - placeholder: String, - disabled: { - type: Function, - value: false, - }, + static get properties() { + return { + accounts: { + type: Array, + value() { return []; }, + notify: true, + }, + change: Object, + filter: Function, + placeholder: String, + disabled: { + type: Function, + value: false, + }, - /** - * Returns suggestions and convert them to list item - * - * @type {Gerrit.GrSuggestionsProvider} - */ - suggestionsProvider: { - type: Object, - }, + /** + * Returns suggestions and convert them to list item + * + * @type {Gerrit.GrSuggestionsProvider} + */ + suggestionsProvider: { + type: Object, + }, - /** - * Needed for template checking since value is initially set to null. - * - * @type {?Object} - */ - pendingConfirmation: { - type: Object, - value: null, - notify: true, - }, - readonly: { - type: Boolean, - value: false, - }, - /** - * When true, allows for non-suggested inputs to be added. - */ - allowAnyInput: { - type: Boolean, - value: false, - }, + /** + * Needed for template checking since value is initially set to null. + * + * @type {?Object} + */ + pendingConfirmation: { + type: Object, + value: null, + notify: true, + }, + readonly: { + type: Boolean, + value: false, + }, + /** + * When true, allows for non-suggested inputs to be added. + */ + allowAnyInput: { + type: Boolean, + value: false, + }, - /** - * Array of values (groups/accounts) that are removable. When this prop is - * undefined, all values are removable. - */ - removableValues: Array, - maxCount: { - type: Number, - value: 0, - }, + /** + * Array of values (groups/accounts) that are removable. When this prop is + * undefined, all values are removable. + */ + removableValues: Array, + maxCount: { + type: Number, + value: 0, + }, - /** - * Returns suggestion items - * - * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>} - */ - _querySuggestions: { - type: Function, - value() { - return this._getSuggestions.bind(this); - }, + /** + * Returns suggestion items + * + * @type {!function(string): Promise<Array<Gerrit.GrSuggestionItem>>} + */ + _querySuggestions: { + type: Function, + value() { + return this._getSuggestions.bind(this); }, + }, - /** - * Set to true to disable suggestions on empty input. - */ - skipSuggestOnEmpty: { - type: Boolean, - value: false, - }, - }; + /** + * Set to true to disable suggestions on empty input. + */ + skipSuggestOnEmpty: { + type: Boolean, + value: false, + }, + }; + } + + /** @override */ + created() { + super.created(); + this.addEventListener('remove', + e => this._handleRemove(e)); + } + + get accountChips() { + return Array.from( + dom(this.root).querySelectorAll('gr-account-chip')); + } + + get focusStart() { + return this.$.entry.focusStart; + } + + _getSuggestions(input) { + if (this.skipSuggestOnEmpty && !input) { + return Promise.resolve([]); } - - /** @override */ - created() { - super.created(); - this.addEventListener('remove', - e => this._handleRemove(e)); + const provider = this.suggestionsProvider; + if (!provider) { + return Promise.resolve([]); } - - get accountChips() { - return Array.from( - Polymer.dom(this.root).querySelectorAll('gr-account-chip')); - } - - get focusStart() { - return this.$.entry.focusStart; - } - - _getSuggestions(input) { - if (this.skipSuggestOnEmpty && !input) { - return Promise.resolve([]); + return provider.getSuggestions(input).then(suggestions => { + if (!suggestions) { return []; } + if (this.filter) { + suggestions = suggestions.filter(this.filter); } - const provider = this.suggestionsProvider; - if (!provider) { - return Promise.resolve([]); + return suggestions.map(suggestion => + provider.makeSuggestionItem(suggestion)); + }); + } + + _handleAdd(e) { + this._addAccountItem(e.detail.value); + } + + _addAccountItem(item) { + // Append new account or group to the accounts property. We add our own + // internal properties to the account/group here, so we clone the object + // to avoid cluttering up the shared change object. + if (item.account) { + const account = + Object.assign({}, item.account, {_pendingAdd: true}); + this.push('accounts', account); + } else if (item.group) { + if (item.confirm) { + this.pendingConfirmation = item; + return; } - return provider.getSuggestions(input).then(suggestions => { - if (!suggestions) { return []; } - if (this.filter) { - suggestions = suggestions.filter(this.filter); - } - return suggestions.map(suggestion => - provider.makeSuggestionItem(suggestion)); - }); - } - - _handleAdd(e) { - this._addAccountItem(e.detail.value); - } - - _addAccountItem(item) { - // Append new account or group to the accounts property. We add our own - // internal properties to the account/group here, so we clone the object - // to avoid cluttering up the shared change object. - if (item.account) { - const account = - Object.assign({}, item.account, {_pendingAdd: true}); - this.push('accounts', account); - } else if (item.group) { - if (item.confirm) { - this.pendingConfirmation = item; - return; - } - const group = Object.assign({}, item.group, - {_pendingAdd: true, _group: true}); - this.push('accounts', group); - } else if (this.allowAnyInput) { - if (!item.includes('@')) { - // Repopulate the input with what the user tried to enter and have - // a toast tell them why they can't enter it. - this.$.entry.setText(item); - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: VALID_EMAIL_ALERT}, - bubbles: true, - composed: true, - })); - return false; - } else { - const account = {email: item, _pendingAdd: true}; - this.push('accounts', account); - } - } - this.pendingConfirmation = null; - return true; - } - - confirmGroup(group) { - group = Object.assign( - {}, group, {confirmed: true, _pendingAdd: true, _group: true}); + const group = Object.assign({}, item.group, + {_pendingAdd: true, _group: true}); this.push('accounts', group); - this.pendingConfirmation = null; - } - - _computeChipClass(account) { - const classes = []; - if (account._group) { - classes.push('group'); + } else if (this.allowAnyInput) { + if (!item.includes('@')) { + // Repopulate the input with what the user tried to enter and have + // a toast tell them why they can't enter it. + this.$.entry.setText(item); + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message: VALID_EMAIL_ALERT}, + bubbles: true, + composed: true, + })); + return false; + } else { + const account = {email: item, _pendingAdd: true}; + this.push('accounts', account); } - if (account._pendingAdd) { - classes.push('pendingAdd'); - } - return classes.join(' '); } + this.pendingConfirmation = null; + return true; + } - _accountMatches(a, b) { - if (a && b) { - if (a._account_id) { - return a._account_id === b._account_id; - } - if (a.email) { - return a.email === b.email; + confirmGroup(group) { + group = Object.assign( + {}, group, {confirmed: true, _pendingAdd: true, _group: true}); + this.push('accounts', group); + this.pendingConfirmation = null; + } + + _computeChipClass(account) { + const classes = []; + if (account._group) { + classes.push('group'); + } + if (account._pendingAdd) { + classes.push('pendingAdd'); + } + return classes.join(' '); + } + + _accountMatches(a, b) { + if (a && b) { + if (a._account_id) { + return a._account_id === b._account_id; + } + if (a.email) { + return a.email === b.email; + } + } + return a === b; + } + + _computeRemovable(account, readonly) { + if (readonly) { return false; } + if (this.removableValues) { + for (let i = 0; i < this.removableValues.length; i++) { + if (this._accountMatches(this.removableValues[i], account)) { + return true; } } - return a === b; + return !!account._pendingAdd; } + return true; + } - _computeRemovable(account, readonly) { - if (readonly) { return false; } - if (this.removableValues) { - for (let i = 0; i < this.removableValues.length; i++) { - if (this._accountMatches(this.removableValues[i], account)) { - return true; - } - } - return !!account._pendingAdd; + _handleRemove(e) { + const toRemove = e.detail.account; + this._removeAccount(toRemove); + this.$.entry.focus(); + } + + _removeAccount(toRemove) { + if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) { + return; + } + for (let i = 0; i < this.accounts.length; i++) { + let matches; + const account = this.accounts[i]; + if (toRemove._group) { + matches = toRemove.id === account.id; + } else { + matches = this._accountMatches(toRemove, account); } - return true; - } - - _handleRemove(e) { - const toRemove = e.detail.account; - this._removeAccount(toRemove); - this.$.entry.focus(); - } - - _removeAccount(toRemove) { - if (!toRemove || !this._computeRemovable(toRemove, this.readonly)) { + if (matches) { + this.splice('accounts', i, 1); return; } - for (let i = 0; i < this.accounts.length; i++) { - let matches; - const account = this.accounts[i]; - if (toRemove._group) { - matches = toRemove.id === account.id; - } else { - matches = this._accountMatches(toRemove, account); + } + console.warn('received remove event for missing account', toRemove); + } + + _getNativeInput(paperInput) { + // In Polymer 2 inputElement isn't nativeInput anymore + return paperInput.$.nativeInput || paperInput.inputElement; + } + + _handleInputKeydown(e) { + const input = this._getNativeInput(e.detail.input); + if (input.selectionStart !== input.selectionEnd || + input.selectionStart !== 0) { + return; + } + switch (e.detail.keyCode) { + case 8: // Backspace + this._removeAccount(this.accounts[this.accounts.length - 1]); + break; + case 37: // Left arrow + if (this.accountChips[this.accountChips.length - 1]) { + this.accountChips[this.accountChips.length - 1].focus(); } - if (matches) { - this.splice('accounts', i, 1); - return; - } - } - console.warn('received remove event for missing account', toRemove); - } - - _getNativeInput(paperInput) { - // In Polymer 2 inputElement isn't nativeInput anymore - return paperInput.$.nativeInput || paperInput.inputElement; - } - - _handleInputKeydown(e) { - const input = this._getNativeInput(e.detail.input); - if (input.selectionStart !== input.selectionEnd || - input.selectionStart !== 0) { - return; - } - switch (e.detail.keyCode) { - case 8: // Backspace - this._removeAccount(this.accounts[this.accounts.length - 1]); - break; - case 37: // Left arrow - if (this.accountChips[this.accountChips.length - 1]) { - this.accountChips[this.accountChips.length - 1].focus(); - } - break; - } - } - - _handleChipKeydown(e) { - const chip = e.target; - const chips = this.accountChips; - const index = chips.indexOf(chip); - switch (e.keyCode) { - case 8: // Backspace - case 13: // Enter - case 32: // Spacebar - case 46: // Delete - this._removeAccount(chip.account); - // Splice from this array to avoid inconsistent ordering of - // event handling. - chips.splice(index, 1); - if (index < chips.length) { - chips[index].focus(); - } else if (index > 0) { - chips[index - 1].focus(); - } else { - this.$.entry.focus(); - } - break; - case 37: // Left arrow - if (index > 0) { - chip.blur(); - chips[index - 1].focus(); - } - break; - case 39: // Right arrow - chip.blur(); - if (index < chips.length - 1) { - chips[index + 1].focus(); - } else { - this.$.entry.focus(); - } - break; - } - } - - /** - * Submit the text of the entry as a reviewer value, if it exists. If it is - * a successful submit of the text, clear the entry value. - * - * @return {boolean} If there is text in the entry, return true if the - * submission was successful and false if not. If there is no text, - * return true. - */ - submitEntryText() { - const text = this.$.entry.getText(); - if (!text.length) { return true; } - const wasSubmitted = this._addAccountItem(text); - if (wasSubmitted) { this.$.entry.clear(); } - return wasSubmitted; - } - - additions() { - return this.accounts - .filter(account => account._pendingAdd) - .map(account => { - if (account._group) { - return {group: account}; - } else { - return {account}; - } - }); - } - - _computeEntryHidden(maxCount, accountsRecord, readonly) { - return (maxCount && maxCount <= accountsRecord.base.length) || readonly; + break; } } - customElements.define(GrAccountList.is, GrAccountList); -})(); + _handleChipKeydown(e) { + const chip = e.target; + const chips = this.accountChips; + const index = chips.indexOf(chip); + switch (e.keyCode) { + case 8: // Backspace + case 13: // Enter + case 32: // Spacebar + case 46: // Delete + this._removeAccount(chip.account); + // Splice from this array to avoid inconsistent ordering of + // event handling. + chips.splice(index, 1); + if (index < chips.length) { + chips[index].focus(); + } else if (index > 0) { + chips[index - 1].focus(); + } else { + this.$.entry.focus(); + } + break; + case 37: // Left arrow + if (index > 0) { + chip.blur(); + chips[index - 1].focus(); + } + break; + case 39: // Right arrow + chip.blur(); + if (index < chips.length - 1) { + chips[index + 1].focus(); + } else { + this.$.entry.focus(); + } + break; + } + } + + /** + * Submit the text of the entry as a reviewer value, if it exists. If it is + * a successful submit of the text, clear the entry value. + * + * @return {boolean} If there is text in the entry, return true if the + * submission was successful and false if not. If there is no text, + * return true. + */ + submitEntryText() { + const text = this.$.entry.getText(); + if (!text.length) { return true; } + const wasSubmitted = this._addAccountItem(text); + if (wasSubmitted) { this.$.entry.clear(); } + return wasSubmitted; + } + + additions() { + return this.accounts + .filter(account => account._pendingAdd) + .map(account => { + if (account._group) { + return {group: account}; + } else { + return {account}; + } + }); + } + + _computeEntryHidden(maxCount, accountsRecord, readonly) { + return (maxCount && maxCount <= accountsRecord.base.length) || readonly; + } +} + +customElements.define(GrAccountList.is, GrAccountList);
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js index 37591d8..2438bc1 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../gr-account-entry/gr-account-entry.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-account-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> gr-account-chip { display: inline-block; @@ -53,27 +47,11 @@ --> <div class="list"> <template id="chips" is="dom-repeat" items="[[accounts]]" as="account"> - <gr-account-chip - account="[[account]]" - class$="[[_computeChipClass(account)]]" - data-account-id$="[[account._account_id]]" - removable="[[_computeRemovable(account, readonly)]]" - on-keydown="_handleChipKeydown" - tabindex="-1"> + <gr-account-chip account="[[account]]" class\$="[[_computeChipClass(account)]]" data-account-id\$="[[account._account_id]]" removable="[[_computeRemovable(account, readonly)]]" on-keydown="_handleChipKeydown" tabindex="-1"> </gr-account-chip> </template> </div> - <gr-account-entry - borderless - hidden$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" - id="entry" - placeholder="[[placeholder]]" - on-add="_handleAdd" - on-input-keydown="_handleInputKeydown" - allow-any-input="[[allowAnyInput]]" - query-suggestions="[[_querySuggestions]]"> + <gr-account-entry borderless="" hidden\$="[[_computeEntryHidden(maxCount, accounts.*, readonly)]]" id="entry" placeholder="[[placeholder]]" on-add="_handleAdd" on-input-keydown="_handleInputKeydown" allow-any-input="[[allowAnyInput]]" query-suggestions="[[_querySuggestions]]"> </gr-account-entry> <slot></slot> - </template> - <script src="gr-account-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html index 39d0a88..1fc78ea 100644 --- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html +++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-account-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-account-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-account-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,505 +40,509 @@ </template> </test-fixture> -<script> - class MockSuggestionsProvider { - getSuggestions(input) { - return Promise.resolve([]); - } +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-account-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; - makeSuggestionItem(item) { - return item; - } +class MockSuggestionsProvider { + getSuggestions(input) { + return Promise.resolve([]); } - suite('gr-account-list tests', async () => { - await readyToTest(); - let _nextAccountId = 0; - const makeAccount = function() { - const accountId = ++_nextAccountId; - return { - _account_id: accountId, - }; + makeSuggestionItem(item) { + return item; + } +} + +suite('gr-account-list tests', () => { + let _nextAccountId = 0; + const makeAccount = function() { + const accountId = ++_nextAccountId; + return { + _account_id: accountId, }; - const makeGroup = function() { - const groupId = 'group' + (++_nextAccountId); - return { - id: groupId, - _group: true, - }; + }; + const makeGroup = function() { + const groupId = 'group' + (++_nextAccountId); + return { + id: groupId, + _group: true, }; + }; - let existingAccount1; - let existingAccount2; - let sandbox; - let element; - let suggestionsProvider; + let existingAccount1; + let existingAccount2; + let sandbox; + let element; + let suggestionsProvider; - function getChips() { - return Polymer.dom(element.root).querySelectorAll('gr-account-chip'); - } + function getChips() { + return dom(element.root).querySelectorAll('gr-account-chip'); + } + setup(() => { + sandbox = sinon.sandbox.create(); + existingAccount1 = makeAccount(); + existingAccount2 = makeAccount(); + + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + element.accounts = [existingAccount1, existingAccount2]; + suggestionsProvider = new MockSuggestionsProvider(); + element.suggestionsProvider = suggestionsProvider; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('account entry only appears when editable', () => { + element.readonly = false; + assert.isFalse(element.$.entry.hasAttribute('hidden')); + element.readonly = true; + assert.isTrue(element.$.entry.hasAttribute('hidden')); + }); + + test('addition and removal of account/group chips', () => { + flushAsynchronousOperations(); + sandbox.stub(element, '_computeRemovable').returns(true); + // Existing accounts are listed. + let chips = getChips(); + assert.equal(chips.length, 2); + assert.isFalse(chips[0].classList.contains('pendingAdd')); + assert.isFalse(chips[1].classList.contains('pendingAdd')); + + // New accounts are added to end with pendingAdd class. + const newAccount = makeAccount(); + element._handleAdd({ + detail: { + value: { + account: newAccount, + }, + }, + }); + flushAsynchronousOperations(); + chips = getChips(); + assert.equal(chips.length, 3); + assert.isFalse(chips[0].classList.contains('pendingAdd')); + assert.isFalse(chips[1].classList.contains('pendingAdd')); + assert.isTrue(chips[2].classList.contains('pendingAdd')); + + // Removed accounts are taken out of the list. + element.fire('remove', {account: existingAccount1}); + flushAsynchronousOperations(); + chips = getChips(); + assert.equal(chips.length, 2); + assert.isFalse(chips[0].classList.contains('pendingAdd')); + assert.isTrue(chips[1].classList.contains('pendingAdd')); + + // Invalid remove is ignored. + element.fire('remove', {account: existingAccount1}); + element.fire('remove', {account: newAccount}); + flushAsynchronousOperations(); + chips = getChips(); + assert.equal(chips.length, 1); + assert.isFalse(chips[0].classList.contains('pendingAdd')); + + // New groups are added to end with pendingAdd and group classes. + const newGroup = makeGroup(); + element._handleAdd({ + detail: { + value: { + group: newGroup, + }, + }, + }); + flushAsynchronousOperations(); + chips = getChips(); + assert.equal(chips.length, 2); + assert.isTrue(chips[1].classList.contains('group')); + assert.isTrue(chips[1].classList.contains('pendingAdd')); + + // Removed groups are taken out of the list. + element.fire('remove', {account: newGroup}); + flushAsynchronousOperations(); + chips = getChips(); + assert.equal(chips.length, 1); + assert.isFalse(chips[0].classList.contains('pendingAdd')); + }); + + test('_getSuggestions uses filter correctly', done => { + const originalSuggestions = [ + { + email: 'abc@example.com', + text: 'abcd', + _account_id: 3, + }, + { + email: 'qwe@example.com', + text: 'qwer', + _account_id: 1, + }, + { + email: 'xyz@example.com', + text: 'aaaaa', + _account_id: 25, + }, + ]; + sandbox.stub(suggestionsProvider, 'getSuggestions') + .returns(Promise.resolve(originalSuggestions)); + sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => { + return { + name: suggestion.email, + value: suggestion._account_id, + }; + }); + + element._getSuggestions().then(suggestions => { + // Default is no filtering. + assert.equal(suggestions.length, 3); + + // Set up filter that only accepts suggestion1. + const accountId = originalSuggestions[0]._account_id; + element.filter = function(suggestion) { + return suggestion._account_id === accountId; + }; + + element._getSuggestions() + .then(suggestions => { + assert.deepEqual(suggestions, + [{name: originalSuggestions[0].email, + value: originalSuggestions[0]._account_id}]); + }) + .then(done); + }); + }); + + test('_computeChipClass', () => { + const account = makeAccount(); + assert.equal(element._computeChipClass(account), ''); + account._pendingAdd = true; + assert.equal(element._computeChipClass(account), 'pendingAdd'); + account._group = true; + assert.equal(element._computeChipClass(account), 'group pendingAdd'); + account._pendingAdd = false; + assert.equal(element._computeChipClass(account), 'group'); + }); + + test('_computeRemovable', () => { + const newAccount = makeAccount(); + newAccount._pendingAdd = true; + element.readonly = false; + element.removableValues = []; + assert.isFalse(element._computeRemovable(existingAccount1, false)); + assert.isTrue(element._computeRemovable(newAccount, false)); + + element.removableValues = [existingAccount1]; + assert.isTrue(element._computeRemovable(existingAccount1, false)); + assert.isTrue(element._computeRemovable(newAccount, false)); + assert.isFalse(element._computeRemovable(existingAccount2, false)); + + element.readonly = true; + assert.isFalse(element._computeRemovable(existingAccount1, true)); + assert.isFalse(element._computeRemovable(newAccount, true)); + }); + + test('submitEntryText', () => { + element.allowAnyInput = true; + flushAsynchronousOperations(); + + const getTextStub = sandbox.stub(element.$.entry, 'getText'); + getTextStub.onFirstCall().returns(''); + getTextStub.onSecondCall().returns('test'); + getTextStub.onThirdCall().returns('test@test'); + + // When entry is empty, return true. + const clearStub = sandbox.stub(element.$.entry, 'clear'); + assert.isTrue(element.submitEntryText()); + assert.isFalse(clearStub.called); + + // When entry is invalid, return false. + assert.isFalse(element.submitEntryText()); + assert.isFalse(clearStub.called); + + // When entry is valid, return true and clear text. + assert.isTrue(element.submitEntryText()); + assert.isTrue(clearStub.called); + assert.equal(element.additions()[0].account.email, 'test@test'); + }); + + test('additions returns sanitized new accounts and groups', () => { + assert.equal(element.additions().length, 0); + + const newAccount = makeAccount(); + element._handleAdd({ + detail: { + value: { + account: newAccount, + }, + }, + }); + const newGroup = makeGroup(); + element._handleAdd({ + detail: { + value: { + group: newGroup, + }, + }, + }); + + assert.deepEqual(element.additions(), [ + { + account: { + _account_id: newAccount._account_id, + _pendingAdd: true, + }, + }, + { + group: { + id: newGroup.id, + _group: true, + _pendingAdd: true, + }, + }, + ]); + }); + + test('large group confirmations', () => { + assert.isNull(element.pendingConfirmation); + assert.deepEqual(element.additions(), []); + + const group = makeGroup(); + const reviewer = { + group, + count: 10, + confirm: true, + }; + element._handleAdd({ + detail: { + value: reviewer, + }, + }); + + assert.deepEqual(element.pendingConfirmation, reviewer); + assert.deepEqual(element.additions(), []); + + element.confirmGroup(group); + assert.isNull(element.pendingConfirmation); + assert.deepEqual(element.additions(), [ + { + group: { + id: group.id, + _group: true, + _pendingAdd: true, + confirmed: true, + }, + }, + ]); + }); + + test('removeAccount fails if account is not removable', () => { + element.readonly = true; + const acct = makeAccount(); + element.accounts = [acct]; + element._removeAccount(acct); + assert.equal(element.accounts.length, 1); + }); + + test('max-count', () => { + element.maxCount = 1; + const acct = makeAccount(); + element._handleAdd({ + detail: { + value: { + account: acct, + }, + }, + }); + flushAsynchronousOperations(); + assert.isTrue(element.$.entry.hasAttribute('hidden')); + }); + + test('enter text calls suggestions provider', done => { + const suggestions = [ + { + email: 'abc@example.com', + text: 'abcd', + }, + { + email: 'qwe@example.com', + text: 'qwer', + }, + ]; + const getSuggestionsStub = + sandbox.stub(suggestionsProvider, 'getSuggestions') + .returns(Promise.resolve(suggestions)); + + const makeSuggestionItemStub = + sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item); + + const input = element.$.entry.$.input; + + input.text = 'newTest'; + MockInteractions.focus(input.$.input); + input.noDebounce = true; + flushAsynchronousOperations(); + flush(() => { + assert.isTrue(getSuggestionsStub.calledOnce); + assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest'); + assert.equal(makeSuggestionItemStub.getCalls().length, 2); + done(); + }); + }); + + test('suggestion on empty', done => { + element.skipSuggestOnEmpty = false; + const suggestions = [ + { + email: 'abc@example.com', + text: 'abcd', + }, + { + email: 'qwe@example.com', + text: 'qwer', + }, + ]; + const getSuggestionsStub = + sandbox.stub(suggestionsProvider, 'getSuggestions') + .returns(Promise.resolve(suggestions)); + + const makeSuggestionItemStub = + sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item); + + const input = element.$.entry.$.input; + + input.text = ''; + MockInteractions.focus(input.$.input); + input.noDebounce = true; + flushAsynchronousOperations(); + flush(() => { + assert.isTrue(getSuggestionsStub.calledOnce); + assert.equal(getSuggestionsStub.lastCall.args[0], ''); + assert.equal(makeSuggestionItemStub.getCalls().length, 2); + done(); + }); + }); + + test('skip suggestion on empty', done => { + element.skipSuggestOnEmpty = true; + const getSuggestionsStub = + sandbox.stub(suggestionsProvider, 'getSuggestions') + .returns(Promise.resolve([])); + + const input = element.$.entry.$.input; + + input.text = ''; + MockInteractions.focus(input.$.input); + input.noDebounce = true; + flushAsynchronousOperations(); + flush(() => { + assert.isTrue(getSuggestionsStub.notCalled); + done(); + }); + }); + + suite('allowAnyInput', () => { setup(() => { - sandbox = sinon.sandbox.create(); - existingAccount1 = makeAccount(); - existingAccount2 = makeAccount(); - - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - element.accounts = [existingAccount1, existingAccount2]; - suggestionsProvider = new MockSuggestionsProvider(); - element.suggestionsProvider = suggestionsProvider; - }); - - teardown(() => { - sandbox.restore(); - }); - - test('account entry only appears when editable', () => { - element.readonly = false; - assert.isFalse(element.$.entry.hasAttribute('hidden')); - element.readonly = true; - assert.isTrue(element.$.entry.hasAttribute('hidden')); - }); - - test('addition and removal of account/group chips', () => { - flushAsynchronousOperations(); - sandbox.stub(element, '_computeRemovable').returns(true); - // Existing accounts are listed. - let chips = getChips(); - assert.equal(chips.length, 2); - assert.isFalse(chips[0].classList.contains('pendingAdd')); - assert.isFalse(chips[1].classList.contains('pendingAdd')); - - // New accounts are added to end with pendingAdd class. - const newAccount = makeAccount(); - element._handleAdd({ - detail: { - value: { - account: newAccount, - }, - }, - }); - flushAsynchronousOperations(); - chips = getChips(); - assert.equal(chips.length, 3); - assert.isFalse(chips[0].classList.contains('pendingAdd')); - assert.isFalse(chips[1].classList.contains('pendingAdd')); - assert.isTrue(chips[2].classList.contains('pendingAdd')); - - // Removed accounts are taken out of the list. - element.fire('remove', {account: existingAccount1}); - flushAsynchronousOperations(); - chips = getChips(); - assert.equal(chips.length, 2); - assert.isFalse(chips[0].classList.contains('pendingAdd')); - assert.isTrue(chips[1].classList.contains('pendingAdd')); - - // Invalid remove is ignored. - element.fire('remove', {account: existingAccount1}); - element.fire('remove', {account: newAccount}); - flushAsynchronousOperations(); - chips = getChips(); - assert.equal(chips.length, 1); - assert.isFalse(chips[0].classList.contains('pendingAdd')); - - // New groups are added to end with pendingAdd and group classes. - const newGroup = makeGroup(); - element._handleAdd({ - detail: { - value: { - group: newGroup, - }, - }, - }); - flushAsynchronousOperations(); - chips = getChips(); - assert.equal(chips.length, 2); - assert.isTrue(chips[1].classList.contains('group')); - assert.isTrue(chips[1].classList.contains('pendingAdd')); - - // Removed groups are taken out of the list. - element.fire('remove', {account: newGroup}); - flushAsynchronousOperations(); - chips = getChips(); - assert.equal(chips.length, 1); - assert.isFalse(chips[0].classList.contains('pendingAdd')); - }); - - test('_getSuggestions uses filter correctly', done => { - const originalSuggestions = [ - { - email: 'abc@example.com', - text: 'abcd', - _account_id: 3, - }, - { - email: 'qwe@example.com', - text: 'qwer', - _account_id: 1, - }, - { - email: 'xyz@example.com', - text: 'aaaaa', - _account_id: 25, - }, - ]; - sandbox.stub(suggestionsProvider, 'getSuggestions') - .returns(Promise.resolve(originalSuggestions)); - sandbox.stub(suggestionsProvider, 'makeSuggestionItem', suggestion => { - return { - name: suggestion.email, - value: suggestion._account_id, - }; - }); - - element._getSuggestions().then(suggestions => { - // Default is no filtering. - assert.equal(suggestions.length, 3); - - // Set up filter that only accepts suggestion1. - const accountId = originalSuggestions[0]._account_id; - element.filter = function(suggestion) { - return suggestion._account_id === accountId; - }; - - element._getSuggestions() - .then(suggestions => { - assert.deepEqual(suggestions, - [{name: originalSuggestions[0].email, - value: originalSuggestions[0]._account_id}]); - }) - .then(done); - }); - }); - - test('_computeChipClass', () => { - const account = makeAccount(); - assert.equal(element._computeChipClass(account), ''); - account._pendingAdd = true; - assert.equal(element._computeChipClass(account), 'pendingAdd'); - account._group = true; - assert.equal(element._computeChipClass(account), 'group pendingAdd'); - account._pendingAdd = false; - assert.equal(element._computeChipClass(account), 'group'); - }); - - test('_computeRemovable', () => { - const newAccount = makeAccount(); - newAccount._pendingAdd = true; - element.readonly = false; - element.removableValues = []; - assert.isFalse(element._computeRemovable(existingAccount1, false)); - assert.isTrue(element._computeRemovable(newAccount, false)); - - element.removableValues = [existingAccount1]; - assert.isTrue(element._computeRemovable(existingAccount1, false)); - assert.isTrue(element._computeRemovable(newAccount, false)); - assert.isFalse(element._computeRemovable(existingAccount2, false)); - - element.readonly = true; - assert.isFalse(element._computeRemovable(existingAccount1, true)); - assert.isFalse(element._computeRemovable(newAccount, true)); - }); - - test('submitEntryText', () => { element.allowAnyInput = true; - flushAsynchronousOperations(); - - const getTextStub = sandbox.stub(element.$.entry, 'getText'); - getTextStub.onFirstCall().returns(''); - getTextStub.onSecondCall().returns('test'); - getTextStub.onThirdCall().returns('test@test'); - - // When entry is empty, return true. - const clearStub = sandbox.stub(element.$.entry, 'clear'); - assert.isTrue(element.submitEntryText()); - assert.isFalse(clearStub.called); - - // When entry is invalid, return false. - assert.isFalse(element.submitEntryText()); - assert.isFalse(clearStub.called); - - // When entry is valid, return true and clear text. - assert.isTrue(element.submitEntryText()); - assert.isTrue(clearStub.called); - assert.equal(element.additions()[0].account.email, 'test@test'); }); - test('additions returns sanitized new accounts and groups', () => { - assert.equal(element.additions().length, 0); - - const newAccount = makeAccount(); - element._handleAdd({ - detail: { - value: { - account: newAccount, - }, - }, - }); - const newGroup = makeGroup(); - element._handleAdd({ - detail: { - value: { - group: newGroup, - }, - }, - }); - - assert.deepEqual(element.additions(), [ - { - account: { - _account_id: newAccount._account_id, - _pendingAdd: true, - }, - }, - { - group: { - id: newGroup.id, - _group: true, - _pendingAdd: true, - }, - }, - ]); + test('adds emails', () => { + const accountLen = element.accounts.length; + element._handleAdd({detail: {value: 'test@test'}}); + assert.equal(element.accounts.length, accountLen + 1); + assert.equal(element.accounts[accountLen].email, 'test@test'); }); - test('large group confirmations', () => { - assert.isNull(element.pendingConfirmation); - assert.deepEqual(element.additions(), []); - - const group = makeGroup(); - const reviewer = { - group, - count: 10, - confirm: true, - }; - element._handleAdd({ - detail: { - value: reviewer, - }, - }); - - assert.deepEqual(element.pendingConfirmation, reviewer); - assert.deepEqual(element.additions(), []); - - element.confirmGroup(group); - assert.isNull(element.pendingConfirmation); - assert.deepEqual(element.additions(), [ - { - group: { - id: group.id, - _group: true, - _pendingAdd: true, - confirmed: true, - }, - }, - ]); + test('toasts on invalid email', () => { + const toastHandler = sandbox.stub(); + element.addEventListener('show-alert', toastHandler); + element._handleAdd({detail: {value: 'test'}}); + assert.isTrue(toastHandler.called); }); + }); - test('removeAccount fails if account is not removable', () => { - element.readonly = true; - const acct = makeAccount(); - element.accounts = [acct]; - element._removeAccount(acct); - assert.equal(element.accounts.length, 1); - }); + test('_accountMatches', () => { + const acct = makeAccount(); - test('max-count', () => { - element.maxCount = 1; - const acct = makeAccount(); - element._handleAdd({ - detail: { - value: { - account: acct, - }, - }, - }); - flushAsynchronousOperations(); - assert.isTrue(element.$.entry.hasAttribute('hidden')); - }); + assert.isTrue(element._accountMatches(acct, acct)); + acct.email = 'test'; + assert.isTrue(element._accountMatches(acct, acct)); + assert.isTrue(element._accountMatches({email: 'test'}, acct)); - test('enter text calls suggestions provider', done => { - const suggestions = [ - { - email: 'abc@example.com', - text: 'abcd', - }, - { - email: 'qwe@example.com', - text: 'qwer', - }, - ]; - const getSuggestionsStub = - sandbox.stub(suggestionsProvider, 'getSuggestions') - .returns(Promise.resolve(suggestions)); + assert.isFalse(element._accountMatches({}, acct)); + assert.isFalse(element._accountMatches({email: 'test2'}, acct)); + assert.isFalse(element._accountMatches({_account_id: -1}, acct)); + }); - const makeSuggestionItemStub = - sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item); - + suite('keyboard interactions', () => { + test('backspace at text input start removes last account', done => { const input = element.$.entry.$.input; - - input.text = 'newTest'; - MockInteractions.focus(input.$.input); - input.noDebounce = true; - flushAsynchronousOperations(); + sandbox.stub(input, '_updateSuggestions'); + sandbox.stub(element, '_computeRemovable').returns(true); flush(() => { - assert.isTrue(getSuggestionsStub.calledOnce); - assert.equal(getSuggestionsStub.lastCall.args[0], 'newTest'); - assert.equal(makeSuggestionItemStub.getCalls().length, 2); - done(); - }); - }); - - test('suggestion on empty', done => { - element.skipSuggestOnEmpty = false; - const suggestions = [ - { - email: 'abc@example.com', - text: 'abcd', - }, - { - email: 'qwe@example.com', - text: 'qwer', - }, - ]; - const getSuggestionsStub = - sandbox.stub(suggestionsProvider, 'getSuggestions') - .returns(Promise.resolve(suggestions)); - - const makeSuggestionItemStub = - sandbox.stub(suggestionsProvider, 'makeSuggestionItem', item => item); - - const input = element.$.entry.$.input; - - input.text = ''; - MockInteractions.focus(input.$.input); - input.noDebounce = true; - flushAsynchronousOperations(); - flush(() => { - assert.isTrue(getSuggestionsStub.calledOnce); - assert.equal(getSuggestionsStub.lastCall.args[0], ''); - assert.equal(makeSuggestionItemStub.getCalls().length, 2); - done(); - }); - }); - - test('skip suggestion on empty', done => { - element.skipSuggestOnEmpty = true; - const getSuggestionsStub = - sandbox.stub(suggestionsProvider, 'getSuggestions') - .returns(Promise.resolve([])); - - const input = element.$.entry.$.input; - - input.text = ''; - MockInteractions.focus(input.$.input); - input.noDebounce = true; - flushAsynchronousOperations(); - flush(() => { - assert.isTrue(getSuggestionsStub.notCalled); - done(); - }); - }); - - suite('allowAnyInput', () => { - setup(() => { - element.allowAnyInput = true; - }); - - test('adds emails', () => { - const accountLen = element.accounts.length; - element._handleAdd({detail: {value: 'test@test'}}); - assert.equal(element.accounts.length, accountLen + 1); - assert.equal(element.accounts[accountLen].email, 'test@test'); - }); - - test('toasts on invalid email', () => { - const toastHandler = sandbox.stub(); - element.addEventListener('show-alert', toastHandler); - element._handleAdd({detail: {value: 'test'}}); - assert.isTrue(toastHandler.called); - }); - }); - - test('_accountMatches', () => { - const acct = makeAccount(); - - assert.isTrue(element._accountMatches(acct, acct)); - acct.email = 'test'; - assert.isTrue(element._accountMatches(acct, acct)); - assert.isTrue(element._accountMatches({email: 'test'}, acct)); - - assert.isFalse(element._accountMatches({}, acct)); - assert.isFalse(element._accountMatches({email: 'test2'}, acct)); - assert.isFalse(element._accountMatches({_account_id: -1}, acct)); - }); - - suite('keyboard interactions', () => { - test('backspace at text input start removes last account', done => { - const input = element.$.entry.$.input; - sandbox.stub(input, '_updateSuggestions'); - sandbox.stub(element, '_computeRemovable').returns(true); - flush(() => { - // Next line is a workaround for Firefix not moving cursor - // on input field update - assert.equal( - element._getNativeInput(input.$.input).selectionStart, 0); - input.text = 'test'; - MockInteractions.focus(input.$.input); - flushAsynchronousOperations(); - assert.equal(element.accounts.length, 2); - MockInteractions.pressAndReleaseKeyOn( - element._getNativeInput(input.$.input), 8); // Backspace - assert.equal(element.accounts.length, 2); - input.text = ''; - MockInteractions.pressAndReleaseKeyOn( - element._getNativeInput(input.$.input), 8); // Backspace - flushAsynchronousOperations(); - assert.equal(element.accounts.length, 1); - done(); - }); - }); - - test('arrow key navigation', done => { - const input = element.$.entry.$.input; + // Next line is a workaround for Firefix not moving cursor + // on input field update + assert.equal( + element._getNativeInput(input.$.input).selectionStart, 0); + input.text = 'test'; + MockInteractions.focus(input.$.input); + flushAsynchronousOperations(); + assert.equal(element.accounts.length, 2); + MockInteractions.pressAndReleaseKeyOn( + element._getNativeInput(input.$.input), 8); // Backspace + assert.equal(element.accounts.length, 2); input.text = ''; - element.accounts = [makeAccount(), makeAccount()]; - flush(() => { - MockInteractions.focus(input.$.input); - flushAsynchronousOperations(); - const chips = element.accountChips; - const chipsOneSpy = sandbox.spy(chips[1], 'focus'); - MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left - assert.isTrue(chipsOneSpy.called); - const chipsZeroSpy = sandbox.spy(chips[0], 'focus'); - MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left - assert.isTrue(chipsZeroSpy.called); - MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left - assert.isTrue(chipsZeroSpy.calledOnce); - MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right - assert.isTrue(chipsOneSpy.calledTwice); - done(); - }); + MockInteractions.pressAndReleaseKeyOn( + element._getNativeInput(input.$.input), 8); // Backspace + flushAsynchronousOperations(); + assert.equal(element.accounts.length, 1); + done(); }); + }); - test('delete', done => { - element.accounts = [makeAccount(), makeAccount()]; - flush(() => { - const focusSpy = sandbox.spy(element.accountChips[1], 'focus'); - const removeSpy = sandbox.spy(element, '_removeAccount'); - MockInteractions.pressAndReleaseKeyOn( - element.accountChips[0], 8); // Backspace - assert.isTrue(focusSpy.called); - assert.isTrue(removeSpy.calledOnce); + test('arrow key navigation', done => { + const input = element.$.entry.$.input; + input.text = ''; + element.accounts = [makeAccount(), makeAccount()]; + flush(() => { + MockInteractions.focus(input.$.input); + flushAsynchronousOperations(); + const chips = element.accountChips; + const chipsOneSpy = sandbox.spy(chips[1], 'focus'); + MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left + assert.isTrue(chipsOneSpy.called); + const chipsZeroSpy = sandbox.spy(chips[0], 'focus'); + MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left + assert.isTrue(chipsZeroSpy.called); + MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left + assert.isTrue(chipsZeroSpy.calledOnce); + MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right + assert.isTrue(chipsOneSpy.calledTwice); + done(); + }); + }); - MockInteractions.pressAndReleaseKeyOn( - element.accountChips[1], 46); // Delete - assert.isTrue(removeSpy.calledTwice); - done(); - }); + test('delete', done => { + element.accounts = [makeAccount(), makeAccount()]; + flush(() => { + const focusSpy = sandbox.spy(element.accountChips[1], 'focus'); + const removeSpy = sandbox.spy(element, '_removeAccount'); + MockInteractions.pressAndReleaseKeyOn( + element.accountChips[0], 8); // Backspace + assert.isTrue(focusSpy.called); + assert.isTrue(removeSpy.calledOnce); + + MockInteractions.pressAndReleaseKeyOn( + element.accountChips[1], 46); // Delete + assert.isTrue(removeSpy.calledTwice); + done(); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js index 6a0769d..dc8eea3 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert.js
@@ -14,94 +14,102 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrAlert extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-alert'; } - /** - * Fired when the action button is pressed. - * - * @event action - */ +import '../gr-button/gr-button.js'; +import '../../../styles/shared-styles.js'; +import '../../../scripts/rootElement.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-alert_html.js'; - static get properties() { - return { - text: String, - actionText: String, - /** @type {?string} */ - type: String, - shown: { - type: Boolean, - value: true, - readOnly: true, - reflectToAttribute: true, - }, - toast: { - type: Boolean, - value: true, - reflectToAttribute: true, - }, +/** @extends Polymer.Element */ +class GrAlert extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _hideActionButton: Boolean, - _boundTransitionEndHandler: { - type: Function, - value() { return this._handleTransitionEnd.bind(this); }, - }, - _actionCallback: Function, - }; - } + static get is() { return 'gr-alert'; } + /** + * Fired when the action button is pressed. + * + * @event action + */ - /** @override */ - attached() { - super.attached(); - this.addEventListener('transitionend', this._boundTransitionEndHandler); - } + static get properties() { + return { + text: String, + actionText: String, + /** @type {?string} */ + type: String, + shown: { + type: Boolean, + value: true, + readOnly: true, + reflectToAttribute: true, + }, + toast: { + type: Boolean, + value: true, + reflectToAttribute: true, + }, - /** @override */ - detached() { - super.detached(); - this.removeEventListener('transitionend', - this._boundTransitionEndHandler); - } + _hideActionButton: Boolean, + _boundTransitionEndHandler: { + type: Function, + value() { return this._handleTransitionEnd.bind(this); }, + }, + _actionCallback: Function, + }; + } - show(text, opt_actionText, opt_actionCallback) { - this.text = text; - this.actionText = opt_actionText; - this._hideActionButton = !opt_actionText; - this._actionCallback = opt_actionCallback; - Gerrit.getRootElement().appendChild(this); - this._setShown(true); - } + /** @override */ + attached() { + super.attached(); + this.addEventListener('transitionend', this._boundTransitionEndHandler); + } - hide() { - this._setShown(false); - if (this._hasZeroTransitionDuration()) { - Gerrit.getRootElement().removeChild(this); - } - } + /** @override */ + detached() { + super.detached(); + this.removeEventListener('transitionend', + this._boundTransitionEndHandler); + } - _hasZeroTransitionDuration() { - const style = window.getComputedStyle(this); - // transitionDuration is always given in seconds. - const duration = Math.round(parseFloat(style.transitionDuration) * 100); - return duration === 0; - } + show(text, opt_actionText, opt_actionCallback) { + this.text = text; + this.actionText = opt_actionText; + this._hideActionButton = !opt_actionText; + this._actionCallback = opt_actionCallback; + Gerrit.getRootElement().appendChild(this); + this._setShown(true); + } - _handleTransitionEnd(e) { - if (this.shown) { return; } - + hide() { + this._setShown(false); + if (this._hasZeroTransitionDuration()) { Gerrit.getRootElement().removeChild(this); } - - _handleActionTap(e) { - e.preventDefault(); - if (this._actionCallback) { this._actionCallback(); } - } } - customElements.define(GrAlert.is, GrAlert); -})(); + _hasZeroTransitionDuration() { + const style = window.getComputedStyle(this); + // transitionDuration is always given in seconds. + const duration = Math.round(parseFloat(style.transitionDuration) * 100); + return duration === 0; + } + + _handleTransitionEnd(e) { + if (this.shown) { return; } + + Gerrit.getRootElement().removeChild(this); + } + + _handleActionTap(e) { + e.preventDefault(); + if (this._actionCallback) { this._actionCallback(); } + } +} + +customElements.define(GrAlert.is, GrAlert);
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js index 0d44164..1190516 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../gr-button/gr-button.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<script src="../../../scripts/rootElement.js"></script> - -<dom-module id="gr-alert"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /** * ALERT: DO NOT ADD TRANSITION PROPERTIES WITHOUT PROPERLY UNDERSTANDING @@ -48,7 +42,7 @@ * (as outside styles always win), .content-wrapper is introduced as a * wrapper around main content to have better encapsulation, styles that * may be affected by outside should be defined on it. - * In this case, `padding:0px` is defined in main.css for all elements + * In this case, \`padding:0px\` is defined in main.css for all elements * with the universal selector: *. */ .content-wrapper { @@ -74,13 +68,6 @@ </style> <div class="content-wrapper"> <span class="text">[[text]]</span> - <gr-button - link - class="action" - hidden$="[[_hideActionButton]]" - on-click="_handleActionTap">[[actionText]]</gr-button> + <gr-button link="" class="action" hidden\$="[[_hideActionButton]]" on-click="_handleActionTap">[[actionText]]</gr-button> </div> - </template> - <script src="gr-alert.js"></script> -</dom-module> - +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html index 68d782b..d291fc7 100644 --- a/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html +++ b/polygerrit-ui/app/elements/shared/gr-alert/gr-alert_test.html
@@ -19,43 +19,45 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-alert</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-alert.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-alert.js"></script> -<script> - suite('gr-alert tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-alert.js'; +suite('gr-alert tests', () => { + let element; - setup(() => { - element = document.createElement('gr-alert'); - }); - - teardown(() => { - if (element.parentNode) { - element.parentNode.removeChild(element); - } - }); - - test('show/hide', () => { - assert.isNull(element.parentNode); - element.show(); - assert.equal(element.parentNode, document.body); - element.updateStyles({'--gr-alert-transition-duration': '0ms'}); - element.hide(); - assert.isNull(element.parentNode); - }); - - test('action event', done => { - element.show(); - element._actionCallback = done; - MockInteractions.tap(element.shadowRoot - .querySelector('.action')); - }); + setup(() => { + element = document.createElement('gr-alert'); }); + + teardown(() => { + if (element.parentNode) { + element.parentNode.removeChild(element); + } + }); + + test('show/hide', () => { + assert.isNull(element.parentNode); + element.show(); + assert.equal(element.parentNode, document.body); + element.updateStyles({'--gr-alert-transition-duration': '0ms'}); + element.hide(); + assert.isNull(element.parentNode); + }); + + test('action event', done => { + element.show(); + element._actionCallback = done; + MockInteractions.tap(element.shadowRoot + .querySelector('.action')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js index 5ca95e1..813d45d 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.js
@@ -14,179 +14,193 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '@polymer/iron-dropdown/iron-dropdown.js'; +import {IronFitBehavior} from '@polymer/iron-fit-behavior/iron-fit-behavior.js'; +import '../gr-cursor-manager/gr-cursor-manager.js'; +import '../../../scripts/rootElement.js'; +import '../../../styles/shared-styles.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-autocomplete-dropdown_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Polymer.IronFitMixin + * @extends Polymer.Element + */ +class GrAutocompleteDropdown extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + IronFitBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-autocomplete-dropdown'; } + /** + * Fired when the dropdown is closed. + * + * @event dropdown-closed + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Polymer.IronFitMixin - * @extends Polymer.Element + * Fired when item is selected. + * + * @event item-selected */ - class GrAutocompleteDropdown extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Polymer.IronFitBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-autocomplete-dropdown'; } - /** - * Fired when the dropdown is closed. - * - * @event dropdown-closed - */ - /** - * Fired when item is selected. - * - * @event item-selected - */ + static get properties() { + return { + index: Number, + isHidden: { + type: Boolean, + value: true, + reflectToAttribute: true, + }, + verticalOffset: { + type: Number, + value: null, + }, + horizontalOffset: { + type: Number, + value: null, + }, + suggestions: { + type: Array, + value: () => [], + observer: '_resetCursorStops', + }, + _suggestionEls: Array, + }; + } - static get properties() { - return { - index: Number, - isHidden: { - type: Boolean, - value: true, - reflectToAttribute: true, - }, - verticalOffset: { - type: Number, - value: null, - }, - horizontalOffset: { - type: Number, - value: null, - }, - suggestions: { - type: Array, - value: () => [], - observer: '_resetCursorStops', - }, - _suggestionEls: Array, - }; - } + get keyBindings() { + return { + up: '_handleUp', + down: '_handleDown', + enter: '_handleEnter', + esc: '_handleEscape', + tab: '_handleTab', + }; + } - get keyBindings() { - return { - up: '_handleUp', - down: '_handleDown', - enter: '_handleEnter', - esc: '_handleEscape', - tab: '_handleTab', - }; - } + close() { + this.isHidden = true; + } - close() { - this.isHidden = true; - } + open() { + this.isHidden = false; + this._resetCursorStops(); + // Refit should run after we call Polymer.flush inside _resetCursorStops + this.refit(); + } - open() { - this.isHidden = false; - this._resetCursorStops(); - // Refit should run after we call Polymer.flush inside _resetCursorStops - this.refit(); - } + getCurrentText() { + return this.getCursorTarget().dataset.value; + } - getCurrentText() { - return this.getCursorTarget().dataset.value; - } - - _handleUp(e) { - if (!this.isHidden) { - e.preventDefault(); - e.stopPropagation(); - this.cursorUp(); - } - } - - _handleDown(e) { - if (!this.isHidden) { - e.preventDefault(); - e.stopPropagation(); - this.cursorDown(); - } - } - - cursorDown() { - if (!this.isHidden) { - this.$.cursor.next(); - } - } - - cursorUp() { - if (!this.isHidden) { - this.$.cursor.previous(); - } - } - - _handleTab(e) { + _handleUp(e) { + if (!this.isHidden) { e.preventDefault(); e.stopPropagation(); - this.fire('item-selected', { - trigger: 'tab', - selected: this.$.cursor.target, - }); - } - - _handleEnter(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('item-selected', { - trigger: 'enter', - selected: this.$.cursor.target, - }); - } - - _handleEscape() { - this._fireClose(); - this.close(); - } - - _handleClickItem(e) { - e.preventDefault(); - e.stopPropagation(); - let selected = e.target; - while (!selected.classList.contains('autocompleteOption')) { - if (!selected || selected === this) { return; } - selected = selected.parentElement; - } - this.fire('item-selected', { - trigger: 'click', - selected, - }); - } - - _fireClose() { - this.fire('dropdown-closed'); - } - - getCursorTarget() { - return this.$.cursor.target; - } - - _resetCursorStops() { - if (this.suggestions.length > 0) { - if (!this.isHidden) { - Polymer.dom.flush(); - this._suggestionEls = Array.from( - this.$.suggestions.querySelectorAll('li')); - this._resetCursorIndex(); - } - } else { - this._suggestionEls = []; - } - } - - _resetCursorIndex() { - this.$.cursor.setCursorAtIndex(0); - } - - _computeLabelClass(item) { - return item.label ? '' : 'hide'; + this.cursorUp(); } } - customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown); -})(); + _handleDown(e) { + if (!this.isHidden) { + e.preventDefault(); + e.stopPropagation(); + this.cursorDown(); + } + } + + cursorDown() { + if (!this.isHidden) { + this.$.cursor.next(); + } + } + + cursorUp() { + if (!this.isHidden) { + this.$.cursor.previous(); + } + } + + _handleTab(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('item-selected', { + trigger: 'tab', + selected: this.$.cursor.target, + }); + } + + _handleEnter(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('item-selected', { + trigger: 'enter', + selected: this.$.cursor.target, + }); + } + + _handleEscape() { + this._fireClose(); + this.close(); + } + + _handleClickItem(e) { + e.preventDefault(); + e.stopPropagation(); + let selected = e.target; + while (!selected.classList.contains('autocompleteOption')) { + if (!selected || selected === this) { return; } + selected = selected.parentElement; + } + this.fire('item-selected', { + trigger: 'click', + selected, + }); + } + + _fireClose() { + this.fire('dropdown-closed'); + } + + getCursorTarget() { + return this.$.cursor.target; + } + + _resetCursorStops() { + if (this.suggestions.length > 0) { + if (!this.isHidden) { + flush(); + this._suggestionEls = Array.from( + this.$.suggestions.querySelectorAll('li')); + this._resetCursorIndex(); + } + } else { + this._suggestionEls = []; + } + } + + _resetCursorIndex() { + this.$.cursor.setCursorAtIndex(0); + } + + _computeLabelClass(item) { + return item.label ? '' : 'hide'; + } +} + +customElements.define(GrAutocompleteDropdown.is, GrAutocompleteDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js index 649cd22..711315d 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_html.js
@@ -1,32 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="/bower_components/iron-fit-behavior/iron-fit-behavior.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> -<script src="../../../scripts/rootElement.js"></script> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-autocomplete-dropdown"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { z-index: 100; @@ -76,33 +66,15 @@ display: none; } </style> - <div - class="dropdown-content" - slot="dropdown-content" - id="suggestions" - role="listbox"> + <div class="dropdown-content" slot="dropdown-content" id="suggestions" role="listbox"> <ul> <template is="dom-repeat" items="[[suggestions]]"> - <li data-index$="[[index]]" - data-value$="[[item.dataValue]]" - tabindex="-1" - aria-label$="[[item.name]]" - class="autocompleteOption" - role="option" - on-click="_handleClickItem"> + <li data-index\$="[[index]]" data-value\$="[[item.dataValue]]" tabindex="-1" aria-label\$="[[item.name]]" class="autocompleteOption" role="option" on-click="_handleClickItem"> <span>[[item.text]]</span> - <span class$="label [[_computeLabelClass(item)]]">[[item.label]]</span> + <span class\$="label [[_computeLabelClass(item)]]">[[item.label]]</span> </li> </template> </ul> </div> - <gr-cursor-manager - id="cursor" - index="{{index}}" - cursor-target-class="selected" - scroll-behavior="never" - focus-on-move - stops="[[_suggestionEls]]"></gr-cursor-manager> - </template> - <script src="gr-autocomplete-dropdown.js"></script> -</dom-module> + <gr-cursor-manager id="cursor" index="{{index}}" cursor-target-class="selected" scroll-behavior="never" focus-on-move="" stops="[[_suggestionEls]]"></gr-cursor-manager> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html index 9ea8259..a18fcac 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-autocomplete-dropdown</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-autocomplete-dropdown.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-autocomplete-dropdown.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-autocomplete-dropdown.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,124 +40,125 @@ </template> </test-fixture> -<script> - suite('gr-autocomplete-dropdown', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-autocomplete-dropdown.js'; +suite('gr-autocomplete-dropdown', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.open(); - element.suggestions = [ - {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'}, - {dataValue: 'test value 2', name: 'test name 2', text: 2}]; - flushAsynchronousOperations(); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.open(); + element.suggestions = [ + {dataValue: 'test value 1', name: 'test name 1', text: 1, label: 'hi'}, + {dataValue: 'test value 2', name: 'test name 2', text: 2}]; + flushAsynchronousOperations(); + }); - teardown(() => { - sandbox.restore(); - if (element.isOpen) element.close(); - }); + teardown(() => { + sandbox.restore(); + if (element.isOpen) element.close(); + }); - test('shows labels', () => { - const els = element.$.suggestions.querySelectorAll('li'); - assert.equal(els[0].innerText.trim(), '1\nhi'); - assert.equal(els[1].innerText.trim(), '2'); - }); + test('shows labels', () => { + const els = element.$.suggestions.querySelectorAll('li'); + assert.equal(els[0].innerText.trim(), '1\nhi'); + assert.equal(els[1].innerText.trim(), '2'); + }); - test('escape key', done => { - const closeSpy = sandbox.spy(element, 'close'); - MockInteractions.pressAndReleaseKeyOn(element, 27); - flushAsynchronousOperations(); - assert.isTrue(closeSpy.called); - done(); - }); + test('escape key', done => { + const closeSpy = sandbox.spy(element, 'close'); + MockInteractions.pressAndReleaseKeyOn(element, 27); + flushAsynchronousOperations(); + assert.isTrue(closeSpy.called); + done(); + }); - test('tab key', () => { - const handleTabSpy = sandbox.spy(element, '_handleTab'); - const itemSelectedStub = sandbox.stub(); - element.addEventListener('item-selected', itemSelectedStub); - MockInteractions.pressAndReleaseKeyOn(element, 9); - assert.isTrue(handleTabSpy.called); - assert.equal(element.$.cursor.index, 0); - assert.isTrue(itemSelectedStub.called); - assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { - trigger: 'tab', - selected: element.getCursorTarget(), - }); - }); - - test('enter key', () => { - const handleEnterSpy = sandbox.spy(element, '_handleEnter'); - const itemSelectedStub = sandbox.stub(); - element.addEventListener('item-selected', itemSelectedStub); - MockInteractions.pressAndReleaseKeyOn(element, 13); - assert.isTrue(handleEnterSpy.called); - assert.equal(element.$.cursor.index, 0); - assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { - trigger: 'enter', - selected: element.getCursorTarget(), - }); - }); - - test('down key', () => { - element.isHidden = true; - const nextSpy = sandbox.spy(element.$.cursor, 'next'); - MockInteractions.pressAndReleaseKeyOn(element, 40); - assert.isFalse(nextSpy.called); - assert.equal(element.$.cursor.index, 0); - element.isHidden = false; - MockInteractions.pressAndReleaseKeyOn(element, 40); - assert.isTrue(nextSpy.called); - assert.equal(element.$.cursor.index, 1); - }); - - test('up key', () => { - element.isHidden = true; - const prevSpy = sandbox.spy(element.$.cursor, 'previous'); - MockInteractions.pressAndReleaseKeyOn(element, 38); - assert.isFalse(prevSpy.called); - assert.equal(element.$.cursor.index, 0); - element.isHidden = false; - element.$.cursor.setCursorAtIndex(1); - assert.equal(element.$.cursor.index, 1); - MockInteractions.pressAndReleaseKeyOn(element, 38); - assert.isTrue(prevSpy.called); - assert.equal(element.$.cursor.index, 0); - }); - - test('tapping selects item', () => { - const itemSelectedStub = sandbox.stub(); - element.addEventListener('item-selected', itemSelectedStub); - - MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]); - flushAsynchronousOperations(); - assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { - trigger: 'click', - selected: element.$.suggestions.querySelectorAll('li')[1], - }); - }); - - test('tapping child still selects item', () => { - const itemSelectedStub = sandbox.stub(); - element.addEventListener('item-selected', itemSelectedStub); - - MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0] - .lastElementChild); - flushAsynchronousOperations(); - assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { - trigger: 'click', - selected: element.$.suggestions.querySelectorAll('li')[0], - }); - }); - - test('updated suggestions resets cursor stops', () => { - const resetStopsSpy = sandbox.spy(element, '_resetCursorStops'); - element.suggestions = []; - assert.isTrue(resetStopsSpy.called); + test('tab key', () => { + const handleTabSpy = sandbox.spy(element, '_handleTab'); + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + MockInteractions.pressAndReleaseKeyOn(element, 9); + assert.isTrue(handleTabSpy.called); + assert.equal(element.$.cursor.index, 0); + assert.isTrue(itemSelectedStub.called); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'tab', + selected: element.getCursorTarget(), }); }); + test('enter key', () => { + const handleEnterSpy = sandbox.spy(element, '_handleEnter'); + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + MockInteractions.pressAndReleaseKeyOn(element, 13); + assert.isTrue(handleEnterSpy.called); + assert.equal(element.$.cursor.index, 0); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'enter', + selected: element.getCursorTarget(), + }); + }); + + test('down key', () => { + element.isHidden = true; + const nextSpy = sandbox.spy(element.$.cursor, 'next'); + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isFalse(nextSpy.called); + assert.equal(element.$.cursor.index, 0); + element.isHidden = false; + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isTrue(nextSpy.called); + assert.equal(element.$.cursor.index, 1); + }); + + test('up key', () => { + element.isHidden = true; + const prevSpy = sandbox.spy(element.$.cursor, 'previous'); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isFalse(prevSpy.called); + assert.equal(element.$.cursor.index, 0); + element.isHidden = false; + element.$.cursor.setCursorAtIndex(1); + assert.equal(element.$.cursor.index, 1); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isTrue(prevSpy.called); + assert.equal(element.$.cursor.index, 0); + }); + + test('tapping selects item', () => { + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + + MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[1]); + flushAsynchronousOperations(); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'click', + selected: element.$.suggestions.querySelectorAll('li')[1], + }); + }); + + test('tapping child still selects item', () => { + const itemSelectedStub = sandbox.stub(); + element.addEventListener('item-selected', itemSelectedStub); + + MockInteractions.tap(element.$.suggestions.querySelectorAll('li')[0] + .lastElementChild); + flushAsynchronousOperations(); + assert.deepEqual(itemSelectedStub.lastCall.args[0].detail, { + trigger: 'click', + selected: element.$.suggestions.querySelectorAll('li')[0], + }); + }); + + test('updated suggestions resets cursor stops', () => { + const resetStopsSpy = sandbox.spy(element, '_resetCursorStops'); + element.suggestions = []; + assert.isTrue(resetStopsSpy.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js index 60985c1..22b62db 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete.js
@@ -14,449 +14,463 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; - const DEBOUNCE_WAIT_MS = 200; +import '@polymer/paper-input/paper-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js'; +import '../gr-cursor-manager/gr-cursor-manager.js'; +import '../gr-icons/gr-icons.js'; +import '../../../styles/shared-styles.js'; +import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-autocomplete_html.js'; + +const TOKENIZE_REGEX = /(?:[^\s"]+|"[^"]*")+/g; +const DEBOUNCE_WAIT_MS = 200; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrAutocomplete extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-autocomplete'; } + /** + * Fired when a value is chosen. + * + * @event commit + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when the user cancels. + * + * @event cancel */ - class GrAutocomplete extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-autocomplete'; } - /** - * Fired when a value is chosen. - * - * @event commit - */ - /** - * Fired when the user cancels. - * - * @event cancel - */ + /** + * Fired on keydown to allow for custom hooks into autocomplete textbox + * behavior. + * + * @event input-keydown + */ - /** - * Fired on keydown to allow for custom hooks into autocomplete textbox - * behavior. - * - * @event input-keydown - */ + static get properties() { + return { - static get properties() { - return { - - /** - * Query for requesting autocomplete suggestions. The function should - * accept the input as a string parameter and return a promise. The - * promise yields an array of suggestion objects with "name", "label", - * "value" properties. The "name" property will be displayed in the - * suggestion entry. The "label" property will, when specified, appear - * next to the "name" as label text. The "value" property will be emitted - * if that suggestion is selected. - * - * @type {function(string): Promise<?>} - */ - query: { - type: Function, - value() { - return function() { - return Promise.resolve([]); - }; - }, + /** + * Query for requesting autocomplete suggestions. The function should + * accept the input as a string parameter and return a promise. The + * promise yields an array of suggestion objects with "name", "label", + * "value" properties. The "name" property will be displayed in the + * suggestion entry. The "label" property will, when specified, appear + * next to the "name" as label text. The "value" property will be emitted + * if that suggestion is selected. + * + * @type {function(string): Promise<?>} + */ + query: { + type: Function, + value() { + return function() { + return Promise.resolve([]); + }; }, + }, - /** - * The number of characters that must be typed before suggestions are - * made. If threshold is zero, default suggestions are enabled. - */ - threshold: { - type: Number, - value: 1, - }, + /** + * The number of characters that must be typed before suggestions are + * made. If threshold is zero, default suggestions are enabled. + */ + threshold: { + type: Number, + value: 1, + }, - allowNonSuggestedValues: Boolean, - borderless: Boolean, - disabled: Boolean, - showSearchIcon: { - type: Boolean, - value: false, - }, - /** - * Vertical offset needed for an element with 20px line-height, 4px - * padding and 1px border (30px height total). Plus 1px spacing between - * input and dropdown. Inputs with different line-height or padding will - * need to tweak vertical offset. - */ - verticalOffset: { - type: Number, - value: 31, - }, + allowNonSuggestedValues: Boolean, + borderless: Boolean, + disabled: Boolean, + showSearchIcon: { + type: Boolean, + value: false, + }, + /** + * Vertical offset needed for an element with 20px line-height, 4px + * padding and 1px border (30px height total). Plus 1px spacing between + * input and dropdown. Inputs with different line-height or padding will + * need to tweak vertical offset. + */ + verticalOffset: { + type: Number, + value: 31, + }, - text: { - type: String, - value: '', - notify: true, - }, + text: { + type: String, + value: '', + notify: true, + }, - placeholder: String, + placeholder: String, - clearOnCommit: { - type: Boolean, - value: false, - }, + clearOnCommit: { + type: Boolean, + value: false, + }, - /** - * When true, tab key autocompletes but does not fire the commit event. - * When false, tab key not caught, and focus is removed from the element. - * See Issue 4556, Issue 6645. - */ - tabComplete: { - type: Boolean, - value: false, - }, + /** + * When true, tab key autocompletes but does not fire the commit event. + * When false, tab key not caught, and focus is removed from the element. + * See Issue 4556, Issue 6645. + */ + tabComplete: { + type: Boolean, + value: false, + }, - value: { - type: String, - notify: true, - }, + value: { + type: String, + notify: true, + }, - /** - * Multi mode appends autocompleted entries to the value. - * If false, autocompleted entries replace value. - */ - multi: { - type: Boolean, - value: false, - }, + /** + * Multi mode appends autocompleted entries to the value. + * If false, autocompleted entries replace value. + */ + multi: { + type: Boolean, + value: false, + }, - /** - * When true and uncommitted text is left in the autocomplete input after - * blurring, the text will appear red. - */ - warnUncommitted: { - type: Boolean, - value: false, - }, + /** + * When true and uncommitted text is left in the autocomplete input after + * blurring, the text will appear red. + */ + warnUncommitted: { + type: Boolean, + value: false, + }, - /** - * When true, querying for suggestions is not debounced w/r/t keypresses - */ - noDebounce: { - type: Boolean, - value: false, - }, + /** + * When true, querying for suggestions is not debounced w/r/t keypresses + */ + noDebounce: { + type: Boolean, + value: false, + }, - /** @type {?} */ - _suggestions: { - type: Array, - value() { return []; }, - }, + /** @type {?} */ + _suggestions: { + type: Array, + value() { return []; }, + }, - _suggestionEls: { - type: Array, - value() { return []; }, - }, + _suggestionEls: { + type: Array, + value() { return []; }, + }, - _index: Number, - _disableSuggestions: { - type: Boolean, - value: false, - }, - _focused: { - type: Boolean, - value: false, - }, + _index: Number, + _disableSuggestions: { + type: Boolean, + value: false, + }, + _focused: { + type: Boolean, + value: false, + }, - /** The DOM element of the selected suggestion. */ - _selected: Object, - }; + /** The DOM element of the selected suggestion. */ + _selected: Object, + }; + } + + static get observers() { + return [ + '_maybeOpenDropdown(_suggestions, _focused)', + '_updateSuggestions(text, threshold, noDebounce)', + ]; + } + + get _nativeInput() { + // In Polymer 2 inputElement isn't nativeInput anymore + return this.$.input.$.nativeInput || this.$.input.inputElement; + } + + /** @override */ + attached() { + super.attached(); + this.listen(document.body, 'click', '_handleBodyClick'); + } + + /** @override */ + detached() { + super.detached(); + this.unlisten(document.body, 'click', '_handleBodyClick'); + this.cancelDebouncer('update-suggestions'); + } + + get focusStart() { + return this.$.input; + } + + focus() { + this._nativeInput.focus(); + } + + selectAll() { + const nativeInputElement = this._nativeInput; + if (!this.$.input.value) { return; } + nativeInputElement.setSelectionRange(0, this.$.input.value.length); + } + + clear() { + this.text = ''; + } + + _handleItemSelect(e) { + // Let _handleKeydown deal with keyboard interaction. + if (e.detail.trigger !== 'click') { return; } + this._selected = e.detail.selected; + this._commit(); + } + + get _inputElement() { + // Polymer2: this.$ can be undefined when this is first evaluated. + return this.$ && this.$.input; + } + + /** + * Set the text of the input without triggering the suggestion dropdown. + * + * @param {string} text The new text for the input. + */ + setText(text) { + this._disableSuggestions = true; + this.text = text; + this._disableSuggestions = false; + } + + _onInputFocus() { + this._focused = true; + this._updateSuggestions(this.text, this.threshold, this.noDebounce); + this.$.input.classList.remove('warnUncommitted'); + // Needed so that --paper-input-container-input updated style is applied. + this.updateStyles(); + } + + _onInputBlur() { + this.$.input.classList.toggle('warnUncommitted', + this.warnUncommitted && this.text.length && !this._focused); + // Needed so that --paper-input-container-input updated style is applied. + this.updateStyles(); + } + + _updateSuggestions(text, threshold, noDebounce) { + // Polymer 2: check for undefined + if ([text, threshold, noDebounce].some(arg => arg === undefined)) { + return; } - static get observers() { - return [ - '_maybeOpenDropdown(_suggestions, _focused)', - '_updateSuggestions(text, threshold, noDebounce)', - ]; + // Reset _suggestions for every update + // This will also prevent from carrying over suggestions: + // @see Issue 12039 + this._suggestions = []; + + // TODO(taoalpha): Also skip if text has not changed + + if (this._disableSuggestions) { return; } + if (text.length < threshold) { + this.value = ''; + return; } - get _nativeInput() { - // In Polymer 2 inputElement isn't nativeInput anymore - return this.$.input.$.nativeInput || this.$.input.inputElement; + if (!this._focused) { + return; } - /** @override */ - attached() { - super.attached(); - this.listen(document.body, 'click', '_handleBodyClick'); - } - - /** @override */ - detached() { - super.detached(); - this.unlisten(document.body, 'click', '_handleBodyClick'); - this.cancelDebouncer('update-suggestions'); - } - - get focusStart() { - return this.$.input; - } - - focus() { - this._nativeInput.focus(); - } - - selectAll() { - const nativeInputElement = this._nativeInput; - if (!this.$.input.value) { return; } - nativeInputElement.setSelectionRange(0, this.$.input.value.length); - } - - clear() { - this.text = ''; - } - - _handleItemSelect(e) { - // Let _handleKeydown deal with keyboard interaction. - if (e.detail.trigger !== 'click') { return; } - this._selected = e.detail.selected; - this._commit(); - } - - get _inputElement() { - // Polymer2: this.$ can be undefined when this is first evaluated. - return this.$ && this.$.input; - } - - /** - * Set the text of the input without triggering the suggestion dropdown. - * - * @param {string} text The new text for the input. - */ - setText(text) { - this._disableSuggestions = true; - this.text = text; - this._disableSuggestions = false; - } - - _onInputFocus() { - this._focused = true; - this._updateSuggestions(this.text, this.threshold, this.noDebounce); - this.$.input.classList.remove('warnUncommitted'); - // Needed so that --paper-input-container-input updated style is applied. - this.updateStyles(); - } - - _onInputBlur() { - this.$.input.classList.toggle('warnUncommitted', - this.warnUncommitted && this.text.length && !this._focused); - // Needed so that --paper-input-container-input updated style is applied. - this.updateStyles(); - } - - _updateSuggestions(text, threshold, noDebounce) { - // Polymer 2: check for undefined - if ([text, threshold, noDebounce].some(arg => arg === undefined)) { - return; - } - - // Reset _suggestions for every update - // This will also prevent from carrying over suggestions: - // @see Issue 12039 - this._suggestions = []; - - // TODO(taoalpha): Also skip if text has not changed - - if (this._disableSuggestions) { return; } - if (text.length < threshold) { - this.value = ''; - return; - } - - if (!this._focused) { - return; - } - - const update = () => { - this.query(text).then(suggestions => { - if (text !== this.text) { - // Late response. - return; - } - for (const suggestion of suggestions) { - suggestion.text = suggestion.name; - } - this._suggestions = suggestions; - Polymer.dom.flush(); - if (this._index === -1) { - this.value = ''; - } - }); - }; - - if (noDebounce) { - update(); - } else { - this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS); - } - } - - _maybeOpenDropdown(suggestions, focused) { - if (suggestions.length > 0 && focused) { - return this.$.suggestions.open(); - } - return this.$.suggestions.close(); - } - - _computeClass(borderless) { - return borderless ? 'borderless' : ''; - } - - /** - * _handleKeydown used for key handling in the this.$.input AND all child - * autocomplete options. - */ - _handleKeydown(e) { - this._focused = true; - switch (e.keyCode) { - case 38: // Up - e.preventDefault(); - this.$.suggestions.cursorUp(); - break; - case 40: // Down - e.preventDefault(); - this.$.suggestions.cursorDown(); - break; - case 27: // Escape - e.preventDefault(); - this._cancel(); - break; - case 9: // Tab - if (this._suggestions.length > 0 && this.tabComplete) { - e.preventDefault(); - this._handleInputCommit(true); - this.focus(); - } else { - this._focused = false; - } - break; - case 13: // Enter - if (this.modifierPressed(e)) { break; } - e.preventDefault(); - this._handleInputCommit(); - break; - default: - // For any normal keypress, return focus to the input to allow for - // unbroken user input. - this.focus(); - - // Since this has been a normal keypress, the suggestions will have - // been based on a previous input. Clear them. This prevents an - // outdated suggestion from being used if the input keystroke is - // immediately followed by a commit keystroke. @see Issue 8655 - this._suggestions = []; - } - this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input}); - } - - _cancel() { - if (this._suggestions.length) { - this.set('_suggestions', []); - } else { - this.fire('cancel'); - } - } - - /** - * @param {boolean=} opt_tabComplete - */ - _handleInputCommit(opt_tabComplete) { - // Nothing to do if the dropdown is not open. - if (!this.allowNonSuggestedValues && - this.$.suggestions.isHidden) { return; } - - this._selected = this.$.suggestions.getCursorTarget(); - this._commit(opt_tabComplete); - } - - _updateValue(suggestion, suggestions) { - if (!suggestion) { return; } - const completed = suggestions[suggestion.dataset.index].value; - if (this.multi) { - // Append the completed text to the end of the string. - // Allow spaces within quoted terms. - const tokens = this.text.match(TOKENIZE_REGEX); - tokens[tokens.length - 1] = completed; - this.value = tokens.join(' '); - } else { - this.value = completed; - } - } - - _handleBodyClick(e) { - const eventPath = Polymer.dom(e).path; - for (let i = 0; i < eventPath.length; i++) { - if (eventPath[i] === this) { + const update = () => { + this.query(text).then(suggestions => { + if (text !== this.text) { + // Late response. return; } - } - this._focused = false; - } - - _handleSuggestionTap(e) { - e.stopPropagation(); - this.$.cursor.setCursor(e.target); - this._commit(); - } - - /** - * Commits the suggestion, optionally firing the commit event. - * - * @param {boolean=} opt_silent Allows for silent committing of an - * autocomplete suggestion in order to handle cases like tab-to-complete - * without firing the commit event. - */ - _commit(opt_silent) { - // Allow values that are not in suggestion list iff suggestions are empty. - if (this._suggestions.length > 0) { - this._updateValue(this._selected, this._suggestions); - } else { - this.value = this.text || ''; - } - - const value = this.value; - - // Value and text are mirrors of each other in multi mode. - if (this.multi) { - this.setText(this.value); - } else { - if (!this.clearOnCommit && this._selected) { - this.setText(this._suggestions[this._selected.dataset.index].name); - } else { - this.clear(); + for (const suggestion of suggestions) { + suggestion.text = suggestion.name; } - } + this._suggestions = suggestions; + flush(); + if (this._index === -1) { + this.value = ''; + } + }); + }; - this._suggestions = []; - if (!opt_silent) { - this.fire('commit', {value}); - } - - this._textChangedSinceCommit = false; - } - - _computeShowSearchIconClass(showSearchIcon) { - return showSearchIcon ? 'showSearchIcon' : ''; + if (noDebounce) { + update(); + } else { + this.debounce('update-suggestions', update, DEBOUNCE_WAIT_MS); } } - customElements.define(GrAutocomplete.is, GrAutocomplete); -})(); + _maybeOpenDropdown(suggestions, focused) { + if (suggestions.length > 0 && focused) { + return this.$.suggestions.open(); + } + return this.$.suggestions.close(); + } + + _computeClass(borderless) { + return borderless ? 'borderless' : ''; + } + + /** + * _handleKeydown used for key handling in the this.$.input AND all child + * autocomplete options. + */ + _handleKeydown(e) { + this._focused = true; + switch (e.keyCode) { + case 38: // Up + e.preventDefault(); + this.$.suggestions.cursorUp(); + break; + case 40: // Down + e.preventDefault(); + this.$.suggestions.cursorDown(); + break; + case 27: // Escape + e.preventDefault(); + this._cancel(); + break; + case 9: // Tab + if (this._suggestions.length > 0 && this.tabComplete) { + e.preventDefault(); + this._handleInputCommit(true); + this.focus(); + } else { + this._focused = false; + } + break; + case 13: // Enter + if (this.modifierPressed(e)) { break; } + e.preventDefault(); + this._handleInputCommit(); + break; + default: + // For any normal keypress, return focus to the input to allow for + // unbroken user input. + this.focus(); + + // Since this has been a normal keypress, the suggestions will have + // been based on a previous input. Clear them. This prevents an + // outdated suggestion from being used if the input keystroke is + // immediately followed by a commit keystroke. @see Issue 8655 + this._suggestions = []; + } + this.fire('input-keydown', {keyCode: e.keyCode, input: this.$.input}); + } + + _cancel() { + if (this._suggestions.length) { + this.set('_suggestions', []); + } else { + this.fire('cancel'); + } + } + + /** + * @param {boolean=} opt_tabComplete + */ + _handleInputCommit(opt_tabComplete) { + // Nothing to do if the dropdown is not open. + if (!this.allowNonSuggestedValues && + this.$.suggestions.isHidden) { return; } + + this._selected = this.$.suggestions.getCursorTarget(); + this._commit(opt_tabComplete); + } + + _updateValue(suggestion, suggestions) { + if (!suggestion) { return; } + const completed = suggestions[suggestion.dataset.index].value; + if (this.multi) { + // Append the completed text to the end of the string. + // Allow spaces within quoted terms. + const tokens = this.text.match(TOKENIZE_REGEX); + tokens[tokens.length - 1] = completed; + this.value = tokens.join(' '); + } else { + this.value = completed; + } + } + + _handleBodyClick(e) { + const eventPath = dom(e).path; + for (let i = 0; i < eventPath.length; i++) { + if (eventPath[i] === this) { + return; + } + } + this._focused = false; + } + + _handleSuggestionTap(e) { + e.stopPropagation(); + this.$.cursor.setCursor(e.target); + this._commit(); + } + + /** + * Commits the suggestion, optionally firing the commit event. + * + * @param {boolean=} opt_silent Allows for silent committing of an + * autocomplete suggestion in order to handle cases like tab-to-complete + * without firing the commit event. + */ + _commit(opt_silent) { + // Allow values that are not in suggestion list iff suggestions are empty. + if (this._suggestions.length > 0) { + this._updateValue(this._selected, this._suggestions); + } else { + this.value = this.text || ''; + } + + const value = this.value; + + // Value and text are mirrors of each other in multi mode. + if (this.multi) { + this.setText(this.value); + } else { + if (!this.clearOnCommit && this._selected) { + this.setText(this._suggestions[this._selected.dataset.index].name); + } else { + this.clear(); + } + } + + this._suggestions = []; + if (!opt_silent) { + this.fire('commit', {value}); + } + + this._textChangedSinceCommit = false; + } + + _computeShowSearchIconClass(showSearchIcon) { + return showSearchIcon ? 'showSearchIcon' : ''; + } +} + +customElements.define(GrAutocomplete.is, GrAutocomplete);
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js index 381ef0b..11726d9 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/paper-input/paper-input.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-autocomplete"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .searchIcon { display: none; @@ -82,38 +74,14 @@ } } </style> - <paper-input - no-label-float - id="input" - class$="[[_computeClass(borderless)]]" - disabled$="[[disabled]]" - value="{{text}}" - placeholder="[[placeholder]]" - on-keydown="_handleKeydown" - on-focus="_onInputFocus" - on-blur="_onInputBlur" - autocomplete="off"> + <paper-input no-label-float="" id="input" class\$="[[_computeClass(borderless)]]" disabled\$="[[disabled]]" value="{{text}}" placeholder="[[placeholder]]" on-keydown="_handleKeydown" on-focus="_onInputFocus" on-blur="_onInputBlur" autocomplete="off"> <!-- prefix as attribute is required to for polymer 1 --> - <div slot="prefix" prefix> - <iron-icon - icon="gr-icons:search" - class$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]"> + <div slot="prefix" prefix=""> + <iron-icon icon="gr-icons:search" class\$="searchIcon [[_computeShowSearchIconClass(showSearchIcon)]]"> </iron-icon> </div> </paper-input> - <gr-autocomplete-dropdown - vertical-align="top" - vertical-offset="[[verticalOffset]]" - horizontal-align="left" - id="suggestions" - on-item-selected="_handleItemSelect" - on-keydown="_handleKeydown" - suggestions="[[_suggestions]]" - role="listbox" - index="[[_index]]" - position-target="[[_inputElement]]"> + <gr-autocomplete-dropdown vertical-align="top" vertical-offset="[[verticalOffset]]" horizontal-align="left" id="suggestions" on-item-selected="_handleItemSelect" on-keydown="_handleKeydown" suggestions="[[_suggestions]]" role="listbox" index="[[_index]]" position-target="[[_inputElement]]"> </gr-autocomplete-dropdown> - </template> - <script src="gr-autocomplete.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html index 295572f..8a8f2f5 100644 --- a/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html +++ b/polygerrit-ui/app/elements/shared/gr-autocomplete/gr-autocomplete_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-autocomplete.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-autocomplete.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-autocomplete.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,580 +40,583 @@ </template> </test-fixture> -<script> - suite('gr-autocomplete tests', async () => { - await readyToTest(); - let element; - let sandbox; - const focusOnInput = element => { +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-autocomplete.js'; +import {dom, flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-autocomplete tests', () => { + let element; + let sandbox; + const focusOnInput = element => { + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + }; + + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('renders', () => { + let promise; + const queryStub = sandbox.spy(input => promise = Promise.resolve([ + {name: input + ' 0', value: 0}, + {name: input + ' 1', value: 1}, + {name: input + ' 2', value: 2}, + {name: input + ' 3', value: 3}, + {name: input + ' 4', value: 4}, + ])); + element.query = queryStub; + assert.isTrue(element.$.suggestions.isHidden); + assert.equal(element.$.suggestions.$.cursor.index, -1); + + focusOnInput(element); + element.text = 'blah'; + + assert.isTrue(queryStub.called); + element._focused = true; + + return promise.then(() => { + assert.isFalse(element.$.suggestions.isHidden); + const suggestions = + dom(element.$.suggestions.root).querySelectorAll('li'); + assert.equal(suggestions.length, 5); + + for (let i = 0; i < 5; i++) { + assert.equal(suggestions[i].innerText.trim(), 'blah ' + i); + } + + assert.notEqual(element.$.suggestions.$.cursor.index, -1); + }); + }); + + test('selectAll', done => { + flush(() => { + const nativeInput = element._nativeInput; + const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange'); + + element.selectAll(); + assert.isFalse(selectionStub.called); + + element.$.input.value = 'test'; + element.selectAll(); + assert.isTrue(selectionStub.called); + done(); + }); + }); + + test('esc key behavior', done => { + let promise; + const queryStub = sandbox.spy(() => promise = Promise.resolve([ + {name: 'blah', value: 123}, + ])); + element.query = queryStub; + + assert.isTrue(element.$.suggestions.isHidden); + + element._focused = true; + element.text = 'blah'; + + promise.then(() => { + assert.isFalse(element.$.suggestions.isHidden); + + const cancelHandler = sandbox.spy(); + element.addEventListener('cancel', cancelHandler); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); + assert.isFalse(cancelHandler.called); + assert.isTrue(element.$.suggestions.isHidden); + assert.equal(element._suggestions.length, 0); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); + assert.isTrue(cancelHandler.called); + done(); + }); + }); + + test('emits commit and handles cursor movement', done => { + let promise; + const queryStub = sandbox.spy(input => promise = Promise.resolve([ + {name: input + ' 0', value: 0}, + {name: input + ' 1', value: 1}, + {name: input + ' 2', value: 2}, + {name: input + ' 3', value: 3}, + {name: input + ' 4', value: 4}, + ])); + element.query = queryStub; + + assert.isTrue(element.$.suggestions.isHidden); + assert.equal(element.$.suggestions.$.cursor.index, -1); + element._focused = true; + element.text = 'blah'; + + promise.then(() => { + assert.isFalse(element.$.suggestions.isHidden); + + const commitHandler = sandbox.spy(); + element.addEventListener('commit', commitHandler); + + assert.equal(element.$.suggestions.$.cursor.index, 0); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, + 'down'); + + assert.equal(element.$.suggestions.$.cursor.index, 1); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, + 'down'); + + assert.equal(element.$.suggestions.$.cursor.index, 2); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up'); + + assert.equal(element.$.suggestions.$.cursor.index, 1); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, 'enter'); - }; + + assert.equal(element.value, 1); + assert.isTrue(commitHandler.called); + assert.equal(commitHandler.getCall(0).args[0].detail.value, 1); + assert.isTrue(element.$.suggestions.isHidden); + assert.isTrue(element._focused); + done(); + }); + }); + + test('clear-on-commit behavior (off)', done => { + let promise; + const queryStub = sandbox.spy(() => { + promise = Promise.resolve([{name: 'suggestion', value: 0}]); + return promise; + }); + element.query = queryStub; + focusOnInput(element); + element.text = 'blah'; + + promise.then(() => { + const commitHandler = sandbox.spy(); + element.addEventListener('commit', commitHandler); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + + assert.isTrue(commitHandler.called); + assert.equal(element.text, 'suggestion'); + done(); + }); + }); + + test('clear-on-commit behavior (on)', done => { + let promise; + const queryStub = sandbox.spy(() => { + promise = Promise.resolve([{name: 'suggestion', value: 0}]); + return promise; + }); + element.query = queryStub; + focusOnInput(element); + element.text = 'blah'; + element.clearOnCommit = true; + + promise.then(() => { + const commitHandler = sandbox.spy(); + element.addEventListener('commit', commitHandler); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + + assert.isTrue(commitHandler.called); + assert.equal(element.text, ''); + done(); + }); + }); + + test('threshold guards the query', () => { + const queryStub = sandbox.spy(() => Promise.resolve([])); + element.query = queryStub; + element.threshold = 2; + focusOnInput(element); + element.text = 'a'; + assert.isFalse(queryStub.called); + element.text = 'ab'; + assert.isTrue(queryStub.called); + }); + + test('noDebounce=false debounces the query', () => { + const queryStub = sandbox.spy(() => Promise.resolve([])); + let callback; + const debounceStub = sandbox.stub(element, 'debounce', + (name, cb) => { callback = cb; }); + element.query = queryStub; + element.noDebounce = false; + focusOnInput(element); + element.text = 'a'; + assert.isFalse(queryStub.called); + assert.isTrue(debounceStub.called); + assert.equal(debounceStub.lastCall.args[2], 200); + assert.isFunction(callback); + callback(); + assert.isTrue(queryStub.called); + }); + + test('_computeClass respects border property', () => { + assert.equal(element._computeClass(), ''); + assert.equal(element._computeClass(false), ''); + assert.equal(element._computeClass(true), 'borderless'); + }); + + test('undefined or empty text results in no suggestions', () => { + element._updateSuggestions(undefined, 0, null); + assert.equal(element._suggestions.length, 0); + }); + + test('when focused', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([ + {name: 'suggestion', value: 0}, + ])); + element.query = queryStub; + element.suggestOnlyWhenFocus = true; + focusOnInput(element); + element.text = 'bla'; + assert.equal(element._focused, true); + flushAsynchronousOperations(); + promise.then(() => { + assert.equal(element._suggestions.length, 1); + assert.equal(queryStub.notCalled, false); + done(); + }); + }); + + test('when not focused', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([ + {name: 'suggestion', value: 0}, + ])); + element.query = queryStub; + element.suggestOnlyWhenFocus = true; + element.text = 'bla'; + assert.equal(element._focused, false); + flushAsynchronousOperations(); + promise.then(() => { + assert.equal(element._suggestions.length, 0); + done(); + }); + }); + + test('suggestions should not carry over', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([ + {name: 'suggestion', value: 0}, + ])); + element.query = queryStub; + focusOnInput(element); + element.text = 'bla'; + flushAsynchronousOperations(); + promise.then(() => { + assert.equal(element._suggestions.length, 1); + element._updateSuggestions('', 0, false); + assert.equal(element._suggestions.length, 0); + done(); + }); + }); + + test('multi completes only the last part of the query', done => { + let promise; + const queryStub = sandbox.stub() + .returns(promise = Promise.resolve([ + {name: 'suggestion', value: 0}, + ])); + element.query = queryStub; + focusOnInput(element); + element.text = 'blah blah'; + element.multi = true; + + promise.then(() => { + const commitHandler = sandbox.spy(); + element.addEventListener('commit', commitHandler); + + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + + assert.isTrue(commitHandler.called); + assert.equal(element.text, 'blah 0'); + done(); + }); + }); + + test('tabComplete flag functions', () => { + // commitHandler checks for the commit event, whereas commitSpy checks for + // the _commit function of the element. + const commitHandler = sandbox.spy(); + element.addEventListener('commit', commitHandler); + const commitSpy = sandbox.spy(element, '_commit'); + element._focused = true; + + element._suggestions = ['tunnel snakes rule!']; + element.tabComplete = false; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + assert.isFalse(commitHandler.called); + assert.isFalse(commitSpy.called); + assert.isFalse(element._focused); + + element.tabComplete = true; + element._focused = true; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + assert.isFalse(commitHandler.called); + assert.isTrue(commitSpy.called); + assert.isTrue(element._focused); + }); + + test('_focused flag properly triggered', done => { + flush(() => { + assert.isFalse(element._focused); + const input = element.shadowRoot + .querySelector('paper-input').inputElement; + MockInteractions.focus(input); + assert.isTrue(element._focused); + done(); + }); + }); + + test('search icon shows with showSearchIcon property', done => { + flush(() => { + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('iron-icon')).display, + 'none'); + element.showSearchIcon = true; + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('iron-icon')).display, + 'none'); + done(); + }); + }); + + test('vertical offset overridden by param if it exists', () => { + assert.equal(element.$.suggestions.verticalOffset, 31); + element.verticalOffset = 30; + assert.equal(element.$.suggestions.verticalOffset, 30); + }); + + test('_focused flag shows/hides the suggestions', () => { + const openStub = sandbox.stub(element.$.suggestions, 'open'); + const closedStub = sandbox.stub(element.$.suggestions, 'close'); + element._suggestions = ['hello', 'its me']; + assert.isFalse(openStub.called); + assert.isTrue(closedStub.calledOnce); + element._focused = true; + assert.isTrue(openStub.calledOnce); + element._suggestions = []; + assert.isTrue(closedStub.calledTwice); + assert.isTrue(openStub.calledOnce); + }); + + test('_handleInputCommit with autocomplete hidden does nothing without' + + 'without allowNonSuggestedValues', () => { + const commitStub = sandbox.stub(element, '_commit'); + element.$.suggestions.isHidden = true; + element._handleInputCommit(); + assert.isFalse(commitStub.called); + }); + + test('_handleInputCommit with autocomplete hidden with' + + 'allowNonSuggestedValues', () => { + const commitStub = sandbox.stub(element, '_commit'); + element.allowNonSuggestedValues = true; + element.$.suggestions.isHidden = true; + element._handleInputCommit(); + assert.isTrue(commitStub.called); + }); + + test('_handleInputCommit with autocomplete open calls commit', () => { + const commitStub = sandbox.stub(element, '_commit'); + element.$.suggestions.isHidden = false; + element._handleInputCommit(); + assert.isTrue(commitStub.calledOnce); + }); + + test('_handleInputCommit with autocomplete open calls commit' + + 'with allowNonSuggestedValues', () => { + const commitStub = sandbox.stub(element, '_commit'); + element.allowNonSuggestedValues = true; + element.$.suggestions.isHidden = false; + element._handleInputCommit(); + assert.isTrue(commitStub.calledOnce); + }); + + test('issue 8655', () => { + function makeSuggestion(s) { return {name: s, text: s, value: s}; } + const keydownSpy = sandbox.spy(element, '_handleKeydown'); + element.setText('file:'); + element._suggestions = + [makeSuggestion('file:'), makeSuggestion('-file:')]; + MockInteractions.pressAndReleaseKeyOn(element.$.input, 88, null, 'x'); + // Must set the value, because the MockInteraction does not. + element.$.input.value = 'file:x'; + assert.isTrue(keydownSpy.calledOnce); + MockInteractions.pressAndReleaseKeyOn( + element.$.input, + 13, + null, + 'enter' + ); + assert.isTrue(keydownSpy.calledTwice); + assert.equal(element.text, 'file:x'); + }); + + suite('focus', () => { + let commitSpy; + let focusSpy; setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); + commitSpy = sandbox.spy(element, '_commit'); }); - teardown(() => { - sandbox.restore(); - }); + test('enter does not call focus', () => { + element._suggestions = ['sugar bombs']; + focusSpy = sandbox.spy(element, 'focus'); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, + 'enter'); + flushAsynchronousOperations(); - test('renders', () => { - let promise; - const queryStub = sandbox.spy(input => promise = Promise.resolve([ - {name: input + ' 0', value: 0}, - {name: input + ' 1', value: 1}, - {name: input + ' 2', value: 2}, - {name: input + ' 3', value: 3}, - {name: input + ' 4', value: 4}, - ])); - element.query = queryStub; - assert.isTrue(element.$.suggestions.isHidden); - assert.equal(element.$.suggestions.$.cursor.index, -1); - - focusOnInput(element); - element.text = 'blah'; - - assert.isTrue(queryStub.called); - element._focused = true; - - return promise.then(() => { - assert.isFalse(element.$.suggestions.isHidden); - const suggestions = - Polymer.dom(element.$.suggestions.root).querySelectorAll('li'); - assert.equal(suggestions.length, 5); - - for (let i = 0; i < 5; i++) { - assert.equal(suggestions[i].innerText.trim(), 'blah ' + i); - } - - assert.notEqual(element.$.suggestions.$.cursor.index, -1); - }); - }); - - test('selectAll', done => { - flush(() => { - const nativeInput = element._nativeInput; - const selectionStub = sandbox.stub(nativeInput, 'setSelectionRange'); - - element.selectAll(); - assert.isFalse(selectionStub.called); - - element.$.input.value = 'test'; - element.selectAll(); - assert.isTrue(selectionStub.called); - done(); - }); - }); - - test('esc key behavior', done => { - let promise; - const queryStub = sandbox.spy(() => promise = Promise.resolve([ - {name: 'blah', value: 123}, - ])); - element.query = queryStub; - - assert.isTrue(element.$.suggestions.isHidden); - - element._focused = true; - element.text = 'blah'; - - promise.then(() => { - assert.isFalse(element.$.suggestions.isHidden); - - const cancelHandler = sandbox.spy(); - element.addEventListener('cancel', cancelHandler); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); - assert.isFalse(cancelHandler.called); - assert.isTrue(element.$.suggestions.isHidden); - assert.equal(element._suggestions.length, 0); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 27, null, 'esc'); - assert.isTrue(cancelHandler.called); - done(); - }); - }); - - test('emits commit and handles cursor movement', done => { - let promise; - const queryStub = sandbox.spy(input => promise = Promise.resolve([ - {name: input + ' 0', value: 0}, - {name: input + ' 1', value: 1}, - {name: input + ' 2', value: 2}, - {name: input + ' 3', value: 3}, - {name: input + ' 4', value: 4}, - ])); - element.query = queryStub; - - assert.isTrue(element.$.suggestions.isHidden); - assert.equal(element.$.suggestions.$.cursor.index, -1); - element._focused = true; - element.text = 'blah'; - - promise.then(() => { - assert.isFalse(element.$.suggestions.isHidden); - - const commitHandler = sandbox.spy(); - element.addEventListener('commit', commitHandler); - - assert.equal(element.$.suggestions.$.cursor.index, 0); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, - 'down'); - - assert.equal(element.$.suggestions.$.cursor.index, 1); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 40, null, - 'down'); - - assert.equal(element.$.suggestions.$.cursor.index, 2); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 38, null, 'up'); - - assert.equal(element.$.suggestions.$.cursor.index, 1); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, - 'enter'); - - assert.equal(element.value, 1); - assert.isTrue(commitHandler.called); - assert.equal(commitHandler.getCall(0).args[0].detail.value, 1); - assert.isTrue(element.$.suggestions.isHidden); - assert.isTrue(element._focused); - done(); - }); - }); - - test('clear-on-commit behavior (off)', done => { - let promise; - const queryStub = sandbox.spy(() => { - promise = Promise.resolve([{name: 'suggestion', value: 0}]); - return promise; - }); - element.query = queryStub; - focusOnInput(element); - element.text = 'blah'; - - promise.then(() => { - const commitHandler = sandbox.spy(); - element.addEventListener('commit', commitHandler); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, - 'enter'); - - assert.isTrue(commitHandler.called); - assert.equal(element.text, 'suggestion'); - done(); - }); - }); - - test('clear-on-commit behavior (on)', done => { - let promise; - const queryStub = sandbox.spy(() => { - promise = Promise.resolve([{name: 'suggestion', value: 0}]); - return promise; - }); - element.query = queryStub; - focusOnInput(element); - element.text = 'blah'; - element.clearOnCommit = true; - - promise.then(() => { - const commitHandler = sandbox.spy(); - element.addEventListener('commit', commitHandler); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, - 'enter'); - - assert.isTrue(commitHandler.called); - assert.equal(element.text, ''); - done(); - }); - }); - - test('threshold guards the query', () => { - const queryStub = sandbox.spy(() => Promise.resolve([])); - element.query = queryStub; - element.threshold = 2; - focusOnInput(element); - element.text = 'a'; - assert.isFalse(queryStub.called); - element.text = 'ab'; - assert.isTrue(queryStub.called); - }); - - test('noDebounce=false debounces the query', () => { - const queryStub = sandbox.spy(() => Promise.resolve([])); - let callback; - const debounceStub = sandbox.stub(element, 'debounce', - (name, cb) => { callback = cb; }); - element.query = queryStub; - element.noDebounce = false; - focusOnInput(element); - element.text = 'a'; - assert.isFalse(queryStub.called); - assert.isTrue(debounceStub.called); - assert.equal(debounceStub.lastCall.args[2], 200); - assert.isFunction(callback); - callback(); - assert.isTrue(queryStub.called); - }); - - test('_computeClass respects border property', () => { - assert.equal(element._computeClass(), ''); - assert.equal(element._computeClass(false), ''); - assert.equal(element._computeClass(true), 'borderless'); - }); - - test('undefined or empty text results in no suggestions', () => { - element._updateSuggestions(undefined, 0, null); + assert.isTrue(commitSpy.called); + assert.isFalse(focusSpy.called); assert.equal(element._suggestions.length, 0); }); - test('when focused', done => { - let promise; - const queryStub = sandbox.stub() - .returns(promise = Promise.resolve([ - {name: 'suggestion', value: 0}, - ])); - element.query = queryStub; - element.suggestOnlyWhenFocus = true; - focusOnInput(element); - element.text = 'bla'; - assert.equal(element._focused, true); - flushAsynchronousOperations(); - promise.then(() => { - assert.equal(element._suggestions.length, 1); - assert.equal(queryStub.notCalled, false); - done(); - }); - }); - - test('when not focused', done => { - let promise; - const queryStub = sandbox.stub() - .returns(promise = Promise.resolve([ - {name: 'suggestion', value: 0}, - ])); - element.query = queryStub; - element.suggestOnlyWhenFocus = true; - element.text = 'bla'; - assert.equal(element._focused, false); - flushAsynchronousOperations(); - promise.then(() => { - assert.equal(element._suggestions.length, 0); - done(); - }); - }); - - test('suggestions should not carry over', done => { - let promise; - const queryStub = sandbox.stub() - .returns(promise = Promise.resolve([ - {name: 'suggestion', value: 0}, - ])); - element.query = queryStub; - focusOnInput(element); - element.text = 'bla'; - flushAsynchronousOperations(); - promise.then(() => { - assert.equal(element._suggestions.length, 1); - element._updateSuggestions('', 0, false); - assert.equal(element._suggestions.length, 0); - done(); - }); - }); - - test('multi completes only the last part of the query', done => { - let promise; - const queryStub = sandbox.stub() - .returns(promise = Promise.resolve([ - {name: 'suggestion', value: 0}, - ])); - element.query = queryStub; - focusOnInput(element); - element.text = 'blah blah'; - element.multi = true; - - promise.then(() => { - const commitHandler = sandbox.spy(); - element.addEventListener('commit', commitHandler); - - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, - 'enter'); - - assert.isTrue(commitHandler.called); - assert.equal(element.text, 'blah 0'); - done(); - }); - }); - - test('tabComplete flag functions', () => { - // commitHandler checks for the commit event, whereas commitSpy checks for - // the _commit function of the element. - const commitHandler = sandbox.spy(); + test('tab in input, tabComplete = true', () => { + focusSpy = sandbox.spy(element, 'focus'); + const commitHandler = sandbox.stub(); element.addEventListener('commit', commitHandler); - const commitSpy = sandbox.spy(element, '_commit'); - element._focused = true; - - element._suggestions = ['tunnel snakes rule!']; - element.tabComplete = false; + element.tabComplete = true; + element._suggestions = ['tunnel snakes drool']; MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + flushAsynchronousOperations(); + + assert.isTrue(commitSpy.called); + assert.isTrue(focusSpy.called); assert.isFalse(commitHandler.called); + assert.equal(element._suggestions.length, 0); + }); + + test('tab in input, tabComplete = false', () => { + element._suggestions = ['sugar bombs']; + focusSpy = sandbox.spy(element, 'focus'); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + flushAsynchronousOperations(); + + assert.isFalse(commitSpy.called); + assert.isFalse(focusSpy.called); + assert.equal(element._suggestions.length, 1); + }); + + test('tab on suggestion, tabComplete = false', () => { + element._suggestions = [{name: 'sugar bombs'}]; + element._focused = true; + // When tabComplete is false, do not focus. + element.tabComplete = false; + focusSpy = sandbox.spy(element, 'focus'); + flush$0(); + assert.isFalse(element.$.suggestions.isHidden); + + MockInteractions.pressAndReleaseKeyOn( + element.$.suggestions.shadowRoot + .querySelector('li:first-child'), 9, null, 'tab'); + flushAsynchronousOperations(); assert.isFalse(commitSpy.called); assert.isFalse(element._focused); + }); - element.tabComplete = true; + test('tab on suggestion, tabComplete = true', () => { + element._suggestions = [{name: 'sugar bombs'}]; element._focused = true; - MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); - assert.isFalse(commitHandler.called); + // When tabComplete is true, focus. + element.tabComplete = true; + focusSpy = sandbox.spy(element, 'focus'); + flush$0(); + assert.isFalse(element.$.suggestions.isHidden); + + MockInteractions.pressAndReleaseKeyOn( + element.$.suggestions.shadowRoot + .querySelector('li:first-child'), 9, null, 'tab'); + flushAsynchronousOperations(); + assert.isTrue(commitSpy.called); assert.isTrue(element._focused); }); - test('_focused flag properly triggered', done => { - flush(() => { - assert.isFalse(element._focused); - const input = element.shadowRoot - .querySelector('paper-input').inputElement; - MockInteractions.focus(input); - assert.isTrue(element._focused); - done(); - }); - }); - - test('search icon shows with showSearchIcon property', done => { - flush(() => { - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('iron-icon')).display, - 'none'); - element.showSearchIcon = true; - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('iron-icon')).display, - 'none'); - done(); - }); - }); - - test('vertical offset overridden by param if it exists', () => { - assert.equal(element.$.suggestions.verticalOffset, 31); - element.verticalOffset = 30; - assert.equal(element.$.suggestions.verticalOffset, 30); - }); - - test('_focused flag shows/hides the suggestions', () => { - const openStub = sandbox.stub(element.$.suggestions, 'open'); - const closedStub = sandbox.stub(element.$.suggestions, 'close'); - element._suggestions = ['hello', 'its me']; - assert.isFalse(openStub.called); - assert.isTrue(closedStub.calledOnce); + test('tap on suggestion commits, does not call focus', () => { + focusSpy = sandbox.spy(element, 'focus'); element._focused = true; - assert.isTrue(openStub.calledOnce); - element._suggestions = []; - assert.isTrue(closedStub.calledTwice); - assert.isTrue(openStub.calledOnce); - }); - - test('_handleInputCommit with autocomplete hidden does nothing without' + - 'without allowNonSuggestedValues', () => { - const commitStub = sandbox.stub(element, '_commit'); - element.$.suggestions.isHidden = true; - element._handleInputCommit(); - assert.isFalse(commitStub.called); - }); - - test('_handleInputCommit with autocomplete hidden with' + - 'allowNonSuggestedValues', () => { - const commitStub = sandbox.stub(element, '_commit'); - element.allowNonSuggestedValues = true; - element.$.suggestions.isHidden = true; - element._handleInputCommit(); - assert.isTrue(commitStub.called); - }); - - test('_handleInputCommit with autocomplete open calls commit', () => { - const commitStub = sandbox.stub(element, '_commit'); - element.$.suggestions.isHidden = false; - element._handleInputCommit(); - assert.isTrue(commitStub.calledOnce); - }); - - test('_handleInputCommit with autocomplete open calls commit' + - 'with allowNonSuggestedValues', () => { - const commitStub = sandbox.stub(element, '_commit'); - element.allowNonSuggestedValues = true; - element.$.suggestions.isHidden = false; - element._handleInputCommit(); - assert.isTrue(commitStub.calledOnce); - }); - - test('issue 8655', () => { - function makeSuggestion(s) { return {name: s, text: s, value: s}; } - const keydownSpy = sandbox.spy(element, '_handleKeydown'); - element.setText('file:'); - element._suggestions = - [makeSuggestion('file:'), makeSuggestion('-file:')]; - MockInteractions.pressAndReleaseKeyOn(element.$.input, 88, null, 'x'); - // Must set the value, because the MockInteraction does not. - element.$.input.value = 'file:x'; - assert.isTrue(keydownSpy.calledOnce); - MockInteractions.pressAndReleaseKeyOn( - element.$.input, - 13, - null, - 'enter' - ); - assert.isTrue(keydownSpy.calledTwice); - assert.equal(element.text, 'file:x'); - }); - - suite('focus', () => { - let commitSpy; - let focusSpy; - - setup(() => { - commitSpy = sandbox.spy(element, '_commit'); - }); - - test('enter does not call focus', () => { - element._suggestions = ['sugar bombs']; - focusSpy = sandbox.spy(element, 'focus'); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 13, null, - 'enter'); - flushAsynchronousOperations(); - - assert.isTrue(commitSpy.called); - assert.isFalse(focusSpy.called); - assert.equal(element._suggestions.length, 0); - }); - - test('tab in input, tabComplete = true', () => { - focusSpy = sandbox.spy(element, 'focus'); - const commitHandler = sandbox.stub(); - element.addEventListener('commit', commitHandler); - element.tabComplete = true; - element._suggestions = ['tunnel snakes drool']; - MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); - flushAsynchronousOperations(); - - assert.isTrue(commitSpy.called); - assert.isTrue(focusSpy.called); - assert.isFalse(commitHandler.called); - assert.equal(element._suggestions.length, 0); - }); - - test('tab in input, tabComplete = false', () => { - element._suggestions = ['sugar bombs']; - focusSpy = sandbox.spy(element, 'focus'); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); - flushAsynchronousOperations(); - - assert.isFalse(commitSpy.called); - assert.isFalse(focusSpy.called); - assert.equal(element._suggestions.length, 1); - }); - - test('tab on suggestion, tabComplete = false', () => { - element._suggestions = [{name: 'sugar bombs'}]; - element._focused = true; - // When tabComplete is false, do not focus. - element.tabComplete = false; - focusSpy = sandbox.spy(element, 'focus'); - Polymer.dom.flush(); - assert.isFalse(element.$.suggestions.isHidden); - - MockInteractions.pressAndReleaseKeyOn( - element.$.suggestions.shadowRoot - .querySelector('li:first-child'), 9, null, 'tab'); - flushAsynchronousOperations(); - assert.isFalse(commitSpy.called); - assert.isFalse(element._focused); - }); - - test('tab on suggestion, tabComplete = true', () => { - element._suggestions = [{name: 'sugar bombs'}]; - element._focused = true; - // When tabComplete is true, focus. - element.tabComplete = true; - focusSpy = sandbox.spy(element, 'focus'); - Polymer.dom.flush(); - assert.isFalse(element.$.suggestions.isHidden); - - MockInteractions.pressAndReleaseKeyOn( - element.$.suggestions.shadowRoot - .querySelector('li:first-child'), 9, null, 'tab'); - flushAsynchronousOperations(); - - assert.isTrue(commitSpy.called); - assert.isTrue(element._focused); - }); - - test('tap on suggestion commits, does not call focus', () => { - focusSpy = sandbox.spy(element, 'focus'); - element._focused = true; - element._suggestions = [{name: 'first suggestion'}]; - Polymer.dom.flush(); - assert.isFalse(element.$.suggestions.isHidden); - MockInteractions.tap(element.$.suggestions.shadowRoot - .querySelector('li:first-child')); - flushAsynchronousOperations(); - - assert.isFalse(focusSpy.called); - assert.isTrue(commitSpy.called); - assert.isTrue(element.$.suggestions.isHidden); - }); - }); - - test('input-keydown event fired', () => { - const listener = sandbox.spy(); - element.addEventListener('input-keydown', listener); - MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + element._suggestions = [{name: 'first suggestion'}]; + flush$0(); + assert.isFalse(element.$.suggestions.isHidden); + MockInteractions.tap(element.$.suggestions.shadowRoot + .querySelector('li:first-child')); flushAsynchronousOperations(); - assert.isTrue(listener.called); - }); - test('enter with modifier does not complete', () => { - const handleSpy = sandbox.spy(element, '_handleKeydown'); - const commitStub = sandbox.stub(element, '_handleInputCommit'); - MockInteractions.pressAndReleaseKeyOn( - element.$.input, 13, 'ctrl', 'enter'); - assert.isTrue(handleSpy.called); - assert.isFalse(commitStub.called); - MockInteractions.pressAndReleaseKeyOn( - element.$.input, 13, null, 'enter'); - assert.isTrue(commitStub.called); - }); - - suite('warnUncommitted', () => { - let inputClassList; - setup(() => { - inputClassList = element.$.input.classList; - }); - - test('enabled', () => { - element.warnUncommitted = true; - element.text = 'blah blah blah'; - MockInteractions.blur(element.$.input); - assert.isTrue(inputClassList.contains('warnUncommitted')); - MockInteractions.focus(element.$.input); - assert.isFalse(inputClassList.contains('warnUncommitted')); - }); - - test('disabled', () => { - element.warnUncommitted = false; - element.text = 'blah blah blah'; - MockInteractions.blur(element.$.input); - assert.isFalse(inputClassList.contains('warnUncommitted')); - }); - - test('no text', () => { - element.warnUncommitted = true; - element.text = ''; - MockInteractions.blur(element.$.input); - assert.isFalse(inputClassList.contains('warnUncommitted')); - }); + assert.isFalse(focusSpy.called); + assert.isTrue(commitSpy.called); + assert.isTrue(element.$.suggestions.isHidden); }); }); + + test('input-keydown event fired', () => { + const listener = sandbox.spy(); + element.addEventListener('input-keydown', listener); + MockInteractions.pressAndReleaseKeyOn(element.$.input, 9, null, 'tab'); + flushAsynchronousOperations(); + assert.isTrue(listener.called); + }); + + test('enter with modifier does not complete', () => { + const handleSpy = sandbox.spy(element, '_handleKeydown'); + const commitStub = sandbox.stub(element, '_handleInputCommit'); + MockInteractions.pressAndReleaseKeyOn( + element.$.input, 13, 'ctrl', 'enter'); + assert.isTrue(handleSpy.called); + assert.isFalse(commitStub.called); + MockInteractions.pressAndReleaseKeyOn( + element.$.input, 13, null, 'enter'); + assert.isTrue(commitStub.called); + }); + + suite('warnUncommitted', () => { + let inputClassList; + setup(() => { + inputClassList = element.$.input.classList; + }); + + test('enabled', () => { + element.warnUncommitted = true; + element.text = 'blah blah blah'; + MockInteractions.blur(element.$.input); + assert.isTrue(inputClassList.contains('warnUncommitted')); + MockInteractions.focus(element.$.input); + assert.isFalse(inputClassList.contains('warnUncommitted')); + }); + + test('disabled', () => { + element.warnUncommitted = false; + element.text = 'blah blah blah'; + MockInteractions.blur(element.$.input); + assert.isFalse(inputClassList.contains('warnUncommitted')); + }); + + test('no text', () => { + element.warnUncommitted = true; + element.text = ''; + MockInteractions.blur(element.$.input); + assert.isFalse(inputClassList.contains('warnUncommitted')); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js index efa97cf..857f1b4 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,89 +14,99 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @extends Polymer.Element - */ - class GrAvatar extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-avatar'; } +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-js-api-interface/gr-js-api-interface.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-avatar_html.js'; - static get properties() { - return { - account: { - type: Object, - observer: '_accountChanged', - }, - imageSize: { - type: Number, - value: 16, - }, - _hasAvatars: { - type: Boolean, - value: false, - }, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @extends Polymer.Element + */ +class GrAvatar extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - Promise.all([ - this._getConfig(), - Gerrit.awaitPluginsLoaded(), - ]).then(([cfg]) => { - this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); + static get is() { return 'gr-avatar'; } - this._updateAvatarURL(); - }); - } + static get properties() { + return { + account: { + type: Object, + observer: '_accountChanged', + }, + imageSize: { + type: Number, + value: 16, + }, + _hasAvatars: { + type: Boolean, + value: false, + }, + }; + } - _getConfig() { - return this.$.restAPI.getConfig(); - } + /** @override */ + attached() { + super.attached(); + Promise.all([ + this._getConfig(), + Gerrit.awaitPluginsLoaded(), + ]).then(([cfg]) => { + this._hasAvatars = !!(cfg && cfg.plugin && cfg.plugin.has_avatars); - _accountChanged(account) { this._updateAvatarURL(); + }); + } + + _getConfig() { + return this.$.restAPI.getConfig(); + } + + _accountChanged(account) { + this._updateAvatarURL(); + } + + _updateAvatarURL() { + if (!this._hasAvatars || !this.account) { + this.hidden = true; + return; } + this.hidden = false; - _updateAvatarURL() { - if (!this._hasAvatars || !this.account) { - this.hidden = true; - return; - } - this.hidden = false; - - const url = this._buildAvatarURL(this.account); - if (url) { - this.style.backgroundImage = 'url("' + url + '")'; - } - } - - _getAccounts(account) { - return account._account_id || account.email || account.username || - account.name; - } - - _buildAvatarURL(account) { - if (!account) { return ''; } - const avatars = account.avatars || []; - for (let i = 0; i < avatars.length; i++) { - if (avatars[i].height === this.imageSize) { - return avatars[i].url; - } - } - return this.getBaseUrl() + '/accounts/' + - encodeURIComponent(this._getAccounts(account)) + - '/avatar?s=' + this.imageSize; + const url = this._buildAvatarURL(this.account); + if (url) { + this.style.backgroundImage = 'url("' + url + '")'; } } - customElements.define(GrAvatar.is, GrAvatar); -})(); + _getAccounts(account) { + return account._account_id || account.email || account.username || + account.name; + } + + _buildAvatarURL(account) { + if (!account) { return ''; } + const avatars = account.avatars || []; + for (let i = 0; i < avatars.length; i++) { + if (avatars[i].height === this.imageSize) { + return avatars[i].url; + } + } + return this.getBaseUrl() + '/accounts/' + + encodeURIComponent(this._getAccounts(account)) + + '/avatar?s=' + this.imageSize; + } +} + +customElements.define(GrAvatar.is, GrAvatar);
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js index 1daffa2..be4c350 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-avatar"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: inline-block; @@ -32,6 +26,4 @@ } </style> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-avatar.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html index bd3f805..2cec20e 100644 --- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html +++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-avatar</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-avatar.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-avatar.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-avatar.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,14 +40,117 @@ </template> </test-fixture> -<script> - suite('gr-avatar tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-avatar.js'; +suite('gr-avatar tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('methods', () => { + assert.equal( + element._buildAvatarURL({ + _account_id: 123, + }), + '/accounts/123/avatar?s=16'); + assert.equal( + element._buildAvatarURL({ + email: 'test@example.com', + }), + '/accounts/test%40example.com/avatar?s=16'); + assert.equal( + element._buildAvatarURL({ + name: 'John Doe', + }), + '/accounts/John%20Doe/avatar?s=16'); + assert.equal( + element._buildAvatarURL({ + username: 'John_Doe', + }), + '/accounts/John_Doe/avatar?s=16'); + assert.equal( + element._buildAvatarURL({ + _account_id: 123, + avatars: [ + { + url: 'https://cdn.example.com/s12-p/photo.jpg', + height: 12, + }, + { + url: 'https://cdn.example.com/s16-p/photo.jpg', + height: 16, + }, + { + url: 'https://cdn.example.com/s100-p/photo.jpg', + height: 100, + }, + ], + }), + 'https://cdn.example.com/s16-p/photo.jpg'); + assert.equal( + element._buildAvatarURL({ + _account_id: 123, + avatars: [ + { + url: 'https://cdn.example.com/s95-p/photo.jpg', + height: 95, + }, + ], + }), + '/accounts/123/avatar?s=16'); + assert.equal(element._buildAvatarURL(undefined), ''); + }); + + test('dom for existing account', () => { + assert.isFalse(element.hasAttribute('hidden')); + + sandbox.stub( + element, + '_getConfig', + () => Promise.resolve({plugin: {has_avatars: true}})); + + element.imageSize = 64; + element.account = { + _account_id: 123, + }; + + assert.strictEqual(element.style.backgroundImage, ''); + + // Emulate plugins loaded. + Gerrit._loadPlugins([]); + + Promise.all([ + element.$.restAPI.getConfig(), + Gerrit.awaitPluginsLoaded(), + ]).then(() => { + assert.isFalse(element.hasAttribute('hidden')); + + assert.isTrue( + element.style.backgroundImage.includes('/accounts/123/avatar?s=64')); + }); + }); + + suite('plugin has avatars', () => { let element; let sandbox; setup(() => { sandbox = sinon.sandbox.create(); + + stub('gr-avatar', { + _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}), + }); + element = fixture('basic'); }); @@ -50,110 +158,49 @@ sandbox.restore(); }); - test('methods', () => { - assert.equal( - element._buildAvatarURL({ - _account_id: 123, - }), - '/accounts/123/avatar?s=16'); - assert.equal( - element._buildAvatarURL({ - email: 'test@example.com', - }), - '/accounts/test%40example.com/avatar?s=16'); - assert.equal( - element._buildAvatarURL({ - name: 'John Doe', - }), - '/accounts/John%20Doe/avatar?s=16'); - assert.equal( - element._buildAvatarURL({ - username: 'John_Doe', - }), - '/accounts/John_Doe/avatar?s=16'); - assert.equal( - element._buildAvatarURL({ - _account_id: 123, - avatars: [ - { - url: 'https://cdn.example.com/s12-p/photo.jpg', - height: 12, - }, - { - url: 'https://cdn.example.com/s16-p/photo.jpg', - height: 16, - }, - { - url: 'https://cdn.example.com/s100-p/photo.jpg', - height: 100, - }, - ], - }), - 'https://cdn.example.com/s16-p/photo.jpg'); - assert.equal( - element._buildAvatarURL({ - _account_id: 123, - avatars: [ - { - url: 'https://cdn.example.com/s95-p/photo.jpg', - height: 95, - }, - ], - }), - '/accounts/123/avatar?s=16'); - assert.equal(element._buildAvatarURL(undefined), ''); - }); - - test('dom for existing account', () => { + test('dom for non available account', () => { assert.isFalse(element.hasAttribute('hidden')); - sandbox.stub( - element, - '_getConfig', - () => Promise.resolve({plugin: {has_avatars: true}})); - - element.imageSize = 64; - element.account = { - _account_id: 123, - }; - - assert.strictEqual(element.style.backgroundImage, ''); - // Emulate plugins loaded. Gerrit._loadPlugins([]); - Promise.all([ + return Promise.all([ element.$.restAPI.getConfig(), Gerrit.awaitPluginsLoaded(), ]).then(() => { - assert.isFalse(element.hasAttribute('hidden')); + assert.isTrue(element.hasAttribute('hidden')); - assert.isTrue( - element.style.backgroundImage.includes('/accounts/123/avatar?s=64')); + assert.strictEqual(element.style.backgroundImage, ''); }); }); + }); - suite('plugin has avatars', () => { - let element; - let sandbox; + suite('config not set', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); + setup(() => { + sandbox = sinon.sandbox.create(); - stub('gr-avatar', { - _getConfig: () => Promise.resolve({plugin: {has_avatars: true}}), - }); - - element = fixture('basic'); + stub('gr-avatar', { + _getConfig: () => Promise.resolve({}), }); - teardown(() => { - sandbox.restore(); - }); + element = fixture('basic'); + }); - test('dom for non available account', () => { + teardown(() => { + sandbox.restore(); + }); + + test('avatar hidden when account set', () => { + flush(() => { assert.isFalse(element.hasAttribute('hidden')); + element.imageSize = 64; + element.account = { + _account_id: 123, + }; // Emulate plugins loaded. Gerrit._loadPlugins([]); @@ -162,49 +209,9 @@ Gerrit.awaitPluginsLoaded(), ]).then(() => { assert.isTrue(element.hasAttribute('hidden')); - - assert.strictEqual(element.style.backgroundImage, ''); - }); - }); - }); - - suite('config not set', () => { - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - - stub('gr-avatar', { - _getConfig: () => Promise.resolve({}), - }); - - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('avatar hidden when account set', () => { - flush(() => { - assert.isFalse(element.hasAttribute('hidden')); - - element.imageSize = 64; - element.account = { - _account_id: 123, - }; - // Emulate plugins loaded. - Gerrit._loadPlugins([]); - - return Promise.all([ - element.$.restAPI.getConfig(), - Gerrit.awaitPluginsLoaded(), - ]).then(() => { - assert.isTrue(element.hasAttribute('hidden')); - }); }); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js index 9d96038..cde56df 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button.js
@@ -14,120 +14,131 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.TooltipMixin - * @extends Polymer.Element - */ - class GrButton extends Polymer.mixinBehaviors( [ - Gerrit.KeyboardShortcutBehavior, - Gerrit.TooltipBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-button'; } +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '@polymer/paper-button/paper-button.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-button_html.js'; - static get properties() { - return { - tooltip: String, - downArrow: { - type: Boolean, - reflectToAttribute: true, - }, - link: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - observer: '_disabledChanged', - reflectToAttribute: true, - }, - noUppercase: { - type: Boolean, - value: false, - }, - loading: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, +/** + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.TooltipMixin + * @extends Polymer.Element + */ +class GrButton extends mixinBehaviors( [ + Gerrit.KeyboardShortcutBehavior, + Gerrit.TooltipBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - _disabled: { - type: Boolean, - computed: '_computeDisabled(disabled, loading)', - }, + static get is() { return 'gr-button'; } - _initialTabindex: { - type: String, - value: '0', - }, - }; - } + static get properties() { + return { + tooltip: String, + downArrow: { + type: Boolean, + reflectToAttribute: true, + }, + link: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + observer: '_disabledChanged', + reflectToAttribute: true, + }, + noUppercase: { + type: Boolean, + value: false, + }, + loading: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, - /** @override */ - created() { - super.created(); - this._initialTabindex = this.getAttribute('tabindex') || '0'; - this.addEventListener('click', e => this._handleAction(e)); - this.addEventListener('keydown', - e => this._handleKeydown(e)); - } + _disabled: { + type: Boolean, + computed: '_computeDisabled(disabled, loading)', + }, - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'button'); - this._ensureAttribute('tabindex', '0'); - } - - _handleAction(e) { - if (this._disabled) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - return; - } - - let el = this.root; - let path = ''; - while (el = el.parentNode || el.host) { - if (el.tagName && el.tagName.startsWith('GR-APP')) { - break; - } - if (el.tagName) { - const idString = el.id ? '#' + el.id : ''; - path = el.tagName + idString + ' ' + path; - } - } - this.$.reporting.reportInteraction('button-click', - {path: path.trim().toLowerCase()}); - } - - _disabledChanged(disabled) { - this.setAttribute('tabindex', disabled ? '-1' : this._initialTabindex); - this.updateStyles(); - } - - _computeDisabled(disabled, loading) { - return disabled || loading; - } - - _handleKeydown(e) { - if (this.modifierPressed(e)) { return; } - e = this.getKeyboardEvent(e); - // Handle `enter`, `space`. - if (e.keyCode === 13 || e.keyCode === 32) { - e.preventDefault(); - e.stopPropagation(); - this.click(); - } - } + _initialTabindex: { + type: String, + value: '0', + }, + }; } - customElements.define(GrButton.is, GrButton); -})(); + /** @override */ + created() { + super.created(); + this._initialTabindex = this.getAttribute('tabindex') || '0'; + this.addEventListener('click', e => this._handleAction(e)); + this.addEventListener('keydown', + e => this._handleKeydown(e)); + } + + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'button'); + this._ensureAttribute('tabindex', '0'); + } + + _handleAction(e) { + if (this._disabled) { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + return; + } + + let el = this.root; + let path = ''; + while (el = el.parentNode || el.host) { + if (el.tagName && el.tagName.startsWith('GR-APP')) { + break; + } + if (el.tagName) { + const idString = el.id ? '#' + el.id : ''; + path = el.tagName + idString + ' ' + path; + } + } + this.$.reporting.reportInteraction('button-click', + {path: path.trim().toLowerCase()}); + } + + _disabledChanged(disabled) { + this.setAttribute('tabindex', disabled ? '-1' : this._initialTabindex); + this.updateStyles(); + } + + _computeDisabled(disabled, loading) { + return disabled || loading; + } + + _handleKeydown(e) { + if (this.modifierPressed(e)) { return; } + e = this.getKeyboardEvent(e); + // Handle `enter`, `space`. + if (e.keyCode === 13 || e.keyCode === 32) { + e.preventDefault(); + e.stopPropagation(); + this.click(); + } + } +} + +customElements.define(GrButton.is, GrButton);
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js index b94359f..ad5c00d 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="/bower_components/paper-button/paper-button.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> - -<dom-module id="gr-button"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* general styles for all buttons */ :host { @@ -172,10 +164,7 @@ border-top-color: var(--deemphasized-text-color); } </style> - <paper-button - raised="[[!link]]" - disabled="[[_disabled]]" - tabindex="-1"> + <paper-button raised="[[!link]]" disabled="[[_disabled]]" tabindex="-1"> <template is="dom-if" if="[[loading]]"> <span class="loadingSpin"></span> </template> @@ -183,6 +172,4 @@ <i class="downArrow"></i> </paper-button> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-button.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html index cfac37f..42f9a5a 100644 --- a/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html +++ b/polygerrit-ui/app/elements/shared/gr-button/gr-button_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-button</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-button.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-button.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-button.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -41,146 +46,149 @@ </template> </test-fixture> -<script> - suite('gr-button tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-button.js'; +import {addListener} from '@polymer/polymer/lib/utils/gestures.js'; +suite('gr-button tests', () => { + let element; + let sandbox; - const addSpyOn = function(eventName) { - const spy = sandbox.spy(); - if (eventName == 'tap') { - Polymer.Gestures.addListener(element, eventName, spy); - } else { - element.addEventListener(eventName, spy); - } - return spy; - }; + const addSpyOn = function(eventName) { + const spy = sandbox.spy(); + if (eventName == 'tap') { + addListener(element, eventName, spy); + } else { + element.addEventListener(eventName, spy); + } + return spy; + }; + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('disabled is set by disabled', () => { + const paperBtn = element.shadowRoot.querySelector('paper-button'); + assert.isFalse(paperBtn.disabled); + element.disabled = true; + assert.isTrue(paperBtn.disabled); + element.disabled = false; + assert.isFalse(paperBtn.disabled); + }); + + test('loading set from listener', done => { + let resolve; + element.addEventListener('click', e => { + e.target.loading = true; + resolve = () => e.target.loading = false; + }); + const paperBtn = element.shadowRoot.querySelector('paper-button'); + assert.isFalse(paperBtn.disabled); + MockInteractions.tap(element); + assert.isTrue(paperBtn.disabled); + assert.isTrue(element.hasAttribute('loading')); + resolve(); + flush(() => { + assert.isFalse(paperBtn.disabled); + assert.isFalse(element.hasAttribute('loading')); + done(); + }); + }); + + test('tabindex should be -1 if disabled', () => { + element.disabled = true; + assert.isTrue(element.getAttribute('tabindex') === '-1'); + }); + + // Regression tests for Issue: 11969 + test('tabindex should be reset to 0 if enabled', () => { + element.disabled = false; + assert.equal(element.getAttribute('tabindex'), '0'); + element.disabled = true; + assert.equal(element.getAttribute('tabindex'), '-1'); + element.disabled = false; + assert.equal(element.getAttribute('tabindex'), '0'); + }); + + test('tabindex should be preserved', () => { + element = fixture('tabindex'); + element.disabled = false; + assert.equal(element.getAttribute('tabindex'), '3'); + element.disabled = true; + assert.equal(element.getAttribute('tabindex'), '-1'); + element.disabled = false; + assert.equal(element.getAttribute('tabindex'), '3'); + }); + + // 'tap' event is tested so we don't loose backward compatibility with older + // plugins who didn't move to on-click which is faster and well supported. + test('dispatches click event', () => { + const spy = addSpyOn('click'); + MockInteractions.click(element); + assert.isTrue(spy.calledOnce); + }); + + test('dispatches tap event', () => { + const spy = addSpyOn('tap'); + MockInteractions.tap(element); + assert.isTrue(spy.calledOnce); + }); + + test('dispatches click from tap event', () => { + const spy = addSpyOn('click'); + MockInteractions.tap(element); + assert.isTrue(spy.calledOnce); + }); + + // Keycodes: 32 for Space, 13 for Enter. + for (const key of [32, 13]) { + test('dispatches click event on keycode ' + key, () => { + const tapSpy = sandbox.spy(); + element.addEventListener('click', tapSpy); + MockInteractions.pressAndReleaseKeyOn(element, key); + assert.isTrue(tapSpy.calledOnce); + }); + + test('dispatches no click event with modifier on keycode ' + key, () => { + const tapSpy = sandbox.spy(); + element.addEventListener('click', tapSpy); + MockInteractions.pressAndReleaseKeyOn(element, key, 'shift'); + MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl'); + MockInteractions.pressAndReleaseKeyOn(element, key, 'meta'); + MockInteractions.pressAndReleaseKeyOn(element, key, 'alt'); + assert.isFalse(tapSpy.calledOnce); + }); + } + + suite('disabled', () => { setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('disabled is set by disabled', () => { - const paperBtn = element.shadowRoot.querySelector('paper-button'); - assert.isFalse(paperBtn.disabled); element.disabled = true; - assert.isTrue(paperBtn.disabled); - element.disabled = false; - assert.isFalse(paperBtn.disabled); }); - test('loading set from listener', done => { - let resolve; - element.addEventListener('click', e => { - e.target.loading = true; - resolve = () => e.target.loading = false; - }); - const paperBtn = element.shadowRoot.querySelector('paper-button'); - assert.isFalse(paperBtn.disabled); - MockInteractions.tap(element); - assert.isTrue(paperBtn.disabled); - assert.isTrue(element.hasAttribute('loading')); - resolve(); - flush(() => { - assert.isFalse(paperBtn.disabled); - assert.isFalse(element.hasAttribute('loading')); - done(); - }); - }); - - test('tabindex should be -1 if disabled', () => { - element.disabled = true; - assert.isTrue(element.getAttribute('tabindex') === '-1'); - }); - - // Regression tests for Issue: 11969 - test('tabindex should be reset to 0 if enabled', () => { - element.disabled = false; - assert.equal(element.getAttribute('tabindex'), '0'); - element.disabled = true; - assert.equal(element.getAttribute('tabindex'), '-1'); - element.disabled = false; - assert.equal(element.getAttribute('tabindex'), '0'); - }); - - test('tabindex should be preserved', () => { - element = fixture('tabindex'); - element.disabled = false; - assert.equal(element.getAttribute('tabindex'), '3'); - element.disabled = true; - assert.equal(element.getAttribute('tabindex'), '-1'); - element.disabled = false; - assert.equal(element.getAttribute('tabindex'), '3'); - }); - - // 'tap' event is tested so we don't loose backward compatibility with older - // plugins who didn't move to on-click which is faster and well supported. - test('dispatches click event', () => { - const spy = addSpyOn('click'); - MockInteractions.click(element); - assert.isTrue(spy.calledOnce); - }); - - test('dispatches tap event', () => { - const spy = addSpyOn('tap'); - MockInteractions.tap(element); - assert.isTrue(spy.calledOnce); - }); - - test('dispatches click from tap event', () => { - const spy = addSpyOn('click'); - MockInteractions.tap(element); - assert.isTrue(spy.calledOnce); - }); - - // Keycodes: 32 for Space, 13 for Enter. - for (const key of [32, 13]) { - test('dispatches click event on keycode ' + key, () => { - const tapSpy = sandbox.spy(); - element.addEventListener('click', tapSpy); - MockInteractions.pressAndReleaseKeyOn(element, key); - assert.isTrue(tapSpy.calledOnce); - }); - - test('dispatches no click event with modifier on keycode ' + key, () => { - const tapSpy = sandbox.spy(); - element.addEventListener('click', tapSpy); - MockInteractions.pressAndReleaseKeyOn(element, key, 'shift'); - MockInteractions.pressAndReleaseKeyOn(element, key, 'ctrl'); - MockInteractions.pressAndReleaseKeyOn(element, key, 'meta'); - MockInteractions.pressAndReleaseKeyOn(element, key, 'alt'); - assert.isFalse(tapSpy.calledOnce); + for (const eventName of ['tap', 'click']) { + test('stops ' + eventName + ' event', () => { + const spy = addSpyOn(eventName); + MockInteractions.tap(element); + assert.isFalse(spy.called); }); } - suite('disabled', () => { - setup(() => { - element.disabled = true; + // Keycodes: 32 for Space, 13 for Enter. + for (const key of [32, 13]) { + test('stops click event on keycode ' + key, () => { + const tapSpy = sandbox.spy(); + element.addEventListener('click', tapSpy); + MockInteractions.pressAndReleaseKeyOn(element, key); + assert.isFalse(tapSpy.called); }); - - for (const eventName of ['tap', 'click']) { - test('stops ' + eventName + ' event', () => { - const spy = addSpyOn(eventName); - MockInteractions.tap(element); - assert.isFalse(spy.called); - }); - } - - // Keycodes: 32 for Space, 13 for Enter. - for (const key of [32, 13]) { - test('stops click event on keycode ' + key, () => { - const tapSpy = sandbox.spy(); - element.addEventListener('click', tapSpy); - MockInteractions.pressAndReleaseKeyOn(element, key); - assert.isFalse(tapSpy.called); - }); - } - }); + } }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js index 001632f..10e06dd 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,49 +14,56 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrChangeStar extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-change-star'; } - /** - * Fired when star state is toggled. - * - * @event toggle-star - */ +import '../gr-icons/gr-icons.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-star_html.js'; - static get properties() { - return { - /** @type {?} */ - change: { - type: Object, - notify: true, - }, - }; - } +/** @extends Polymer.Element */ +class GrChangeStar extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _computeStarClass(starred) { - return starred ? 'active' : ''; - } + static get is() { return 'gr-change-star'; } + /** + * Fired when star state is toggled. + * + * @event toggle-star + */ - _computeStarIcon(starred) { - // Hollow star is used to indicate inactive state. - return `gr-icons:star${starred ? '' : '-border'}`; - } - - toggleStar() { - const newVal = !this.change.starred; - this.set('change.starred', newVal); - this.dispatchEvent(new CustomEvent('toggle-star', { - bubbles: true, - composed: true, - detail: {change: this.change, starred: newVal}, - })); - } + static get properties() { + return { + /** @type {?} */ + change: { + type: Object, + notify: true, + }, + }; } - customElements.define(GrChangeStar.is, GrChangeStar); -})(); + _computeStarClass(starred) { + return starred ? 'active' : ''; + } + + _computeStarIcon(starred) { + // Hollow star is used to indicate inactive state. + return `gr-icons:star${starred ? '' : '-border'}`; + } + + toggleStar() { + const newVal = !this.change.starred; + this.set('change.starred', newVal); + this.dispatchEvent(new CustomEvent('toggle-star', { + bubbles: true, + composed: true, + detail: {change: this.change, starred: newVal}, + })); + } +} + +customElements.define(GrChangeStar.is, GrChangeStar);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js index dc8ba34..a4925aa 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_html.js
@@ -1,26 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-change-star"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> button { background-color: transparent; @@ -36,10 +32,6 @@ } </style> <button aria-label="Change star" on-click="toggleStar"> - <iron-icon - class$="[[_computeStarClass(change.starred)]]" - icon$="[[_computeStarIcon(change.starred)]]"></iron-icon> + <iron-icon class\$="[[_computeStarClass(change.starred)]]" icon\$="[[_computeStarIcon(change.starred)]]"></iron-icon> </button> - </template> - <script src="gr-change-star.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html index b76ce4d..aa138d3 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html +++ b/polygerrit-ui/app/elements/shared/gr-change-star/gr-change-star_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-star</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-change-star.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-change-star.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-star.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,51 +40,53 @@ </template> </test-fixture> -<script> - suite('gr-change-star tests', async () => { - await readyToTest(); - let element; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-star.js'; +suite('gr-change-star tests', () => { + let element; - setup(() => { - element = fixture('basic'); - element.change = { - _number: 2, - starred: true, - }; - }); - - test('star visibility states', () => { - element.set('change.starred', true); - let icon = element.shadowRoot - .querySelector('iron-icon'); - assert.isTrue(icon.classList.contains('active')); - assert.equal(icon.icon, 'gr-icons:star'); - - element.set('change.starred', false); - icon = element.shadowRoot - .querySelector('iron-icon'); - assert.isFalse(icon.classList.contains('active')); - assert.equal(icon.icon, 'gr-icons:star-border'); - }); - - test('starring', done => { - element.addEventListener('toggle-star', () => { - assert.equal(element.change.starred, true); - done(); - }); - element.set('change.starred', false); - MockInteractions.tap(element.shadowRoot - .querySelector('button')); - }); - - test('unstarring', done => { - element.addEventListener('toggle-star', () => { - assert.equal(element.change.starred, false); - done(); - }); - element.set('change.starred', true); - MockInteractions.tap(element.shadowRoot - .querySelector('button')); - }); + setup(() => { + element = fixture('basic'); + element.change = { + _number: 2, + starred: true, + }; }); + + test('star visibility states', () => { + element.set('change.starred', true); + let icon = element.shadowRoot + .querySelector('iron-icon'); + assert.isTrue(icon.classList.contains('active')); + assert.equal(icon.icon, 'gr-icons:star'); + + element.set('change.starred', false); + icon = element.shadowRoot + .querySelector('iron-icon'); + assert.isFalse(icon.classList.contains('active')); + assert.equal(icon.icon, 'gr-icons:star-border'); + }); + + test('starring', done => { + element.addEventListener('toggle-star', () => { + assert.equal(element.change.starred, true); + done(); + }); + element.set('change.starred', false); + MockInteractions.tap(element.shadowRoot + .querySelector('button')); + }); + + test('unstarring', done => { + element.addEventListener('toggle-star', () => { + assert.equal(element.change.starred, false); + done(); + }); + element.set('change.starred', true); + MockInteractions.tap(element.shadowRoot + .querySelector('button')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js index 7052a6a..b99612e 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js +++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status.js
@@ -14,85 +14,93 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const ChangeStates = { - MERGED: 'Merged', - ABANDONED: 'Abandoned', - MERGE_CONFLICT: 'Merge Conflict', - WIP: 'WIP', - PRIVATE: 'Private', - }; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-tooltip-content/gr-tooltip-content.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-change-status_html.js'; - const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' + - 'It will not appear on dashboards unless you are CC\'ed or assigned, ' + - 'and email notifications will be silenced until the review is started.'; +const ChangeStates = { + MERGED: 'Merged', + ABANDONED: 'Abandoned', + MERGE_CONFLICT: 'Merge Conflict', + WIP: 'WIP', + PRIVATE: 'Private', +}; - const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' + - 'Download the patch and run "git rebase master". ' + - 'Upload a new patchset after resolving all merge conflicts.'; +const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' + + 'It will not appear on dashboards unless you are CC\'ed or assigned, ' + + 'and email notifications will be silenced until the review is started.'; - const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' + - 'current reviewers (or anyone with "View Private Changes" permission).'; +const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' + + 'Download the patch and run "git rebase master". ' + + 'Upload a new patchset after resolving all merge conflicts.'; - /** @extends Polymer.Element */ - class GrChangeStatus extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-change-status'; } +const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' + + 'current reviewers (or anyone with "View Private Changes" permission).'; - static get properties() { - return { - flat: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - status: { - type: String, - observer: '_updateChipDetails', - }, - tooltipText: { - type: String, - value: '', - }, - }; - } +/** @extends Polymer.Element */ +class GrChangeStatus extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _computeStatusString(status) { - if (status === ChangeStates.WIP && !this.flat) { - return 'Work in Progress'; - } - return status; - } + static get is() { return 'gr-change-status'; } - _toClassName(str) { - return str.toLowerCase().replace(/\s/g, '-'); - } - - _updateChipDetails(status, previousStatus) { - if (previousStatus) { - this.classList.remove(this._toClassName(previousStatus)); - } - this.classList.add(this._toClassName(status)); - - switch (status) { - case ChangeStates.WIP: - this.tooltipText = WIP_TOOLTIP; - break; - case ChangeStates.PRIVATE: - this.tooltipText = PRIVATE_TOOLTIP; - break; - case ChangeStates.MERGE_CONFLICT: - this.tooltipText = MERGE_CONFLICT_TOOLTIP; - break; - default: - this.tooltipText = ''; - break; - } - } + static get properties() { + return { + flat: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + status: { + type: String, + observer: '_updateChipDetails', + }, + tooltipText: { + type: String, + value: '', + }, + }; } - customElements.define(GrChangeStatus.is, GrChangeStatus); -})(); + _computeStatusString(status) { + if (status === ChangeStates.WIP && !this.flat) { + return 'Work in Progress'; + } + return status; + } + + _toClassName(str) { + return str.toLowerCase().replace(/\s/g, '-'); + } + + _updateChipDetails(status, previousStatus) { + if (previousStatus) { + this.classList.remove(this._toClassName(previousStatus)); + } + this.classList.add(this._toClassName(status)); + + switch (status) { + case ChangeStates.WIP: + this.tooltipText = WIP_TOOLTIP; + break; + case ChangeStates.PRIVATE: + this.tooltipText = PRIVATE_TOOLTIP; + break; + case ChangeStates.MERGE_CONFLICT: + this.tooltipText = MERGE_CONFLICT_TOOLTIP; + break; + default: + this.tooltipText = ''; + break; + } + } +} + +customElements.define(GrChangeStatus.is, GrChangeStatus);
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js index eaca593..1a1bc1b8 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js +++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-change-status"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .chip { border-radius: var(--border-radius); @@ -70,17 +64,9 @@ color: white; } </style> - <gr-tooltip-content - has-tooltip - position-below - title="[[tooltipText]]" - max-width="40em"> - <div - class="chip" - aria-label$="Label: [[status]]"> + <gr-tooltip-content has-tooltip="" position-below="" title="[[tooltipText]]" max-width="40em"> + <div class="chip" aria-label\$="Label: [[status]]"> [[_computeStatusString(status)]] </div> </gr-tooltip-content> - </template> - <script src="gr-change-status.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html index d78cc3a..819411f 100644 --- a/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html +++ b/polygerrit-ui/app/elements/shared/gr-change-status/gr-change-status_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-status</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-change-status.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-change-status.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-status.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,106 +40,108 @@ </template> </test-fixture> -<script> - const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' + - 'It will not appear on dashboards unless you are CC\'ed or assigned, ' + - 'and email notifications will be silenced until the review is started.'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-change-status.js'; +const WIP_TOOLTIP = 'This change isn\'t ready to be reviewed or submitted. ' + + 'It will not appear on dashboards unless you are CC\'ed or assigned, ' + + 'and email notifications will be silenced until the review is started.'; - const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' + - 'Download the patch and run "git rebase master". ' + - 'Upload a new patchset after resolving all merge conflicts.'; +const MERGE_CONFLICT_TOOLTIP = 'This change has merge conflicts. ' + + 'Download the patch and run "git rebase master". ' + + 'Upload a new patchset after resolving all merge conflicts.'; - const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' + - 'current reviewers (or anyone with "View Private Changes" permission).'; +const PRIVATE_TOOLTIP = 'This change is only visible to its owner and ' + + 'current reviewers (or anyone with "View Private Changes" permission).'; - suite('gr-change-status tests', async () => { - await readyToTest(); - let element; - let sandbox; +suite('gr-change-status tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('WIP', () => { - element.status = 'WIP'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, 'Work in Progress'); - assert.equal(element.tooltipText, WIP_TOOLTIP); - assert.isTrue(element.classList.contains('wip')); - }); - - test('WIP flat', () => { - element.flat = true; - element.status = 'WIP'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, 'WIP'); - assert.isDefined(element.tooltipText); - assert.isTrue(element.classList.contains('wip')); - assert.isTrue(element.hasAttribute('flat')); - }); - - test('merged', () => { - element.status = 'Merged'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, element.status); - assert.equal(element.tooltipText, ''); - assert.isTrue(element.classList.contains('merged')); - }); - - test('abandoned', () => { - element.status = 'Abandoned'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, element.status); - assert.equal(element.tooltipText, ''); - assert.isTrue(element.classList.contains('abandoned')); - }); - - test('merge conflict', () => { - element.status = 'Merge Conflict'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, element.status); - assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP); - assert.isTrue(element.classList.contains('merge-conflict')); - }); - - test('private', () => { - element.status = 'Private'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, element.status); - assert.equal(element.tooltipText, PRIVATE_TOOLTIP); - assert.isTrue(element.classList.contains('private')); - }); - - test('active', () => { - element.status = 'Active'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, element.status); - assert.equal(element.tooltipText, ''); - assert.isTrue(element.classList.contains('active')); - }); - - test('ready to submit', () => { - element.status = 'Ready to submit'; - assert.equal(element.shadowRoot - .querySelector('.chip').innerText, element.status); - assert.equal(element.tooltipText, ''); - assert.isTrue(element.classList.contains('ready-to-submit')); - }); - - test('updating status removes the previous class', () => { - element.status = 'Private'; - assert.isTrue(element.classList.contains('private')); - assert.isFalse(element.classList.contains('wip')); - - element.status = 'WIP'; - assert.isFalse(element.classList.contains('private')); - assert.isTrue(element.classList.contains('wip')); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('WIP', () => { + element.status = 'WIP'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, 'Work in Progress'); + assert.equal(element.tooltipText, WIP_TOOLTIP); + assert.isTrue(element.classList.contains('wip')); + }); + + test('WIP flat', () => { + element.flat = true; + element.status = 'WIP'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, 'WIP'); + assert.isDefined(element.tooltipText); + assert.isTrue(element.classList.contains('wip')); + assert.isTrue(element.hasAttribute('flat')); + }); + + test('merged', () => { + element.status = 'Merged'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, element.status); + assert.equal(element.tooltipText, ''); + assert.isTrue(element.classList.contains('merged')); + }); + + test('abandoned', () => { + element.status = 'Abandoned'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, element.status); + assert.equal(element.tooltipText, ''); + assert.isTrue(element.classList.contains('abandoned')); + }); + + test('merge conflict', () => { + element.status = 'Merge Conflict'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, element.status); + assert.equal(element.tooltipText, MERGE_CONFLICT_TOOLTIP); + assert.isTrue(element.classList.contains('merge-conflict')); + }); + + test('private', () => { + element.status = 'Private'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, element.status); + assert.equal(element.tooltipText, PRIVATE_TOOLTIP); + assert.isTrue(element.classList.contains('private')); + }); + + test('active', () => { + element.status = 'Active'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, element.status); + assert.equal(element.tooltipText, ''); + assert.isTrue(element.classList.contains('active')); + }); + + test('ready to submit', () => { + element.status = 'Ready to submit'; + assert.equal(element.shadowRoot + .querySelector('.chip').innerText, element.status); + assert.equal(element.tooltipText, ''); + assert.isTrue(element.classList.contains('ready-to-submit')); + }); + + test('updating status removes the previous class', () => { + element.status = 'Private'; + assert.isTrue(element.classList.contains('private')); + assert.isFalse(element.classList.contains('wip')); + + element.status = 'WIP'; + assert.isFalse(element.classList.contains('private')); + assert.isTrue(element.classList.contains('wip')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js index d8a56f8..765c5cc 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js +++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,517 +14,532 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const UNRESOLVED_EXPAND_COUNT = 5; - const NEWLINE_PATTERN = /\n/g; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-storage/gr-storage.js'; +import '../gr-comment/gr-comment.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-comment-thread_html.js'; + +const UNRESOLVED_EXPAND_COUNT = 5; +const NEWLINE_PATTERN = /\n/g; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @appliesMixin Gerrit.PathListMixin + * @extends Polymer.Element + */ +class GrCommentThread extends mixinBehaviors( [ + /** + * Not used in this element rather other elements tests + */ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, + Gerrit.PathListBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-comment-thread'; } + /** + * Fired when the thread should be discarded. + * + * @event thread-discard + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @appliesMixin Gerrit.PathListMixin - * @extends Polymer.Element + * Fired when a comment in the thread is permanently modified. + * + * @event thread-changed */ - class GrCommentThread extends Polymer.mixinBehaviors( [ - /** - * Not used in this element rather other elements tests - */ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - Gerrit.PathListBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-comment-thread'; } - /** - * Fired when the thread should be discarded. - * - * @event thread-discard - */ - /** - * Fired when a comment in the thread is permanently modified. - * - * @event thread-changed - */ + /** + * gr-comment-thread exposes the following attributes that allow a + * diff widget like gr-diff to show the thread in the right location: + * + * line-num: + * 1-based line number or undefined if it refers to the entire file. + * + * comment-side: + * "left" or "right". These indicate which of the two diffed versions + * the comment relates to. In the case of unified diff, the left + * version is the one whose line number column is further to the left. + * + * range: + * The range of text that the comment refers to (start_line, + * start_character, end_line, end_character), serialized as JSON. If + * set, range's end_line will have the same value as line-num. Line + * numbers are 1-based, char numbers are 0-based. The start position + * (start_line, start_character) is inclusive, and the end position + * (end_line, end_character) is exclusive. + */ + static get properties() { + return { + changeNum: String, + comments: { + type: Array, + value() { return []; }, + }, + /** + * @type {?{start_line: number, start_character: number, end_line: number, + * end_character: number}} + */ + range: { + type: Object, + reflectToAttribute: true, + }, + keyEventTarget: { + type: Object, + value() { return document.body; }, + }, + commentSide: { + type: String, + reflectToAttribute: true, + }, + patchNum: String, + path: String, + projectName: { + type: String, + observer: '_projectNameChanged', + }, + hasDraft: { + type: Boolean, + notify: true, + reflectToAttribute: true, + }, + isOnParent: { + type: Boolean, + value: false, + }, + parentIndex: { + type: Number, + value: null, + }, + rootId: { + type: String, + notify: true, + computed: '_computeRootId(comments.*)', + }, + /** + * If this is true, the comment thread also needs to have the change and + * line properties property set + */ + showFilePath: { + type: Boolean, + value: false, + }, + /** Necessary only if showFilePath is true or when used with gr-diff */ + lineNum: { + type: Number, + reflectToAttribute: true, + }, + unresolved: { + type: Boolean, + notify: true, + reflectToAttribute: true, + }, + _showActions: Boolean, + _lastComment: Object, + _orderedComments: Array, + _projectConfig: Object, + isRobotComment: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + }; + } - /** - * gr-comment-thread exposes the following attributes that allow a - * diff widget like gr-diff to show the thread in the right location: - * - * line-num: - * 1-based line number or undefined if it refers to the entire file. - * - * comment-side: - * "left" or "right". These indicate which of the two diffed versions - * the comment relates to. In the case of unified diff, the left - * version is the one whose line number column is further to the left. - * - * range: - * The range of text that the comment refers to (start_line, - * start_character, end_line, end_character), serialized as JSON. If - * set, range's end_line will have the same value as line-num. Line - * numbers are 1-based, char numbers are 0-based. The start position - * (start_line, start_character) is inclusive, and the end position - * (end_line, end_character) is exclusive. - */ - static get properties() { - return { - changeNum: String, - comments: { - type: Array, - value() { return []; }, - }, - /** - * @type {?{start_line: number, start_character: number, end_line: number, - * end_character: number}} - */ - range: { - type: Object, - reflectToAttribute: true, - }, - keyEventTarget: { - type: Object, - value() { return document.body; }, - }, - commentSide: { - type: String, - reflectToAttribute: true, - }, - patchNum: String, - path: String, - projectName: { - type: String, - observer: '_projectNameChanged', - }, - hasDraft: { - type: Boolean, - notify: true, - reflectToAttribute: true, - }, - isOnParent: { - type: Boolean, - value: false, - }, - parentIndex: { - type: Number, - value: null, - }, - rootId: { - type: String, - notify: true, - computed: '_computeRootId(comments.*)', - }, - /** - * If this is true, the comment thread also needs to have the change and - * line properties property set - */ - showFilePath: { - type: Boolean, - value: false, - }, - /** Necessary only if showFilePath is true or when used with gr-diff */ - lineNum: { - type: Number, - reflectToAttribute: true, - }, - unresolved: { - type: Boolean, - notify: true, - reflectToAttribute: true, - }, - _showActions: Boolean, - _lastComment: Object, - _orderedComments: Array, - _projectConfig: Object, - isRobotComment: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - }; - } + static get observers() { + return [ + '_commentsChanged(comments.*)', + ]; + } - static get observers() { - return [ - '_commentsChanged(comments.*)', - ]; - } + get keyBindings() { + return { + 'e shift+e': '_handleEKey', + }; + } - get keyBindings() { - return { - 'e shift+e': '_handleEKey', - }; - } + /** @override */ + created() { + super.created(); + this.addEventListener('comment-update', + e => this._handleCommentUpdate(e)); + } - /** @override */ - created() { - super.created(); - this.addEventListener('comment-update', - e => this._handleCommentUpdate(e)); - } + /** @override */ + attached() { + super.attached(); + this._getLoggedIn().then(loggedIn => { + this._showActions = loggedIn; + }); + this._setInitialExpandedState(); + } - /** @override */ - attached() { - super.attached(); - this._getLoggedIn().then(loggedIn => { - this._showActions = loggedIn; - }); - this._setInitialExpandedState(); - } + addOrEditDraft(opt_lineNum, opt_range) { + const lastComment = this.comments[this.comments.length - 1] || {}; + if (lastComment.__draft) { + const commentEl = this._commentElWithDraftID( + lastComment.id || lastComment.__draftID); + commentEl.editing = true; - addOrEditDraft(opt_lineNum, opt_range) { - const lastComment = this.comments[this.comments.length - 1] || {}; - if (lastComment.__draft) { - const commentEl = this._commentElWithDraftID( - lastComment.id || lastComment.__draftID); - commentEl.editing = true; - - // If the comment was collapsed, re-open it to make it clear which - // actions are available. - commentEl.collapsed = false; - } else { - const range = opt_range ? opt_range : - lastComment ? lastComment.range : undefined; - const unresolved = lastComment ? lastComment.unresolved : undefined; - this.addDraft(opt_lineNum, range, unresolved); - } - } - - addDraft(opt_lineNum, opt_range, opt_unresolved) { - const draft = this._newDraft(opt_lineNum, opt_range); - draft.__editing = true; - draft.unresolved = opt_unresolved === false ? opt_unresolved : true; - this.push('comments', draft); - } - - fireRemoveSelf() { - this.dispatchEvent(new CustomEvent('thread-discard', - {detail: {rootId: this.rootId}, bubbles: false})); - } - - _getDiffUrlForComment(projectName, changeNum, path, patchNum) { - return Gerrit.Nav.getUrlForDiffById(changeNum, - projectName, path, patchNum, - null, this.lineNum); - } - - _computeDisplayPath(path) { - const lineString = this.lineNum ? `#${this.lineNum}` : ''; - return this.computeDisplayPath(path) + lineString; - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _commentsChanged() { - this._orderedComments = this._sortedComments(this.comments); - this.updateThreadProperties(); - } - - updateThreadProperties() { - if (this._orderedComments.length) { - this._lastComment = this._getLastComment(); - this.unresolved = this._lastComment.unresolved; - this.hasDraft = this._lastComment.__draft; - this.isRobotComment = !!(this._lastComment.robot_id); - } - } - - _shouldDisableAction(_showActions, _lastComment) { - return !_showActions || !_lastComment || !!_lastComment.__draft; - } - - _hideActions(_showActions, _lastComment) { - return this._shouldDisableAction(_showActions, _lastComment) || - !!_lastComment.robot_id; - } - - _getLastComment() { - return this._orderedComments[this._orderedComments.length - 1] || {}; - } - - _handleEKey(e) { - if (this.shouldSuppressKeyboardShortcut(e)) { return; } - - // Don’t preventDefault in this case because it will render the event - // useless for other handlers (other gr-comment-thread elements). - if (e.detail.keyboardEvent.shiftKey) { - this._expandCollapseComments(true); - } else { - if (this.modifierPressed(e)) { return; } - this._expandCollapseComments(false); - } - } - - _expandCollapseComments(actionIsCollapse) { - const comments = - Polymer.dom(this.root).querySelectorAll('gr-comment'); - for (const comment of comments) { - comment.collapsed = actionIsCollapse; - } - } - - /** - * Sets the initial state of the comment thread. - * Expands the thread if one of the following is true: - * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the - * thread is unresolved, - * - it's a robot comment. - */ - _setInitialExpandedState() { - if (this._orderedComments) { - for (let i = 0; i < this._orderedComments.length; i++) { - const comment = this._orderedComments[i]; - const isRobotComment = !!comment.robot_id; - // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT. - const resolvedThread = !this.unresolved || - this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT; - comment.collapsed = !isRobotComment && resolvedThread; - } - } - } - - _sortedComments(comments) { - return comments.slice().sort((c1, c2) => { - const c1Date = c1.__date || util.parseDate(c1.updated); - const c2Date = c2.__date || util.parseDate(c2.updated); - const dateCompare = c1Date - c2Date; - // Ensure drafts are at the end. There should only be one but in edge - // cases could be more. In the unlikely event two drafts are being - // compared, use the typical date compare. - if (c2.__draft && !c1.__draft ) { return -1; } - if (c1.__draft && !c2.__draft ) { return 1; } - if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; } - // If same date, fall back to sorting by id. - return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); - }); - } - - _createReplyComment(parent, content, opt_isEditing, - opt_unresolved) { - this.$.reporting.recordDraftInteraction(); - const reply = this._newReply( - this._orderedComments[this._orderedComments.length - 1].id, - parent.line, - content, - opt_unresolved, - parent.range); - - // If there is currently a comment in an editing state, add an attribute - // so that the gr-comment knows not to populate the draft text. - for (let i = 0; i < this.comments.length; i++) { - if (this.comments[i].__editing) { - reply.__otherEditing = true; - break; - } - } - - if (opt_isEditing) { - reply.__editing = true; - } - - this.push('comments', reply); - - if (!opt_isEditing) { - // Allow the reply to render in the dom-repeat. - this.async(() => { - const commentEl = this._commentElWithDraftID(reply.__draftID); - commentEl.save(); - }, 1); - } - } - - _isDraft(comment) { - return !!comment.__draft; - } - - /** - * @param {boolean=} opt_quote - */ - _processCommentReply(opt_quote) { - const comment = this._lastComment; - let quoteStr; - if (opt_quote) { - const msg = comment.message; - quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; - } - this._createReplyComment(comment, quoteStr, true, comment.unresolved); - } - - _handleCommentReply(e) { - this._processCommentReply(); - } - - _handleCommentQuote(e) { - this._processCommentReply(true); - } - - _handleCommentAck(e) { - const comment = this._lastComment; - this._createReplyComment(comment, 'Ack', false, false); - } - - _handleCommentDone(e) { - const comment = this._lastComment; - this._createReplyComment(comment, 'Done', false, false); - } - - _handleCommentFix(e) { - const comment = e.detail.comment; - const msg = comment.message; - const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; - const response = quoteStr + 'Please fix.'; - this._createReplyComment(comment, response, false, true); - } - - _commentElWithDraftID(id) { - const els = Polymer.dom(this.root).querySelectorAll('gr-comment'); - for (const el of els) { - if (el.comment.id === id || el.comment.__draftID === id) { - return el; - } - } - return null; - } - - _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved, - opt_range) { - const d = this._newDraft(opt_lineNum); - d.in_reply_to = inReplyTo; - d.range = opt_range; - if (opt_message != null) { - d.message = opt_message; - } - if (opt_unresolved !== undefined) { - d.unresolved = opt_unresolved; - } - return d; - } - - /** - * @param {number=} opt_lineNum - * @param {!Object=} opt_range - */ - _newDraft(opt_lineNum, opt_range) { - const d = { - __draft: true, - __draftID: Math.random().toString(36), - __date: new Date(), - path: this.path, - patchNum: this.patchNum, - side: this._getSide(this.isOnParent), - __commentSide: this.commentSide, - }; - if (opt_lineNum) { - d.line = opt_lineNum; - } - if (opt_range) { - d.range = opt_range; - } - if (this.parentIndex) { - d.parent = this.parentIndex; - } - return d; - } - - _getSide(isOnParent) { - if (isOnParent) { return 'PARENT'; } - return 'REVISION'; - } - - _computeRootId(comments) { - // Keep the root ID even if the comment was removed, so that notification - // to sync will know which thread to remove. - if (!comments.base.length) { return this.rootId; } - const rootComment = comments.base[0]; - return rootComment.id || rootComment.__draftID; - } - - _handleCommentDiscard(e) { - const diffCommentEl = Polymer.dom(e).rootTarget; - const comment = diffCommentEl.comment; - const idx = this._indexOf(comment, this.comments); - if (idx == -1) { - throw Error('Cannot find comment ' + - JSON.stringify(diffCommentEl.comment)); - } - this.splice('comments', idx, 1); - if (this.comments.length === 0) { - this.fireRemoveSelf(); - } - this._handleCommentSavedOrDiscarded(e); - - // Check to see if there are any other open comments getting edited and - // set the local storage value to its message value. - for (const changeComment of this.comments) { - if (changeComment.__editing) { - const commentLocation = { - changeNum: this.changeNum, - patchNum: this.patchNum, - path: changeComment.path, - line: changeComment.line, - }; - return this.$.storage.setDraftComment(commentLocation, - changeComment.message); - } - } - } - - _handleCommentSavedOrDiscarded(e) { - this.dispatchEvent(new CustomEvent('thread-changed', - {detail: {rootId: this.rootId, path: this.path}, - bubbles: false})); - } - - _handleCommentUpdate(e) { - const comment = e.detail.comment; - const index = this._indexOf(comment, this.comments); - if (index === -1) { - // This should never happen: comment belongs to another thread. - console.warn('Comment update for another comment thread.'); - return; - } - this.set(['comments', index], comment); - // Because of the way we pass these comment objects around by-ref, in - // combination with the fact that Polymer does dirty checking in - // observers, the this.set() call above will not cause a thread update in - // some situations. - this.updateThreadProperties(); - } - - _indexOf(comment, arr) { - for (let i = 0; i < arr.length; i++) { - const c = arr[i]; - if ((c.__draftID != null && c.__draftID == comment.__draftID) || - (c.id != null && c.id == comment.id)) { - return i; - } - } - return -1; - } - - _computeHostClass(unresolved) { - if (this.isRobotComment) { - return 'robotComment'; - } - return unresolved ? 'unresolved' : ''; - } - - /** - * Load the project config when a project name has been provided. - * - * @param {string} name The project name. - */ - _projectNameChanged(name) { - if (!name) { return; } - this.$.restAPI.getProjectConfig(name).then(config => { - this._projectConfig = config; - }); + // If the comment was collapsed, re-open it to make it clear which + // actions are available. + commentEl.collapsed = false; + } else { + const range = opt_range ? opt_range : + lastComment ? lastComment.range : undefined; + const unresolved = lastComment ? lastComment.unresolved : undefined; + this.addDraft(opt_lineNum, range, unresolved); } } - customElements.define(GrCommentThread.is, GrCommentThread); -})(); + addDraft(opt_lineNum, opt_range, opt_unresolved) { + const draft = this._newDraft(opt_lineNum, opt_range); + draft.__editing = true; + draft.unresolved = opt_unresolved === false ? opt_unresolved : true; + this.push('comments', draft); + } + + fireRemoveSelf() { + this.dispatchEvent(new CustomEvent('thread-discard', + {detail: {rootId: this.rootId}, bubbles: false})); + } + + _getDiffUrlForComment(projectName, changeNum, path, patchNum) { + return Gerrit.Nav.getUrlForDiffById(changeNum, + projectName, path, patchNum, + null, this.lineNum); + } + + _computeDisplayPath(path) { + const lineString = this.lineNum ? `#${this.lineNum}` : ''; + return this.computeDisplayPath(path) + lineString; + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _commentsChanged() { + this._orderedComments = this._sortedComments(this.comments); + this.updateThreadProperties(); + } + + updateThreadProperties() { + if (this._orderedComments.length) { + this._lastComment = this._getLastComment(); + this.unresolved = this._lastComment.unresolved; + this.hasDraft = this._lastComment.__draft; + this.isRobotComment = !!(this._lastComment.robot_id); + } + } + + _shouldDisableAction(_showActions, _lastComment) { + return !_showActions || !_lastComment || !!_lastComment.__draft; + } + + _hideActions(_showActions, _lastComment) { + return this._shouldDisableAction(_showActions, _lastComment) || + !!_lastComment.robot_id; + } + + _getLastComment() { + return this._orderedComments[this._orderedComments.length - 1] || {}; + } + + _handleEKey(e) { + if (this.shouldSuppressKeyboardShortcut(e)) { return; } + + // Don’t preventDefault in this case because it will render the event + // useless for other handlers (other gr-comment-thread elements). + if (e.detail.keyboardEvent.shiftKey) { + this._expandCollapseComments(true); + } else { + if (this.modifierPressed(e)) { return; } + this._expandCollapseComments(false); + } + } + + _expandCollapseComments(actionIsCollapse) { + const comments = + dom(this.root).querySelectorAll('gr-comment'); + for (const comment of comments) { + comment.collapsed = actionIsCollapse; + } + } + + /** + * Sets the initial state of the comment thread. + * Expands the thread if one of the following is true: + * - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the + * thread is unresolved, + * - it's a robot comment. + */ + _setInitialExpandedState() { + if (this._orderedComments) { + for (let i = 0; i < this._orderedComments.length; i++) { + const comment = this._orderedComments[i]; + const isRobotComment = !!comment.robot_id; + // False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT. + const resolvedThread = !this.unresolved || + this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT; + comment.collapsed = !isRobotComment && resolvedThread; + } + } + } + + _sortedComments(comments) { + return comments.slice().sort((c1, c2) => { + const c1Date = c1.__date || util.parseDate(c1.updated); + const c2Date = c2.__date || util.parseDate(c2.updated); + const dateCompare = c1Date - c2Date; + // Ensure drafts are at the end. There should only be one but in edge + // cases could be more. In the unlikely event two drafts are being + // compared, use the typical date compare. + if (c2.__draft && !c1.__draft ) { return -1; } + if (c1.__draft && !c2.__draft ) { return 1; } + if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; } + // If same date, fall back to sorting by id. + return dateCompare ? dateCompare : c1.id.localeCompare(c2.id); + }); + } + + _createReplyComment(parent, content, opt_isEditing, + opt_unresolved) { + this.$.reporting.recordDraftInteraction(); + const reply = this._newReply( + this._orderedComments[this._orderedComments.length - 1].id, + parent.line, + content, + opt_unresolved, + parent.range); + + // If there is currently a comment in an editing state, add an attribute + // so that the gr-comment knows not to populate the draft text. + for (let i = 0; i < this.comments.length; i++) { + if (this.comments[i].__editing) { + reply.__otherEditing = true; + break; + } + } + + if (opt_isEditing) { + reply.__editing = true; + } + + this.push('comments', reply); + + if (!opt_isEditing) { + // Allow the reply to render in the dom-repeat. + this.async(() => { + const commentEl = this._commentElWithDraftID(reply.__draftID); + commentEl.save(); + }, 1); + } + } + + _isDraft(comment) { + return !!comment.__draft; + } + + /** + * @param {boolean=} opt_quote + */ + _processCommentReply(opt_quote) { + const comment = this._lastComment; + let quoteStr; + if (opt_quote) { + const msg = comment.message; + quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; + } + this._createReplyComment(comment, quoteStr, true, comment.unresolved); + } + + _handleCommentReply(e) { + this._processCommentReply(); + } + + _handleCommentQuote(e) { + this._processCommentReply(true); + } + + _handleCommentAck(e) { + const comment = this._lastComment; + this._createReplyComment(comment, 'Ack', false, false); + } + + _handleCommentDone(e) { + const comment = this._lastComment; + this._createReplyComment(comment, 'Done', false, false); + } + + _handleCommentFix(e) { + const comment = e.detail.comment; + const msg = comment.message; + const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n'; + const response = quoteStr + 'Please fix.'; + this._createReplyComment(comment, response, false, true); + } + + _commentElWithDraftID(id) { + const els = dom(this.root).querySelectorAll('gr-comment'); + for (const el of els) { + if (el.comment.id === id || el.comment.__draftID === id) { + return el; + } + } + return null; + } + + _newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved, + opt_range) { + const d = this._newDraft(opt_lineNum); + d.in_reply_to = inReplyTo; + d.range = opt_range; + if (opt_message != null) { + d.message = opt_message; + } + if (opt_unresolved !== undefined) { + d.unresolved = opt_unresolved; + } + return d; + } + + /** + * @param {number=} opt_lineNum + * @param {!Object=} opt_range + */ + _newDraft(opt_lineNum, opt_range) { + const d = { + __draft: true, + __draftID: Math.random().toString(36), + __date: new Date(), + path: this.path, + patchNum: this.patchNum, + side: this._getSide(this.isOnParent), + __commentSide: this.commentSide, + }; + if (opt_lineNum) { + d.line = opt_lineNum; + } + if (opt_range) { + d.range = opt_range; + } + if (this.parentIndex) { + d.parent = this.parentIndex; + } + return d; + } + + _getSide(isOnParent) { + if (isOnParent) { return 'PARENT'; } + return 'REVISION'; + } + + _computeRootId(comments) { + // Keep the root ID even if the comment was removed, so that notification + // to sync will know which thread to remove. + if (!comments.base.length) { return this.rootId; } + const rootComment = comments.base[0]; + return rootComment.id || rootComment.__draftID; + } + + _handleCommentDiscard(e) { + const diffCommentEl = dom(e).rootTarget; + const comment = diffCommentEl.comment; + const idx = this._indexOf(comment, this.comments); + if (idx == -1) { + throw Error('Cannot find comment ' + + JSON.stringify(diffCommentEl.comment)); + } + this.splice('comments', idx, 1); + if (this.comments.length === 0) { + this.fireRemoveSelf(); + } + this._handleCommentSavedOrDiscarded(e); + + // Check to see if there are any other open comments getting edited and + // set the local storage value to its message value. + for (const changeComment of this.comments) { + if (changeComment.__editing) { + const commentLocation = { + changeNum: this.changeNum, + patchNum: this.patchNum, + path: changeComment.path, + line: changeComment.line, + }; + return this.$.storage.setDraftComment(commentLocation, + changeComment.message); + } + } + } + + _handleCommentSavedOrDiscarded(e) { + this.dispatchEvent(new CustomEvent('thread-changed', + {detail: {rootId: this.rootId, path: this.path}, + bubbles: false})); + } + + _handleCommentUpdate(e) { + const comment = e.detail.comment; + const index = this._indexOf(comment, this.comments); + if (index === -1) { + // This should never happen: comment belongs to another thread. + console.warn('Comment update for another comment thread.'); + return; + } + this.set(['comments', index], comment); + // Because of the way we pass these comment objects around by-ref, in + // combination with the fact that Polymer does dirty checking in + // observers, the this.set() call above will not cause a thread update in + // some situations. + this.updateThreadProperties(); + } + + _indexOf(comment, arr) { + for (let i = 0; i < arr.length; i++) { + const c = arr[i]; + if ((c.__draftID != null && c.__draftID == comment.__draftID) || + (c.id != null && c.id == comment.id)) { + return i; + } + } + return -1; + } + + _computeHostClass(unresolved) { + if (this.isRobotComment) { + return 'robotComment'; + } + return unresolved ? 'unresolved' : ''; + } + + /** + * Load the project config when a project name has been provided. + * + * @param {string} name The project name. + */ + _projectNameChanged(name) { + if (!name) { return; } + this.$.restAPI.getProjectConfig(name).then(config => { + this._projectConfig = config; + }); + } +} + +customElements.define(GrCommentThread.is, GrCommentThread);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js index d615a7f..1d991cb 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js +++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_html.js
@@ -1,32 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-storage/gr-storage.html"> -<link rel="import" href="../gr-comment/gr-comment.html"> - -<dom-module id="gr-comment-thread"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { font-family: var(--font-family); @@ -83,58 +73,25 @@ </style> <template is="dom-if" if="[[showFilePath]]"> <div class="pathInfo"> - <a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a> + <a href\$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a> <span class="descriptionText">Patchset [[patchNum]]</span> </div> </template> - <div id="container" class$="[[_computeHostClass(unresolved, isRobotComment)]]"> - <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" - as="comment"> - <gr-comment - comment="{{comment}}" - comments="{{comments}}" - robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]" - change-num="[[changeNum]]" - patch-num="[[patchNum]]" - draft="[[_isDraft(comment)]]" - show-actions="[[_showActions]]" - comment-side="[[comment.__commentSide]]" - side="[[comment.side]]" - project-config="[[_projectConfig]]" - on-create-fix-comment="_handleCommentFix" - on-comment-discard="_handleCommentDiscard" - on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment> + <div id="container" class\$="[[_computeHostClass(unresolved, isRobotComment)]]"> + <template id="commentList" is="dom-repeat" items="[[_orderedComments]]" as="comment"> + <gr-comment comment="{{comment}}" comments="{{comments}}" robot-button-disabled="[[_shouldDisableAction(_showActions, _lastComment)]]" change-num="[[changeNum]]" patch-num="[[patchNum]]" draft="[[_isDraft(comment)]]" show-actions="[[_showActions]]" comment-side="[[comment.__commentSide]]" side="[[comment.side]]" project-config="[[_projectConfig]]" on-create-fix-comment="_handleCommentFix" on-comment-discard="_handleCommentDiscard" on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment> </template> - <div id="commentInfoContainer" - hidden$="[[_hideActions(_showActions, _lastComment)]]"> - <span id="unresolvedLabel" hidden$="[[!unresolved]]">Unresolved</span> + <div id="commentInfoContainer" hidden\$="[[_hideActions(_showActions, _lastComment)]]"> + <span id="unresolvedLabel" hidden\$="[[!unresolved]]">Unresolved</span> <div id="actions"> - <gr-button - id="replyBtn" - link - class="action reply" - on-click="_handleCommentReply">Reply</gr-button> - <gr-button - id="quoteBtn" - link - class="action quote" - on-click="_handleCommentQuote">Quote</gr-button> - <gr-button - id="ackBtn" - link - class="action ack" - on-click="_handleCommentAck">Ack</gr-button> - <gr-button - id="doneBtn" - link - class="action done" - on-click="_handleCommentDone">Done</gr-button> + <gr-button id="replyBtn" link="" class="action reply" on-click="_handleCommentReply">Reply</gr-button> + <gr-button id="quoteBtn" link="" class="action quote" on-click="_handleCommentQuote">Quote</gr-button> + <gr-button id="ackBtn" link="" class="action ack" on-click="_handleCommentAck">Ack</gr-button> + <gr-button id="doneBtn" link="" class="action done" on-click="_handleCommentDone">Done</gr-button> </div> </div> </div> <gr-reporting id="reporting"></gr-reporting> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> - </template> - <script src="gr-comment-thread.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html index a17a174..bb71869 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html +++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment-thread</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-comment-thread.html"> +<script type="module" src="./gr-comment-thread.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-comment-thread.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -43,197 +49,14 @@ </template> </test-fixture> -<script> - suite('gr-comment-thread tests', async () => { - await readyToTest(); - - suite('basic test', () => { - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getLoggedIn() { return Promise.resolve(false); }, - }); - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('comments are sorted correctly', () => { - const comments = [ - { - message: 'i like you, too', - in_reply_to: 'sallys_confession', - __date: new Date('2015-12-25'), - }, { - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-24 15:00:20.396000000', - }, { - id: 'sally_to_dr_finklestein', - message: 'i’m running away', - updated: '2015-10-31 09:00:20.396000000', - }, { - id: 'sallys_defiance', - in_reply_to: 'sally_to_dr_finklestein', - message: 'i will poison you so i can get away', - updated: '2015-10-31 15:00:20.396000000', - }, { - id: 'dr_finklesteins_response', - in_reply_to: 'sally_to_dr_finklestein', - message: 'no i will pull a thread and your arm will fall off', - updated: '2015-10-31 11:00:20.396000000', - }, { - id: 'sallys_mission', - message: 'i have to find santa', - updated: '2015-12-24 15:00:20.396000000', - }, - ]; - const results = element._sortedComments(comments); - assert.deepEqual(results, [ - { - id: 'sally_to_dr_finklestein', - message: 'i’m running away', - updated: '2015-10-31 09:00:20.396000000', - }, { - id: 'dr_finklesteins_response', - in_reply_to: 'sally_to_dr_finklestein', - message: 'no i will pull a thread and your arm will fall off', - updated: '2015-10-31 11:00:20.396000000', - }, { - id: 'sallys_defiance', - in_reply_to: 'sally_to_dr_finklestein', - message: 'i will poison you so i can get away', - updated: '2015-10-31 15:00:20.396000000', - }, { - id: 'sallys_confession', - message: 'i like you, jack', - updated: '2015-12-24 15:00:20.396000000', - }, { - id: 'sallys_mission', - message: 'i have to find santa', - updated: '2015-12-24 15:00:20.396000000', - }, { - message: 'i like you, too', - in_reply_to: 'sallys_confession', - __date: new Date('2015-12-25'), - }, - ]); - }); - - test('addOrEditDraft w/ edit draft', () => { - element.comments = [{ - id: 'jacks_reply', - message: 'i like you, too', - in_reply_to: 'sallys_confession', - updated: '2015-12-25 15:00:20.396000000', - __draft: true, - }]; - const commentElStub = sandbox.stub(element, '_commentElWithDraftID', - () => { return {}; }); - const addDraftStub = sandbox.stub(element, 'addDraft'); - - element.addOrEditDraft(123); - - assert.isTrue(commentElStub.called); - assert.isFalse(addDraftStub.called); - }); - - test('addOrEditDraft w/o edit draft', () => { - element.comments = []; - const commentElStub = sandbox.stub(element, '_commentElWithDraftID', - () => { return {}; }); - const addDraftStub = sandbox.stub(element, 'addDraft'); - - element.addOrEditDraft(123); - - assert.isFalse(commentElStub.called); - assert.isTrue(addDraftStub.called); - }); - - test('_shouldDisableAction', () => { - let showActions = true; - const lastComment = {}; - assert.equal( - element._shouldDisableAction(showActions, lastComment), false); - showActions = false; - assert.equal( - element._shouldDisableAction(showActions, lastComment), true); - showActions = true; - lastComment.__draft = true; - assert.equal( - element._shouldDisableAction(showActions, lastComment), true); - const robotComment = {}; - robotComment.robot_id = true; - assert.equal( - element._shouldDisableAction(showActions, robotComment), false); - }); - - test('_hideActions', () => { - let showActions = true; - const lastComment = {}; - assert.equal(element._hideActions(showActions, lastComment), false); - showActions = false; - assert.equal(element._hideActions(showActions, lastComment), true); - showActions = true; - lastComment.__draft = true; - assert.equal(element._hideActions(showActions, lastComment), true); - const robotComment = {}; - robotComment.robot_id = true; - assert.equal(element._hideActions(showActions, robotComment), true); - }); - - test('setting project name loads the project config', done => { - const projectName = 'foo/bar/baz'; - const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig') - .returns(Promise.resolve({})); - element.projectName = projectName; - flush(() => { - assert.isTrue(getProjectStub.calledWithExactly(projectName)); - done(); - }); - }); - - test('optionally show file path', () => { - // Path info doesn't exist when showFilePath is false. Because it's in a - // dom-if it is not yet in the dom. - assert.isNotOk(element.shadowRoot - .querySelector('.pathInfo')); - - sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); - element.changeNum = 123; - element.projectName = 'test project'; - element.path = 'path/to/file'; - element.patchNum = 3; - element.lineNum = 5; - element.showFilePath = true; - flushAsynchronousOperations(); - assert.isOk(element.shadowRoot - .querySelector('.pathInfo')); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.pathInfo')).display, - 'none'); - assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly( - element.changeNum, element.projectName, element.path, - element.patchNum, null, element.lineNum)); - }); - - test('_computeDisplayPath', () => { - const path = 'path/to/file'; - assert.equal(element._computeDisplayPath(path), 'path/to/file'); - - element.lineNum = 5; - assert.equal(element._computeDisplayPath(path), 'path/to/file#5'); - }); - }); - }); - - suite('comment action tests', () => { +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-comment-thread.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-comment-thread tests', () => { + suite('basic test', () => { let element; let sandbox; @@ -241,535 +64,721 @@ sandbox = sinon.sandbox.create(); stub('gr-rest-api-interface', { getLoggedIn() { return Promise.resolve(false); }, - saveDiffDraft() { - return Promise.resolve({ - ok: true, - text() { - return Promise.resolve(')]}\'\n' + - JSON.stringify({ - id: '7afa4931_de3d65bd', - path: '/path/to/file.txt', - line: 5, - in_reply_to: 'baf0414d_60047215', - updated: '2015-12-21 02:01:10.850000000', - message: 'Done', - })); - }, - }); - }, - deleteDiffDraft() { return Promise.resolve({ok: true}); }, }); - element = fixture('withComment'); - element.comments = [{ - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - id: 'baf0414d_60047215', - line: 5, - message: 'is this a crossover episode!?', - updated: '2015-12-08 19:48:33.843000000', - path: '/path/to/file.txt', - }]; - flushAsynchronousOperations(); + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); teardown(() => { sandbox.restore(); }); - test('reply', () => { - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - assert.ok(commentEl); - - const replyBtn = element.$.replyBtn; - MockInteractions.tap(replyBtn); - flushAsynchronousOperations(); - - const drafts = element._orderedComments.filter(c => c.__draft == true); - assert.equal(drafts.length, 1); - assert.notOk(drafts[0].message, 'message should be empty'); - assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); - assert.isTrue(reportStub.calledOnce); - }); - - test('quote reply', () => { - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - assert.ok(commentEl); - - const quoteBtn = element.$.quoteBtn; - MockInteractions.tap(quoteBtn); - flushAsynchronousOperations(); - - const drafts = element._orderedComments.filter(c => c.__draft == true); - assert.equal(drafts.length, 1); - assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n'); - assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); - assert.isTrue(reportStub.calledOnce); - }); - - test('quote reply multiline', () => { - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - element.comments = [{ - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', + test('comments are sorted correctly', () => { + const comments = [ + { + message: 'i like you, too', + in_reply_to: 'sallys_confession', + __date: new Date('2015-12-25'), + }, { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, { + id: 'sally_to_dr_finklestein', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, { + id: 'sallys_defiance', + in_reply_to: 'sally_to_dr_finklestein', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }, { + id: 'dr_finklesteins_response', + in_reply_to: 'sally_to_dr_finklestein', + message: 'no i will pull a thread and your arm will fall off', + updated: '2015-10-31 11:00:20.396000000', + }, { + id: 'sallys_mission', + message: 'i have to find santa', + updated: '2015-12-24 15:00:20.396000000', }, - id: 'baf0414d_60047215', - line: 5, - message: 'is this a crossover episode!?\nIt might be!', - updated: '2015-12-08 19:48:33.843000000', - }]; - flushAsynchronousOperations(); - - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - assert.ok(commentEl); - - const quoteBtn = element.$.quoteBtn; - MockInteractions.tap(quoteBtn); - flushAsynchronousOperations(); - - const drafts = element._orderedComments.filter(c => c.__draft == true); - assert.equal(drafts.length, 1); - assert.equal(drafts[0].message, - '> is this a crossover episode!?\n> It might be!\n\n'); - assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); - assert.isTrue(reportStub.calledOnce); - }); - - test('ack', done => { - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - element.changeNum = '42'; - element.patchNum = '1'; - - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - assert.ok(commentEl); - - const ackBtn = element.$.ackBtn; - MockInteractions.tap(ackBtn); - flush(() => { - const drafts = element.comments.filter(c => c.__draft == true); - assert.equal(drafts.length, 1); - assert.equal(drafts[0].message, 'Ack'); - assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); - assert.equal(drafts[0].unresolved, false); - assert.isTrue(reportStub.calledOnce); - done(); - }); - }); - - test('done', done => { - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - element.changeNum = '42'; - element.patchNum = '1'; - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - assert.ok(commentEl); - - const doneBtn = element.$.doneBtn; - MockInteractions.tap(doneBtn); - flush(() => { - const drafts = element.comments.filter(c => c.__draft == true); - assert.equal(drafts.length, 1); - assert.equal(drafts[0].message, 'Done'); - assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); - assert.isFalse(drafts[0].unresolved); - assert.isTrue(reportStub.calledOnce); - done(); - }); - }); - - test('save', done => { - element.changeNum = '42'; - element.patchNum = '1'; - element.path = '/path/to/file.txt'; - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - assert.ok(commentEl); - - const saveOrDiscardStub = sandbox.stub(); - element.addEventListener('thread-changed', saveOrDiscardStub); - element.shadowRoot - .querySelector('gr-comment')._fireSave(); - - flush(() => { - assert.isTrue(saveOrDiscardStub.called); - assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId, - 'baf0414d_60047215'); - assert.equal(element.rootId, 'baf0414d_60047215'); - assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path, - '/path/to/file.txt'); - done(); - }); - }); - - test('please fix', done => { - element.changeNum = '42'; - element.patchNum = '1'; - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - assert.ok(commentEl); - commentEl.addEventListener('create-fix-comment', () => { - const drafts = element._orderedComments.filter(c => c.__draft == true); - assert.equal(drafts.length, 1); - assert.equal( - drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.'); - assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); - assert.isTrue(drafts[0].unresolved); - done(); - }); - commentEl.fire('create-fix-comment', {comment: commentEl.comment}, - {bubbles: false}); - }); - - test('discard', done => { - element.changeNum = '42'; - element.patchNum = '1'; - element.path = '/path/to/file.txt'; - element.push('comments', element._newReply( - element.comments[0].id, - element.comments[0].line, - element.comments[0].path, - 'it’s pronouced jiff, not giff')); - flushAsynchronousOperations(); - - const saveOrDiscardStub = sandbox.stub(); - element.addEventListener('thread-changed', saveOrDiscardStub); - const draftEl = - Polymer.dom(element.root).querySelectorAll('gr-comment')[1]; - assert.ok(draftEl); - draftEl.addEventListener('comment-discard', () => { - const drafts = element.comments.filter(c => c.__draft == true); - assert.equal(drafts.length, 0); - assert.isTrue(saveOrDiscardStub.called); - assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId, - element.rootId); - assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path, - element.path); - done(); - }); - draftEl.fire('comment-discard', {comment: draftEl.comment}, - {bubbles: false}); - }); - - test('discard with a single comment still fires event with previous rootId', - done => { - element.changeNum = '42'; - element.patchNum = '1'; - element.path = '/path/to/file.txt'; - element.comments = []; - element.addOrEditDraft('1'); - flushAsynchronousOperations(); - const rootId = element.rootId; - assert.isOk(rootId); - - const saveOrDiscardStub = sandbox.stub(); - element.addEventListener('thread-changed', saveOrDiscardStub); - const draftEl = - Polymer.dom(element.root).querySelectorAll('gr-comment')[0]; - assert.ok(draftEl); - draftEl.addEventListener('comment-discard', () => { - assert.equal(element.comments.length, 0); - assert.isTrue(saveOrDiscardStub.called); - assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId, - rootId); - assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path, - element.path); - done(); - }); - draftEl.fire('comment-discard', {comment: draftEl.comment}, - {bubbles: false}); - }); - - test('first editing comment does not add __otherEditing attribute', () => { - element.comments = [{ - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', + ]; + const results = element._sortedComments(comments); + assert.deepEqual(results, [ + { + id: 'sally_to_dr_finklestein', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, { + id: 'dr_finklesteins_response', + in_reply_to: 'sally_to_dr_finklestein', + message: 'no i will pull a thread and your arm will fall off', + updated: '2015-10-31 11:00:20.396000000', + }, { + id: 'sallys_defiance', + in_reply_to: 'sally_to_dr_finklestein', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }, { + id: 'sallys_confession', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, { + id: 'sallys_mission', + message: 'i have to find santa', + updated: '2015-12-24 15:00:20.396000000', + }, { + message: 'i like you, too', + in_reply_to: 'sallys_confession', + __date: new Date('2015-12-25'), }, - id: 'baf0414d_60047215', - line: 5, - message: 'is this a crossover episode!?', - updated: '2015-12-08 19:48:33.843000000', + ]); + }); + + test('addOrEditDraft w/ edit draft', () => { + element.comments = [{ + id: 'jacks_reply', + message: 'i like you, too', + in_reply_to: 'sallys_confession', + updated: '2015-12-25 15:00:20.396000000', __draft: true, }]; + const commentElStub = sandbox.stub(element, '_commentElWithDraftID', + () => { return {}; }); + const addDraftStub = sandbox.stub(element, 'addDraft'); - const replyBtn = element.$.replyBtn; - MockInteractions.tap(replyBtn); - flushAsynchronousOperations(); + element.addOrEditDraft(123); - const editing = element._orderedComments.filter(c => c.__editing == true); - assert.equal(editing.length, 1); - assert.equal(!!editing[0].__otherEditing, false); + assert.isTrue(commentElStub.called); + assert.isFalse(addDraftStub.called); }); - test('When not editing other comments, local storage not set' + - ' after discard', done => { - element.changeNum = '42'; - element.patchNum = '1'; - element.comments = [{ - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - id: 'baf0414d_60047215', - line: 5, - message: 'is this a crossover episode!?', - updated: '2015-12-08 19:48:31.843000000', - }, - { - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - __draftID: '1', - in_reply_to: 'baf0414d_60047215', - line: 5, - message: 'yes', - updated: '2015-12-08 19:48:32.843000000', - __draft: true, - __editing: true, - }, - { - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - __draftID: '2', - in_reply_to: 'baf0414d_60047215', - line: 5, - message: 'no', - updated: '2015-12-08 19:48:33.843000000', - __draft: true, - }]; - const storageStub = sinon.stub(element.$.storage, 'setDraftComment'); - flushAsynchronousOperations(); + test('addOrEditDraft w/o edit draft', () => { + element.comments = []; + const commentElStub = sandbox.stub(element, '_commentElWithDraftID', + () => { return {}; }); + const addDraftStub = sandbox.stub(element, 'addDraft'); - const draftEl = - Polymer.dom(element.root).querySelectorAll('gr-comment')[1]; - assert.ok(draftEl); - draftEl.addEventListener('comment-discard', () => { - assert.isFalse(storageStub.called); - storageStub.restore(); + element.addOrEditDraft(123); + + assert.isFalse(commentElStub.called); + assert.isTrue(addDraftStub.called); + }); + + test('_shouldDisableAction', () => { + let showActions = true; + const lastComment = {}; + assert.equal( + element._shouldDisableAction(showActions, lastComment), false); + showActions = false; + assert.equal( + element._shouldDisableAction(showActions, lastComment), true); + showActions = true; + lastComment.__draft = true; + assert.equal( + element._shouldDisableAction(showActions, lastComment), true); + const robotComment = {}; + robotComment.robot_id = true; + assert.equal( + element._shouldDisableAction(showActions, robotComment), false); + }); + + test('_hideActions', () => { + let showActions = true; + const lastComment = {}; + assert.equal(element._hideActions(showActions, lastComment), false); + showActions = false; + assert.equal(element._hideActions(showActions, lastComment), true); + showActions = true; + lastComment.__draft = true; + assert.equal(element._hideActions(showActions, lastComment), true); + const robotComment = {}; + robotComment.robot_id = true; + assert.equal(element._hideActions(showActions, robotComment), true); + }); + + test('setting project name loads the project config', done => { + const projectName = 'foo/bar/baz'; + const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig') + .returns(Promise.resolve({})); + element.projectName = projectName; + flush(() => { + assert.isTrue(getProjectStub.calledWithExactly(projectName)); done(); }); - draftEl.fire('comment-discard', {comment: draftEl.comment}, - {bubbles: false}); }); - test('comment-update', () => { - const commentEl = element.shadowRoot - .querySelector('gr-comment'); - const updatedComment = { - id: element.comments[0].id, - foo: 'bar', - }; - commentEl.fire('comment-update', {comment: updatedComment}); - assert.strictEqual(element.comments[0], updatedComment); - }); + test('optionally show file path', () => { + // Path info doesn't exist when showFilePath is false. Because it's in a + // dom-if it is not yet in the dom. + assert.isNotOk(element.shadowRoot + .querySelector('.pathInfo')); - suite('jack and sally comment data test consolidation', () => { - setup(() => { - element.comments = [ - { - id: 'jacks_reply', - message: 'i like you, too', - in_reply_to: 'sallys_confession', - updated: '2015-12-25 15:00:20.396000000', - unresolved: false, - }, { - id: 'sallys_confession', - in_reply_to: 'nonexistent_comment', - message: 'i like you, jack', - updated: '2015-12-24 15:00:20.396000000', - }, { - id: 'sally_to_dr_finklestein', - in_reply_to: 'nonexistent_comment', - message: 'i’m running away', - updated: '2015-10-31 09:00:20.396000000', - }, { - id: 'sallys_defiance', - message: 'i will poison you so i can get away', - updated: '2015-10-31 15:00:20.396000000', - }]; - }); - - test('orphan replies', () => { - assert.equal(4, element._orderedComments.length); - }); - - test('keyboard shortcuts', () => { - const expandCollapseStub = - sinon.stub(element, '_expandCollapseComments'); - MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e'); - assert.isTrue(expandCollapseStub.lastCall.calledWith(false)); - - MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e'); - assert.isTrue(expandCollapseStub.lastCall.calledWith(true)); - }); - - test('comment in_reply_to is either null or most recent comment', () => { - element._createReplyComment(element.comments[3], 'dummy', true); - flushAsynchronousOperations(); - assert.equal(element._orderedComments.length, 5); - assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply'); - }); - - test('resolvable comments', () => { - assert.isFalse(element.unresolved); - element._createReplyComment(element.comments[3], 'dummy', true, true); - flushAsynchronousOperations(); - assert.isTrue(element.unresolved); - }); - - test('_setInitialExpandedState', () => { - element.unresolved = true; - element._setInitialExpandedState(); - for (let i = 0; i < element.comments.length; i++) { - assert.isFalse(element.comments[i].collapsed); - } - element.unresolved = false; - element._setInitialExpandedState(); - for (let i = 0; i < element.comments.length; i++) { - assert.isTrue(element.comments[i].collapsed); - } - for (let i = 0; i < element.comments.length; i++) { - element.comments[i].robot_id = 123; - } - element._setInitialExpandedState(); - for (let i = 0; i < element.comments.length; i++) { - assert.isFalse(element.comments[i].collapsed); - } - }); - }); - - test('_computeHostClass', () => { - assert.equal(element._computeHostClass(true), 'unresolved'); - assert.equal(element._computeHostClass(false), ''); - }); - - test('addDraft sets unresolved state correctly', () => { - let unresolved = true; - element.comments = []; - element.addDraft(null, null, unresolved); - assert.equal(element.comments[0].unresolved, true); - - unresolved = false; // comment should get added as actually resolved. - element.comments = []; - element.addDraft(null, null, unresolved); - assert.equal(element.comments[0].unresolved, false); - - element.comments = []; - element.addDraft(); - assert.equal(element.comments[0].unresolved, true); - }); - - test('_newDraft', () => { - element.commentSide = 'left'; + sandbox.stub(Gerrit.Nav, 'getUrlForDiffById'); + element.changeNum = 123; + element.projectName = 'test project'; + element.path = 'path/to/file'; element.patchNum = 3; - const draft = element._newDraft(); - assert.equal(draft.__commentSide, 'left'); - assert.equal(draft.patchNum, 3); + element.lineNum = 5; + element.showFilePath = true; + flushAsynchronousOperations(); + assert.isOk(element.shadowRoot + .querySelector('.pathInfo')); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.pathInfo')).display, + 'none'); + assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly( + element.changeNum, element.projectName, element.path, + element.patchNum, null, element.lineNum)); }); - test('new comment gets created', () => { - element.comments = []; - element.addOrEditDraft(1); - assert.equal(element.comments.length, 1); - // Mock a submitted comment. - element.comments[0].id = element.comments[0].__draftID; - element.comments[0].__draft = false; - element.addOrEditDraft(1); - assert.equal(element.comments.length, 2); - }); + test('_computeDisplayPath', () => { + const path = 'path/to/file'; + assert.equal(element._computeDisplayPath(path), 'path/to/file'); - test('unresolved label', () => { - element.unresolved = false; - assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden')); - element.unresolved = true; - assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden')); - }); - - test('draft comments are at the end of orderedComments', () => { - element.comments = [{ - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - id: 2, - line: 5, - message: 'Earlier draft', - updated: '2015-12-08 19:48:33.843000000', - __draft: true, - }, - { - author: { - name: 'Mr. Peanutbutter2', - email: 'tenn1sballchaser@aol.com', - }, - id: 1, - line: 5, - message: 'This comment was left last but is not a draft', - updated: '2015-12-10 19:48:33.843000000', - }, - { - author: { - name: 'Mr. Peanutbutter2', - email: 'tenn1sballchaser@aol.com', - }, - id: 3, - line: 5, - message: 'Later draft', - updated: '2015-12-09 19:48:33.843000000', - __draft: true, - }]; - assert.equal(element._orderedComments[0].id, '1'); - assert.equal(element._orderedComments[1].id, '2'); - assert.equal(element._orderedComments[2].id, '3'); - }); - - test('reflects lineNum and commentSide to attributes', () => { - element.lineNum = 7; - element.commentSide = 'left'; - - assert.equal(element.getAttribute('line-num'), '7'); - assert.equal(element.getAttribute('comment-side'), 'left'); - }); - - test('reflects range to JSON serialized attribute if set', () => { - element.range = { - start_line: 4, - end_line: 5, - start_character: 6, - end_character: 7, - }; - - assert.deepEqual( - JSON.parse(element.getAttribute('range')), - {start_line: 4, end_line: 5, start_character: 6, end_character: 7}); - }); - - test('removes range attribute if range is unset', () => { - element.range = { - start_line: 4, - end_line: 5, - start_character: 6, - end_character: 7, - }; - element.range = undefined; - - assert.notOk(element.hasAttribute('range')); + element.lineNum = 5; + assert.equal(element._computeDisplayPath(path), 'path/to/file#5'); }); }); +}); + +suite('comment action tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getLoggedIn() { return Promise.resolve(false); }, + saveDiffDraft() { + return Promise.resolve({ + ok: true, + text() { + return Promise.resolve(')]}\'\n' + + JSON.stringify({ + id: '7afa4931_de3d65bd', + path: '/path/to/file.txt', + line: 5, + in_reply_to: 'baf0414d_60047215', + updated: '2015-12-21 02:01:10.850000000', + message: 'Done', + })); + }, + }); + }, + deleteDiffDraft() { return Promise.resolve({ok: true}); }, + }); + element = fixture('withComment'); + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + path: '/path/to/file.txt', + }]; + flushAsynchronousOperations(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('reply', () => { + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + assert.ok(commentEl); + + const replyBtn = element.$.replyBtn; + MockInteractions.tap(replyBtn); + flushAsynchronousOperations(); + + const drafts = element._orderedComments.filter(c => c.__draft == true); + assert.equal(drafts.length, 1); + assert.notOk(drafts[0].message, 'message should be empty'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.isTrue(reportStub.calledOnce); + }); + + test('quote reply', () => { + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + assert.ok(commentEl); + + const quoteBtn = element.$.quoteBtn; + MockInteractions.tap(quoteBtn); + flushAsynchronousOperations(); + + const drafts = element._orderedComments.filter(c => c.__draft == true); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.isTrue(reportStub.calledOnce); + }); + + test('quote reply multiline', () => { + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?\nIt might be!', + updated: '2015-12-08 19:48:33.843000000', + }]; + flushAsynchronousOperations(); + + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + assert.ok(commentEl); + + const quoteBtn = element.$.quoteBtn; + MockInteractions.tap(quoteBtn); + flushAsynchronousOperations(); + + const drafts = element._orderedComments.filter(c => c.__draft == true); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, + '> is this a crossover episode!?\n> It might be!\n\n'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.isTrue(reportStub.calledOnce); + }); + + test('ack', done => { + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + element.changeNum = '42'; + element.patchNum = '1'; + + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + assert.ok(commentEl); + + const ackBtn = element.$.ackBtn; + MockInteractions.tap(ackBtn); + flush(() => { + const drafts = element.comments.filter(c => c.__draft == true); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, 'Ack'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.equal(drafts[0].unresolved, false); + assert.isTrue(reportStub.calledOnce); + done(); + }); + }); + + test('done', done => { + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + element.changeNum = '42'; + element.patchNum = '1'; + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + assert.ok(commentEl); + + const doneBtn = element.$.doneBtn; + MockInteractions.tap(doneBtn); + flush(() => { + const drafts = element.comments.filter(c => c.__draft == true); + assert.equal(drafts.length, 1); + assert.equal(drafts[0].message, 'Done'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.isFalse(drafts[0].unresolved); + assert.isTrue(reportStub.calledOnce); + done(); + }); + }); + + test('save', done => { + element.changeNum = '42'; + element.patchNum = '1'; + element.path = '/path/to/file.txt'; + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + assert.ok(commentEl); + + const saveOrDiscardStub = sandbox.stub(); + element.addEventListener('thread-changed', saveOrDiscardStub); + element.shadowRoot + .querySelector('gr-comment')._fireSave(); + + flush(() => { + assert.isTrue(saveOrDiscardStub.called); + assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId, + 'baf0414d_60047215'); + assert.equal(element.rootId, 'baf0414d_60047215'); + assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path, + '/path/to/file.txt'); + done(); + }); + }); + + test('please fix', done => { + element.changeNum = '42'; + element.patchNum = '1'; + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + assert.ok(commentEl); + commentEl.addEventListener('create-fix-comment', () => { + const drafts = element._orderedComments.filter(c => c.__draft == true); + assert.equal(drafts.length, 1); + assert.equal( + drafts[0].message, '> is this a crossover episode!?\n\nPlease fix.'); + assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215'); + assert.isTrue(drafts[0].unresolved); + done(); + }); + commentEl.fire('create-fix-comment', {comment: commentEl.comment}, + {bubbles: false}); + }); + + test('discard', done => { + element.changeNum = '42'; + element.patchNum = '1'; + element.path = '/path/to/file.txt'; + element.push('comments', element._newReply( + element.comments[0].id, + element.comments[0].line, + element.comments[0].path, + 'it’s pronouced jiff, not giff')); + flushAsynchronousOperations(); + + const saveOrDiscardStub = sandbox.stub(); + element.addEventListener('thread-changed', saveOrDiscardStub); + const draftEl = + dom(element.root).querySelectorAll('gr-comment')[1]; + assert.ok(draftEl); + draftEl.addEventListener('comment-discard', () => { + const drafts = element.comments.filter(c => c.__draft == true); + assert.equal(drafts.length, 0); + assert.isTrue(saveOrDiscardStub.called); + assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId, + element.rootId); + assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path, + element.path); + done(); + }); + draftEl.fire('comment-discard', {comment: draftEl.comment}, + {bubbles: false}); + }); + + test('discard with a single comment still fires event with previous rootId', + done => { + element.changeNum = '42'; + element.patchNum = '1'; + element.path = '/path/to/file.txt'; + element.comments = []; + element.addOrEditDraft('1'); + flushAsynchronousOperations(); + const rootId = element.rootId; + assert.isOk(rootId); + + const saveOrDiscardStub = sandbox.stub(); + element.addEventListener('thread-changed', saveOrDiscardStub); + const draftEl = + dom(element.root).querySelectorAll('gr-comment')[0]; + assert.ok(draftEl); + draftEl.addEventListener('comment-discard', () => { + assert.equal(element.comments.length, 0); + assert.isTrue(saveOrDiscardStub.called); + assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId, + rootId); + assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path, + element.path); + done(); + }); + draftEl.fire('comment-discard', {comment: draftEl.comment}, + {bubbles: false}); + }); + + test('first editing comment does not add __otherEditing attribute', () => { + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + __draft: true, + }]; + + const replyBtn = element.$.replyBtn; + MockInteractions.tap(replyBtn); + flushAsynchronousOperations(); + + const editing = element._orderedComments.filter(c => c.__editing == true); + assert.equal(editing.length, 1); + assert.equal(!!editing[0].__otherEditing, false); + }); + + test('When not editing other comments, local storage not set' + + ' after discard', done => { + element.changeNum = '42'; + element.patchNum = '1'; + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:31.843000000', + }, + { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + __draftID: '1', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'yes', + updated: '2015-12-08 19:48:32.843000000', + __draft: true, + __editing: true, + }, + { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + __draftID: '2', + in_reply_to: 'baf0414d_60047215', + line: 5, + message: 'no', + updated: '2015-12-08 19:48:33.843000000', + __draft: true, + }]; + const storageStub = sinon.stub(element.$.storage, 'setDraftComment'); + flushAsynchronousOperations(); + + const draftEl = + dom(element.root).querySelectorAll('gr-comment')[1]; + assert.ok(draftEl); + draftEl.addEventListener('comment-discard', () => { + assert.isFalse(storageStub.called); + storageStub.restore(); + done(); + }); + draftEl.fire('comment-discard', {comment: draftEl.comment}, + {bubbles: false}); + }); + + test('comment-update', () => { + const commentEl = element.shadowRoot + .querySelector('gr-comment'); + const updatedComment = { + id: element.comments[0].id, + foo: 'bar', + }; + commentEl.fire('comment-update', {comment: updatedComment}); + assert.strictEqual(element.comments[0], updatedComment); + }); + + suite('jack and sally comment data test consolidation', () => { + setup(() => { + element.comments = [ + { + id: 'jacks_reply', + message: 'i like you, too', + in_reply_to: 'sallys_confession', + updated: '2015-12-25 15:00:20.396000000', + unresolved: false, + }, { + id: 'sallys_confession', + in_reply_to: 'nonexistent_comment', + message: 'i like you, jack', + updated: '2015-12-24 15:00:20.396000000', + }, { + id: 'sally_to_dr_finklestein', + in_reply_to: 'nonexistent_comment', + message: 'i’m running away', + updated: '2015-10-31 09:00:20.396000000', + }, { + id: 'sallys_defiance', + message: 'i will poison you so i can get away', + updated: '2015-10-31 15:00:20.396000000', + }]; + }); + + test('orphan replies', () => { + assert.equal(4, element._orderedComments.length); + }); + + test('keyboard shortcuts', () => { + const expandCollapseStub = + sinon.stub(element, '_expandCollapseComments'); + MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e'); + assert.isTrue(expandCollapseStub.lastCall.calledWith(false)); + + MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e'); + assert.isTrue(expandCollapseStub.lastCall.calledWith(true)); + }); + + test('comment in_reply_to is either null or most recent comment', () => { + element._createReplyComment(element.comments[3], 'dummy', true); + flushAsynchronousOperations(); + assert.equal(element._orderedComments.length, 5); + assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply'); + }); + + test('resolvable comments', () => { + assert.isFalse(element.unresolved); + element._createReplyComment(element.comments[3], 'dummy', true, true); + flushAsynchronousOperations(); + assert.isTrue(element.unresolved); + }); + + test('_setInitialExpandedState', () => { + element.unresolved = true; + element._setInitialExpandedState(); + for (let i = 0; i < element.comments.length; i++) { + assert.isFalse(element.comments[i].collapsed); + } + element.unresolved = false; + element._setInitialExpandedState(); + for (let i = 0; i < element.comments.length; i++) { + assert.isTrue(element.comments[i].collapsed); + } + for (let i = 0; i < element.comments.length; i++) { + element.comments[i].robot_id = 123; + } + element._setInitialExpandedState(); + for (let i = 0; i < element.comments.length; i++) { + assert.isFalse(element.comments[i].collapsed); + } + }); + }); + + test('_computeHostClass', () => { + assert.equal(element._computeHostClass(true), 'unresolved'); + assert.equal(element._computeHostClass(false), ''); + }); + + test('addDraft sets unresolved state correctly', () => { + let unresolved = true; + element.comments = []; + element.addDraft(null, null, unresolved); + assert.equal(element.comments[0].unresolved, true); + + unresolved = false; // comment should get added as actually resolved. + element.comments = []; + element.addDraft(null, null, unresolved); + assert.equal(element.comments[0].unresolved, false); + + element.comments = []; + element.addDraft(); + assert.equal(element.comments[0].unresolved, true); + }); + + test('_newDraft', () => { + element.commentSide = 'left'; + element.patchNum = 3; + const draft = element._newDraft(); + assert.equal(draft.__commentSide, 'left'); + assert.equal(draft.patchNum, 3); + }); + + test('new comment gets created', () => { + element.comments = []; + element.addOrEditDraft(1); + assert.equal(element.comments.length, 1); + // Mock a submitted comment. + element.comments[0].id = element.comments[0].__draftID; + element.comments[0].__draft = false; + element.addOrEditDraft(1); + assert.equal(element.comments.length, 2); + }); + + test('unresolved label', () => { + element.unresolved = false; + assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden')); + element.unresolved = true; + assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden')); + }); + + test('draft comments are at the end of orderedComments', () => { + element.comments = [{ + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 2, + line: 5, + message: 'Earlier draft', + updated: '2015-12-08 19:48:33.843000000', + __draft: true, + }, + { + author: { + name: 'Mr. Peanutbutter2', + email: 'tenn1sballchaser@aol.com', + }, + id: 1, + line: 5, + message: 'This comment was left last but is not a draft', + updated: '2015-12-10 19:48:33.843000000', + }, + { + author: { + name: 'Mr. Peanutbutter2', + email: 'tenn1sballchaser@aol.com', + }, + id: 3, + line: 5, + message: 'Later draft', + updated: '2015-12-09 19:48:33.843000000', + __draft: true, + }]; + assert.equal(element._orderedComments[0].id, '1'); + assert.equal(element._orderedComments[1].id, '2'); + assert.equal(element._orderedComments[2].id, '3'); + }); + + test('reflects lineNum and commentSide to attributes', () => { + element.lineNum = 7; + element.commentSide = 'left'; + + assert.equal(element.getAttribute('line-num'), '7'); + assert.equal(element.getAttribute('comment-side'), 'left'); + }); + + test('reflects range to JSON serialized attribute if set', () => { + element.range = { + start_line: 4, + end_line: 5, + start_character: 6, + end_character: 7, + }; + + assert.deepEqual( + JSON.parse(element.getAttribute('range')), + {start_line: 4, end_line: 5, start_character: 6, end_character: 7}); + }); + + test('removes range attribute if range is unset', () => { + element.range = { + start_line: 4, + end_line: 5, + start_character: 6, + end_character: 7, + }; + element.range = undefined; + + assert.notOk(element.hasAttribute('range')); + }); +}); </script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js index 9880e88..6f1eaa8 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js +++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,797 +14,823 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const STORAGE_DEBOUNCE_INTERVAL = 400; - const TOAST_DEBOUNCE_INTERVAL = 200; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js'; +import '../../plugins/gr-endpoint-param/gr-endpoint-param.js'; +import '../gr-button/gr-button.js'; +import '../gr-dialog/gr-dialog.js'; +import '../gr-date-formatter/gr-date-formatter.js'; +import '../gr-formatted-text/gr-formatted-text.js'; +import '../gr-icons/gr-icons.js'; +import '../gr-overlay/gr-overlay.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-storage/gr-storage.js'; +import '../gr-textarea/gr-textarea.js'; +import '../gr-tooltip-content/gr-tooltip-content.js'; +import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js'; +import '../../../scripts/rootElement.js'; +import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-comment_html.js'; - const SAVING_MESSAGE = 'Saving'; - const DRAFT_SINGULAR = 'draft...'; - const DRAFT_PLURAL = 'drafts...'; - const SAVED_MESSAGE = 'All changes saved'; +const STORAGE_DEBOUNCE_INTERVAL = 400; +const TOAST_DEBOUNCE_INTERVAL = 200; - const REPORT_CREATE_DRAFT = 'CreateDraftComment'; - const REPORT_UPDATE_DRAFT = 'UpdateDraftComment'; - const REPORT_DISCARD_DRAFT = 'DiscardDraftComment'; +const SAVING_MESSAGE = 'Saving'; +const DRAFT_SINGULAR = 'draft...'; +const DRAFT_PLURAL = 'drafts...'; +const SAVED_MESSAGE = 'All changes saved'; - const FILE = 'FILE'; +const REPORT_CREATE_DRAFT = 'CreateDraftComment'; +const REPORT_UPDATE_DRAFT = 'UpdateDraftComment'; +const REPORT_DISCARD_DRAFT = 'DiscardDraftComment'; + +const FILE = 'FILE'; + +/** + * All candidates tips to show, will pick randomly. + */ +const RESPECTFUL_REVIEW_TIPS= [ + 'DO: Assume competence.', + 'DO: Provide rationale or context.', + 'DO: Consider how comments may be interpreted.', + 'DON’T: Criticize the person.', + 'DON’T: Use harsh language.', +]; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrComment extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-comment'; } + /** + * Fired when the create fix comment action is triggered. + * + * @event create-fix-comment + */ /** - * All candidates tips to show, will pick randomly. + * Fired when the show fix preview action is triggered. + * + * @event open-fix-preview */ - const RESPECTFUL_REVIEW_TIPS= [ - 'DO: Assume competence.', - 'DO: Provide rationale or context.', - 'DO: Consider how comments may be interpreted.', - 'DON’T: Criticize the person.', - 'DON’T: Use harsh language.', - ]; /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when this comment is discarded. + * + * @event comment-discard */ - class GrComment extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-comment'; } - /** - * Fired when the create fix comment action is triggered. - * - * @event create-fix-comment - */ - /** - * Fired when the show fix preview action is triggered. - * - * @event open-fix-preview - */ + /** + * Fired when this comment is saved. + * + * @event comment-save + */ - /** - * Fired when this comment is discarded. - * - * @event comment-discard - */ + /** + * Fired when this comment is updated. + * + * @event comment-update + */ - /** - * Fired when this comment is saved. - * - * @event comment-save - */ + /** + * Fired when the comment's timestamp is tapped. + * + * @event comment-anchor-tap + */ - /** - * Fired when this comment is updated. - * - * @event comment-update - */ + static get properties() { + return { + changeNum: String, + /** @type {!Gerrit.Comment} */ + comment: { + type: Object, + notify: true, + observer: '_commentChanged', + }, + comments: { + type: Array, + }, + isRobotComment: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + draft: { + type: Boolean, + value: false, + observer: '_draftChanged', + }, + editing: { + type: Boolean, + value: false, + observer: '_editingChanged', + }, + discarding: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + hasChildren: Boolean, + patchNum: String, + showActions: Boolean, + _showHumanActions: Boolean, + _showRobotActions: Boolean, + collapsed: { + type: Boolean, + value: true, + observer: '_toggleCollapseClass', + }, + /** @type {?} */ + projectConfig: Object, + robotButtonDisabled: Boolean, + _hasHumanReply: Boolean, + _isAdmin: { + type: Boolean, + value: false, + }, - /** - * Fired when the comment's timestamp is tapped. - * - * @event comment-anchor-tap - */ + _xhrPromise: Object, // Used for testing. + _messageText: { + type: String, + value: '', + observer: '_messageTextChanged', + }, + commentSide: String, + side: String, - static get properties() { - return { - changeNum: String, - /** @type {!Gerrit.Comment} */ - comment: { - type: Object, - notify: true, - observer: '_commentChanged', - }, - comments: { - type: Array, - }, - isRobotComment: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - draft: { - type: Boolean, - value: false, - observer: '_draftChanged', - }, - editing: { - type: Boolean, - value: false, - observer: '_editingChanged', - }, - discarding: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - hasChildren: Boolean, - patchNum: String, - showActions: Boolean, - _showHumanActions: Boolean, - _showRobotActions: Boolean, - collapsed: { - type: Boolean, - value: true, - observer: '_toggleCollapseClass', - }, - /** @type {?} */ - projectConfig: Object, - robotButtonDisabled: Boolean, - _hasHumanReply: Boolean, - _isAdmin: { - type: Boolean, - value: false, - }, + resolved: Boolean, - _xhrPromise: Object, // Used for testing. - _messageText: { - type: String, - value: '', - observer: '_messageTextChanged', - }, - commentSide: String, - side: String, + _numPendingDraftRequests: { + type: Object, + value: + {number: 0}, // Intentional to share the object across instances. + }, - resolved: Boolean, + _enableOverlay: { + type: Boolean, + value: false, + }, - _numPendingDraftRequests: { - type: Object, - value: - {number: 0}, // Intentional to share the object across instances. - }, + /** + * Property for storing references to overlay elements. When the overlays + * are moved to Gerrit.getRootElement() to be shown they are no-longer + * children, so they can't be queried along the tree, so they are stored + * here. + */ + _overlays: { + type: Object, + value: () => { return {}; }, + }, - _enableOverlay: { - type: Boolean, - value: false, - }, + _showRespectfulTip: { + type: Boolean, + value: false, + }, + _respectfulReviewTip: String, + _respectfulTipDismissed: { + type: Boolean, + value: false, + }, + }; + } - /** - * Property for storing references to overlay elements. When the overlays - * are moved to Gerrit.getRootElement() to be shown they are no-longer - * children, so they can't be queried along the tree, so they are stored - * here. - */ - _overlays: { - type: Object, - value: () => { return {}; }, - }, + static get observers() { + return [ + '_commentMessageChanged(comment.message)', + '_loadLocalDraft(changeNum, patchNum, comment)', + '_isRobotComment(comment)', + '_calculateActionstoShow(showActions, isRobotComment)', + '_computeHasHumanReply(comment, comments.*)', + '_onEditingChange(editing)', + ]; + } - _showRespectfulTip: { - type: Boolean, - value: false, - }, - _respectfulReviewTip: String, - _respectfulTipDismissed: { - type: Boolean, - value: false, - }, - }; + get keyBindings() { + return { + 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey', + 'esc': '_handleEsc', + }; + } + + /** @override */ + attached() { + super.attached(); + if (this.editing) { + this.collapsed = false; + } else if (this.comment) { + this.collapsed = this.comment.collapsed; } + this._getIsAdmin().then(isAdmin => { + this._isAdmin = isAdmin; + }); + } - static get observers() { - return [ - '_commentMessageChanged(comment.message)', - '_loadLocalDraft(changeNum, patchNum, comment)', - '_isRobotComment(comment)', - '_calculateActionstoShow(showActions, isRobotComment)', - '_computeHasHumanReply(comment, comments.*)', - '_onEditingChange(editing)', - ]; + /** @override */ + detached() { + super.detached(); + this.cancelDebouncer('fire-update'); + if (this.textarea) { + this.textarea.closeDropdown(); } + } - get keyBindings() { - return { - 'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey', - 'esc': '_handleEsc', - }; - } - - /** @override */ - attached() { - super.attached(); - if (this.editing) { - this.collapsed = false; - } else if (this.comment) { - this.collapsed = this.comment.collapsed; - } - this._getIsAdmin().then(isAdmin => { - this._isAdmin = isAdmin; - }); - } - - /** @override */ - detached() { - super.detached(); - this.cancelDebouncer('fire-update'); - if (this.textarea) { - this.textarea.closeDropdown(); - } - } - - _onEditingChange(editing) { - if (!editing) return; - // visibility based on cache this will make sure we only and always show - // a tip once every Math.max(a day, period between creating comments) - const cachedVisibilityOfRespectfulTip = - this.$.storage.getRespectfulTipVisibility(); - if (!cachedVisibilityOfRespectfulTip) { - // we still want to show the tip with a probability of 30% - if (this.getRandomNum(0, 3) >= 1) return; - this._showRespectfulTip = true; - const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length); - this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx]; - this.$.reporting.reportInteraction( - 'respectful-tip-appeared', - {tip: this._respectfulReviewTip} - ); - // update cache - this.$.storage.setRespectfulTipVisibility(); - } - } - - /** Set as a separate method so easy to stub. */ - getRandomNum(min, max) { - return Math.floor(Math.random() * (max - min) + min); - } - - _computeVisibilityOfTip(showTip, tipDismissed) { - return showTip && !tipDismissed; - } - - _dismissRespectfulTip() { - this._respectfulTipDismissed = true; + _onEditingChange(editing) { + if (!editing) return; + // visibility based on cache this will make sure we only and always show + // a tip once every Math.max(a day, period between creating comments) + const cachedVisibilityOfRespectfulTip = + this.$.storage.getRespectfulTipVisibility(); + if (!cachedVisibilityOfRespectfulTip) { + // we still want to show the tip with a probability of 30% + if (this.getRandomNum(0, 3) >= 1) return; + this._showRespectfulTip = true; + const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length); + this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx]; this.$.reporting.reportInteraction( - 'respectful-tip-dismissed', + 'respectful-tip-appeared', {tip: this._respectfulReviewTip} ); - // add a 3 day delay to the tip cache - this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3); + // update cache + this.$.storage.setRespectfulTipVisibility(); + } + } + + /** Set as a separate method so easy to stub. */ + getRandomNum(min, max) { + return Math.floor(Math.random() * (max - min) + min); + } + + _computeVisibilityOfTip(showTip, tipDismissed) { + return showTip && !tipDismissed; + } + + _dismissRespectfulTip() { + this._respectfulTipDismissed = true; + this.$.reporting.reportInteraction( + 'respectful-tip-dismissed', + {tip: this._respectfulReviewTip} + ); + // add a 3 day delay to the tip cache + this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3); + } + + _onRespectfulReadMoreClick() { + this.$.reporting.reportInteraction('respectful-read-more-clicked'); + } + + get textarea() { + return this.shadowRoot.querySelector('#editTextarea'); + } + + get confirmDeleteOverlay() { + if (!this._overlays.confirmDelete) { + this._enableOverlay = true; + flush(); + this._overlays.confirmDelete = this.shadowRoot + .querySelector('#confirmDeleteOverlay'); + } + return this._overlays.confirmDelete; + } + + get confirmDiscardOverlay() { + if (!this._overlays.confirmDiscard) { + this._enableOverlay = true; + flush(); + this._overlays.confirmDiscard = this.shadowRoot + .querySelector('#confirmDiscardOverlay'); + } + return this._overlays.confirmDiscard; + } + + _computeShowHideIcon(collapsed) { + return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less'; + } + + _calculateActionstoShow(showActions, isRobotComment) { + // Polymer 2: check for undefined + if ([showActions, isRobotComment].some(arg => arg === undefined)) { + return; } - _onRespectfulReadMoreClick() { - this.$.reporting.reportInteraction('respectful-read-more-clicked'); + this._showHumanActions = showActions && !isRobotComment; + this._showRobotActions = showActions && isRobotComment; + } + + _isRobotComment(comment) { + this.isRobotComment = !!comment.robot_id; + } + + isOnParent() { + return this.side === 'PARENT'; + } + + _getIsAdmin() { + return this.$.restAPI.getIsAdmin(); + } + + /** + * @param {*=} opt_comment + */ + save(opt_comment) { + let comment = opt_comment; + if (!comment) { + comment = this.comment; } - get textarea() { - return this.shadowRoot.querySelector('#editTextarea'); + this.set('comment.message', this._messageText); + this.editing = false; + this.disabled = true; + + if (!this._messageText) { + return this._discardDraft(); } - get confirmDeleteOverlay() { - if (!this._overlays.confirmDelete) { - this._enableOverlay = true; - Polymer.dom.flush(); - this._overlays.confirmDelete = this.shadowRoot - .querySelector('#confirmDeleteOverlay'); - } - return this._overlays.confirmDelete; - } + this._xhrPromise = this._saveDraft(comment).then(response => { + this.disabled = false; + if (!response.ok) { return response; } - get confirmDiscardOverlay() { - if (!this._overlays.confirmDiscard) { - this._enableOverlay = true; - Polymer.dom.flush(); - this._overlays.confirmDiscard = this.shadowRoot - .querySelector('#confirmDiscardOverlay'); - } - return this._overlays.confirmDiscard; - } - - _computeShowHideIcon(collapsed) { - return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less'; - } - - _calculateActionstoShow(showActions, isRobotComment) { - // Polymer 2: check for undefined - if ([showActions, isRobotComment].some(arg => arg === undefined)) { - return; - } - - this._showHumanActions = showActions && !isRobotComment; - this._showRobotActions = showActions && isRobotComment; - } - - _isRobotComment(comment) { - this.isRobotComment = !!comment.robot_id; - } - - isOnParent() { - return this.side === 'PARENT'; - } - - _getIsAdmin() { - return this.$.restAPI.getIsAdmin(); - } - - /** - * @param {*=} opt_comment - */ - save(opt_comment) { - let comment = opt_comment; - if (!comment) { - comment = this.comment; - } - - this.set('comment.message', this._messageText); - this.editing = false; - this.disabled = true; - - if (!this._messageText) { - return this._discardDraft(); - } - - this._xhrPromise = this._saveDraft(comment).then(response => { - this.disabled = false; - if (!response.ok) { return response; } - - this._eraseDraftComment(); - return this.$.restAPI.getResponseObject(response).then(obj => { - const resComment = obj; - resComment.__draft = true; - // Maintain the ephemeral draft ID for identification by other - // elements. - if (this.comment.__draftID) { - resComment.__draftID = this.comment.__draftID; - } - resComment.__commentSide = this.commentSide; - this.comment = resComment; - this._fireSave(); - return obj; + this._eraseDraftComment(); + return this.$.restAPI.getResponseObject(response).then(obj => { + const resComment = obj; + resComment.__draft = true; + // Maintain the ephemeral draft ID for identification by other + // elements. + if (this.comment.__draftID) { + resComment.__draftID = this.comment.__draftID; + } + resComment.__commentSide = this.commentSide; + this.comment = resComment; + this._fireSave(); + return obj; + }); + }) + .catch(err => { + this.disabled = false; + throw err; }); - }) - .catch(err => { - this.disabled = false; - throw err; - }); - return this._xhrPromise; + return this._xhrPromise; + } + + _eraseDraftComment() { + // Prevents a race condition in which removing the draft comment occurs + // prior to it being saved. + this.cancelDebouncer('store'); + + this.$.storage.eraseDraftComment({ + changeNum: this.changeNum, + patchNum: this._getPatchNum(), + path: this.comment.path, + line: this.comment.line, + range: this.comment.range, + }); + } + + _commentChanged(comment) { + this.editing = !!comment.__editing; + this.resolved = !comment.unresolved; + if (this.editing) { // It's a new draft/reply, notify. + this._fireUpdate(); + } + } + + _computeHasHumanReply() { + if (!this.comment || !this.comments) return; + // hide please fix button for robot comment that has human reply + this._hasHumanReply = this.comments + .some(c => c.in_reply_to && c.in_reply_to === this.comment.id && + !c.robot_id); + } + + /** + * @param {!Object=} opt_mixin + * + * @return {!Object} + */ + _getEventPayload(opt_mixin) { + return Object.assign({}, opt_mixin, { + comment: this.comment, + patchNum: this.patchNum, + }); + } + + _fireSave() { + this.fire('comment-save', this._getEventPayload()); + } + + _fireUpdate() { + this.debounce('fire-update', () => { + this.fire('comment-update', this._getEventPayload()); + }); + } + + _draftChanged(draft) { + this.$.container.classList.toggle('draft', draft); + } + + _editingChanged(editing, previousValue) { + // Polymer 2: observer fires when at least one property is defined. + // Do nothing to prevent comment.__editing being overwritten + // if previousValue is undefined + if (previousValue === undefined) return; + + this.$.container.classList.toggle('editing', editing); + if (this.comment && this.comment.id) { + this.shadowRoot.querySelector('.cancel').hidden = !editing; + } + if (this.comment) { + this.comment.__editing = this.editing; + } + if (editing != !!previousValue) { + // To prevent event firing on comment creation. + this._fireUpdate(); + } + if (editing) { + this.async(() => { + flush(); + this.textarea && this.textarea.putCursorAtEnd(); + }, 1); + } + } + + _computeDeleteButtonClass(isAdmin, draft) { + return isAdmin && !draft ? 'showDeleteButtons' : ''; + } + + _computeSaveDisabled(draft, comment, resolved) { + // If resolved state has changed and a msg exists, save should be enabled. + if (!comment || comment.unresolved === resolved && draft) { + return false; + } + return !draft || draft.trim() === ''; + } + + _handleSaveKey(e) { + if (!this._computeSaveDisabled(this._messageText, this.comment, + this.resolved)) { + e.preventDefault(); + this._handleSave(e); + } + } + + _handleEsc(e) { + if (!this._messageText.length) { + e.preventDefault(); + this._handleCancel(e); + } + } + + _handleToggleCollapsed() { + this.collapsed = !this.collapsed; + } + + _toggleCollapseClass(collapsed) { + if (collapsed) { + this.$.container.classList.add('collapsed'); + } else { + this.$.container.classList.remove('collapsed'); + } + } + + _commentMessageChanged(message) { + this._messageText = message || ''; + } + + _messageTextChanged(newValue, oldValue) { + if (!this.comment || (this.comment && this.comment.id)) { + return; } - _eraseDraftComment() { - // Prevents a race condition in which removing the draft comment occurs - // prior to it being saved. - this.cancelDebouncer('store'); - - this.$.storage.eraseDraftComment({ + this.debounce('store', () => { + const message = this._messageText; + const commentLocation = { changeNum: this.changeNum, patchNum: this._getPatchNum(), path: this.comment.path, line: this.comment.line, range: this.comment.range, - }); - } + }; - _commentChanged(comment) { - this.editing = !!comment.__editing; - this.resolved = !comment.unresolved; - if (this.editing) { // It's a new draft/reply, notify. - this._fireUpdate(); - } - } - - _computeHasHumanReply() { - if (!this.comment || !this.comments) return; - // hide please fix button for robot comment that has human reply - this._hasHumanReply = this.comments - .some(c => c.in_reply_to && c.in_reply_to === this.comment.id && - !c.robot_id); - } - - /** - * @param {!Object=} opt_mixin - * - * @return {!Object} - */ - _getEventPayload(opt_mixin) { - return Object.assign({}, opt_mixin, { - comment: this.comment, - patchNum: this.patchNum, - }); - } - - _fireSave() { - this.fire('comment-save', this._getEventPayload()); - } - - _fireUpdate() { - this.debounce('fire-update', () => { - this.fire('comment-update', this._getEventPayload()); - }); - } - - _draftChanged(draft) { - this.$.container.classList.toggle('draft', draft); - } - - _editingChanged(editing, previousValue) { - // Polymer 2: observer fires when at least one property is defined. - // Do nothing to prevent comment.__editing being overwritten - // if previousValue is undefined - if (previousValue === undefined) return; - - this.$.container.classList.toggle('editing', editing); - if (this.comment && this.comment.id) { - this.shadowRoot.querySelector('.cancel').hidden = !editing; - } - if (this.comment) { - this.comment.__editing = this.editing; - } - if (editing != !!previousValue) { - // To prevent event firing on comment creation. - this._fireUpdate(); - } - if (editing) { - this.async(() => { - Polymer.dom.flush(); - this.textarea && this.textarea.putCursorAtEnd(); - }, 1); - } - } - - _computeDeleteButtonClass(isAdmin, draft) { - return isAdmin && !draft ? 'showDeleteButtons' : ''; - } - - _computeSaveDisabled(draft, comment, resolved) { - // If resolved state has changed and a msg exists, save should be enabled. - if (!comment || comment.unresolved === resolved && draft) { - return false; - } - return !draft || draft.trim() === ''; - } - - _handleSaveKey(e) { - if (!this._computeSaveDisabled(this._messageText, this.comment, - this.resolved)) { - e.preventDefault(); - this._handleSave(e); - } - } - - _handleEsc(e) { - if (!this._messageText.length) { - e.preventDefault(); - this._handleCancel(e); - } - } - - _handleToggleCollapsed() { - this.collapsed = !this.collapsed; - } - - _toggleCollapseClass(collapsed) { - if (collapsed) { - this.$.container.classList.add('collapsed'); + if ((!this._messageText || !this._messageText.length) && oldValue) { + // If the draft has been modified to be empty, then erase the storage + // entry. + this.$.storage.eraseDraftComment(commentLocation); } else { - this.$.container.classList.remove('collapsed'); + this.$.storage.setDraftComment(commentLocation, message); } + }, STORAGE_DEBOUNCE_INTERVAL); + } + + _handleAnchorClick(e) { + e.preventDefault(); + if (!this.comment.line) { + return; + } + this.dispatchEvent(new CustomEvent('comment-anchor-tap', { + bubbles: true, + composed: true, + detail: { + number: this.comment.line || FILE, + side: this.side, + }, + })); + } + + _handleEdit(e) { + e.preventDefault(); + this._messageText = this.comment.message; + this.editing = true; + this.$.reporting.recordDraftInteraction(); + } + + _handleSave(e) { + e.preventDefault(); + + // Ignore saves started while already saving. + if (this.disabled) { + return; + } + const timingLabel = this.comment.id ? + REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT; + const timer = this.$.reporting.getTimer(timingLabel); + this.set('comment.__editing', false); + return this.save().then(() => { timer.end(); }); + } + + _handleCancel(e) { + e.preventDefault(); + + if (!this.comment.message || + this.comment.message.trim().length === 0 || + !this.comment.id) { + this._fireDiscard(); + return; + } + this._messageText = this.comment.message; + this.editing = false; + } + + _fireDiscard() { + this.cancelDebouncer('fire-update'); + this.fire('comment-discard', this._getEventPayload()); + } + + _handleFix() { + this.dispatchEvent(new CustomEvent('create-fix-comment', { + bubbles: true, + composed: true, + detail: this._getEventPayload(), + })); + } + + _handleShowFix() { + this.dispatchEvent(new CustomEvent('open-fix-preview', { + bubbles: true, + composed: true, + detail: this._getEventPayload(), + })); + } + + _hasNoFix(comment) { + return !comment || !comment.fix_suggestions; + } + + _handleDiscard(e) { + e.preventDefault(); + this.$.reporting.recordDraftInteraction(); + + if (!this._messageText) { + this._discardDraft(); + return; } - _commentMessageChanged(message) { - this._messageText = message || ''; + this._openOverlay(this.confirmDiscardOverlay).then(() => { + this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog') + .resetFocus(); + }); + } + + _handleConfirmDiscard(e) { + e.preventDefault(); + const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT); + this._closeConfirmDiscardOverlay(); + return this._discardDraft().then(() => { timer.end(); }); + } + + _discardDraft() { + if (!this.comment.__draft) { + throw Error('Cannot discard a non-draft comment.'); + } + this.discarding = true; + this.editing = false; + this.disabled = true; + this._eraseDraftComment(); + + if (!this.comment.id) { + this.disabled = false; + this._fireDiscard(); + return; } - _messageTextChanged(newValue, oldValue) { - if (!this.comment || (this.comment && this.comment.id)) { - return; + this._xhrPromise = this._deleteDraft(this.comment).then(response => { + this.disabled = false; + if (!response.ok) { + this.discarding = false; + return response; } - this.debounce('store', () => { - const message = this._messageText; - const commentLocation = { - changeNum: this.changeNum, - patchNum: this._getPatchNum(), - path: this.comment.path, - line: this.comment.line, - range: this.comment.range, - }; + this._fireDiscard(); + }) + .catch(err => { + this.disabled = false; + throw err; + }); - if ((!this._messageText || !this._messageText.length) && oldValue) { - // If the draft has been modified to be empty, then erase the storage - // entry. - this.$.storage.eraseDraftComment(commentLocation); - } else { - this.$.storage.setDraftComment(commentLocation, message); - } - }, STORAGE_DEBOUNCE_INTERVAL); + return this._xhrPromise; + } + + _closeConfirmDiscardOverlay() { + this._closeOverlay(this.confirmDiscardOverlay); + } + + _getSavingMessage(numPending) { + if (numPending === 0) { + return SAVED_MESSAGE; } + return [ + SAVING_MESSAGE, + numPending, + numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL, + ].join(' '); + } - _handleAnchorClick(e) { - e.preventDefault(); - if (!this.comment.line) { - return; + _showStartRequest() { + const numPending = ++this._numPendingDraftRequests.number; + this._updateRequestToast(numPending); + } + + _showEndRequest() { + const numPending = --this._numPendingDraftRequests.number; + this._updateRequestToast(numPending); + } + + _handleFailedDraftRequest() { + this._numPendingDraftRequests.number--; + + // Cancel the debouncer so that error toasts from the error-manager will + // not be overridden. + this.cancelDebouncer('draft-toast'); + } + + _updateRequestToast(numPending) { + const message = this._getSavingMessage(numPending); + this.debounce('draft-toast', () => { + // Note: the event is fired on the body rather than this element because + // this element may not be attached by the time this executes, in which + // case the event would not bubble. + document.body.dispatchEvent(new CustomEvent( + 'show-alert', {detail: {message}, bubbles: true, composed: true})); + }, TOAST_DEBOUNCE_INTERVAL); + } + + _saveDraft(draft) { + this._showStartRequest(); + return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft) + .then(result => { + if (result.ok) { + this._showEndRequest(); + } else { + this._handleFailedDraftRequest(); + } + return result; + }); + } + + _deleteDraft(draft) { + this._showStartRequest(); + return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum, + draft).then(result => { + if (result.ok) { + this._showEndRequest(); + } else { + this._handleFailedDraftRequest(); } - this.dispatchEvent(new CustomEvent('comment-anchor-tap', { - bubbles: true, - composed: true, - detail: { - number: this.comment.line || FILE, - side: this.side, - }, - })); + return result; + }); + } + + _getPatchNum() { + return this.isOnParent() ? 'PARENT' : this.patchNum; + } + + _loadLocalDraft(changeNum, patchNum, comment) { + // Polymer 2: check for undefined + if ([changeNum, patchNum, comment].some(arg => arg === undefined)) { + return; } - _handleEdit(e) { - e.preventDefault(); - this._messageText = this.comment.message; - this.editing = true; - this.$.reporting.recordDraftInteraction(); + // Only apply local drafts to comments that haven't been saved + // remotely, and haven't been given a default message already. + // + // Don't get local draft if there is another comment that is currently + // in an editing state. + if (!comment || comment.id || comment.message || comment.__otherEditing) { + delete comment.__otherEditing; + return; } - _handleSave(e) { - e.preventDefault(); + const draft = this.$.storage.getDraftComment({ + changeNum, + patchNum: this._getPatchNum(), + path: comment.path, + line: comment.line, + range: comment.range, + }); - // Ignore saves started while already saving. - if (this.disabled) { - return; - } - const timingLabel = this.comment.id ? - REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT; - const timer = this.$.reporting.getTimer(timingLabel); - this.set('comment.__editing', false); - return this.save().then(() => { timer.end(); }); - } - - _handleCancel(e) { - e.preventDefault(); - - if (!this.comment.message || - this.comment.message.trim().length === 0 || - !this.comment.id) { - this._fireDiscard(); - return; - } - this._messageText = this.comment.message; - this.editing = false; - } - - _fireDiscard() { - this.cancelDebouncer('fire-update'); - this.fire('comment-discard', this._getEventPayload()); - } - - _handleFix() { - this.dispatchEvent(new CustomEvent('create-fix-comment', { - bubbles: true, - composed: true, - detail: this._getEventPayload(), - })); - } - - _handleShowFix() { - this.dispatchEvent(new CustomEvent('open-fix-preview', { - bubbles: true, - composed: true, - detail: this._getEventPayload(), - })); - } - - _hasNoFix(comment) { - return !comment || !comment.fix_suggestions; - } - - _handleDiscard(e) { - e.preventDefault(); - this.$.reporting.recordDraftInteraction(); - - if (!this._messageText) { - this._discardDraft(); - return; - } - - this._openOverlay(this.confirmDiscardOverlay).then(() => { - this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog') - .resetFocus(); - }); - } - - _handleConfirmDiscard(e) { - e.preventDefault(); - const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT); - this._closeConfirmDiscardOverlay(); - return this._discardDraft().then(() => { timer.end(); }); - } - - _discardDraft() { - if (!this.comment.__draft) { - throw Error('Cannot discard a non-draft comment.'); - } - this.discarding = true; - this.editing = false; - this.disabled = true; - this._eraseDraftComment(); - - if (!this.comment.id) { - this.disabled = false; - this._fireDiscard(); - return; - } - - this._xhrPromise = this._deleteDraft(this.comment).then(response => { - this.disabled = false; - if (!response.ok) { - this.discarding = false; - return response; - } - - this._fireDiscard(); - }) - .catch(err => { - this.disabled = false; - throw err; - }); - - return this._xhrPromise; - } - - _closeConfirmDiscardOverlay() { - this._closeOverlay(this.confirmDiscardOverlay); - } - - _getSavingMessage(numPending) { - if (numPending === 0) { - return SAVED_MESSAGE; - } - return [ - SAVING_MESSAGE, - numPending, - numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL, - ].join(' '); - } - - _showStartRequest() { - const numPending = ++this._numPendingDraftRequests.number; - this._updateRequestToast(numPending); - } - - _showEndRequest() { - const numPending = --this._numPendingDraftRequests.number; - this._updateRequestToast(numPending); - } - - _handleFailedDraftRequest() { - this._numPendingDraftRequests.number--; - - // Cancel the debouncer so that error toasts from the error-manager will - // not be overridden. - this.cancelDebouncer('draft-toast'); - } - - _updateRequestToast(numPending) { - const message = this._getSavingMessage(numPending); - this.debounce('draft-toast', () => { - // Note: the event is fired on the body rather than this element because - // this element may not be attached by the time this executes, in which - // case the event would not bubble. - document.body.dispatchEvent(new CustomEvent( - 'show-alert', {detail: {message}, bubbles: true, composed: true})); - }, TOAST_DEBOUNCE_INTERVAL); - } - - _saveDraft(draft) { - this._showStartRequest(); - return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft) - .then(result => { - if (result.ok) { - this._showEndRequest(); - } else { - this._handleFailedDraftRequest(); - } - return result; - }); - } - - _deleteDraft(draft) { - this._showStartRequest(); - return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum, - draft).then(result => { - if (result.ok) { - this._showEndRequest(); - } else { - this._handleFailedDraftRequest(); - } - return result; - }); - } - - _getPatchNum() { - return this.isOnParent() ? 'PARENT' : this.patchNum; - } - - _loadLocalDraft(changeNum, patchNum, comment) { - // Polymer 2: check for undefined - if ([changeNum, patchNum, comment].some(arg => arg === undefined)) { - return; - } - - // Only apply local drafts to comments that haven't been saved - // remotely, and haven't been given a default message already. - // - // Don't get local draft if there is another comment that is currently - // in an editing state. - if (!comment || comment.id || comment.message || comment.__otherEditing) { - delete comment.__otherEditing; - return; - } - - const draft = this.$.storage.getDraftComment({ - changeNum, - patchNum: this._getPatchNum(), - path: comment.path, - line: comment.line, - range: comment.range, - }); - - if (draft) { - this.set('comment.message', draft.message); - } - } - - _handleToggleResolved() { - this.$.reporting.recordDraftInteraction(); - this.resolved = !this.resolved; - // Modify payload instead of this.comment, as this.comment is passed from - // the parent by ref. - const payload = this._getEventPayload(); - payload.comment.unresolved = !this.$.resolvedCheckbox.checked; - this.fire('comment-update', payload); - if (!this.editing) { - // Save the resolved state immediately. - this.save(payload.comment); - } - } - - _handleCommentDelete() { - this._openOverlay(this.confirmDeleteOverlay); - } - - _handleCancelDeleteComment() { - this._closeOverlay(this.confirmDeleteOverlay); - } - - _openOverlay(overlay) { - Polymer.dom(Gerrit.getRootElement()).appendChild(overlay); - return overlay.open(); - } - - _computeAuthorName(comment) { - if (!comment) return ''; - if (comment.robot_id) { - return comment.robot_id; - } - return comment.author && comment.author.name; - } - - _computeHideRunDetails(comment, collapsed) { - if (!comment) return true; - return !(comment.robot_id && comment.url && !collapsed); - } - - _closeOverlay(overlay) { - Polymer.dom(Gerrit.getRootElement()).removeChild(overlay); - overlay.close(); - } - - _handleConfirmDeleteComment() { - const dialog = - this.confirmDeleteOverlay.querySelector('#confirmDeleteComment'); - this.$.restAPI.deleteComment( - this.changeNum, this.patchNum, this.comment.id, dialog.message) - .then(newComment => { - this._handleCancelDeleteComment(); - this.comment = newComment; - }); + if (draft) { + this.set('comment.message', draft.message); } } - customElements.define(GrComment.is, GrComment); -})(); + _handleToggleResolved() { + this.$.reporting.recordDraftInteraction(); + this.resolved = !this.resolved; + // Modify payload instead of this.comment, as this.comment is passed from + // the parent by ref. + const payload = this._getEventPayload(); + payload.comment.unresolved = !this.$.resolvedCheckbox.checked; + this.fire('comment-update', payload); + if (!this.editing) { + // Save the resolved state immediately. + this.save(payload.comment); + } + } + + _handleCommentDelete() { + this._openOverlay(this.confirmDeleteOverlay); + } + + _handleCancelDeleteComment() { + this._closeOverlay(this.confirmDeleteOverlay); + } + + _openOverlay(overlay) { + dom(Gerrit.getRootElement()).appendChild(overlay); + return overlay.open(); + } + + _computeAuthorName(comment) { + if (!comment) return ''; + if (comment.robot_id) { + return comment.robot_id; + } + return comment.author && comment.author.name; + } + + _computeHideRunDetails(comment, collapsed) { + if (!comment) return true; + return !(comment.robot_id && comment.url && !collapsed); + } + + _closeOverlay(overlay) { + dom(Gerrit.getRootElement()).removeChild(overlay); + overlay.close(); + } + + _handleConfirmDeleteComment() { + const dialog = + this.confirmDeleteOverlay.querySelector('#confirmDeleteComment'); + this.$.restAPI.deleteComment( + this.changeNum, this.patchNum, this.comment.id, dialog.message) + .then(newComment => { + this._handleCancelDeleteComment(); + this.comment = newComment; + }); + } +} + +customElements.define(GrComment.is, GrComment);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js index 18ffc0e..4a0f388 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js +++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -1,43 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html"> -<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-storage/gr-storage.html"> -<link rel="import" href="../../shared/gr-textarea/gr-textarea.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html"> -<script src="../../../scripts/rootElement.js"></script> - -<dom-module id="gr-comment"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -257,144 +236,85 @@ <div class="headerLeft"> <span class="authorName">[[_computeAuthorName(comment)]]</span> <span class="draftLabel">DRAFT</span> - <gr-tooltip-content class="draftTooltip" - has-tooltip - title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key." - max-width="20em" - show-icon></gr-tooltip-content> + <gr-tooltip-content class="draftTooltip" has-tooltip="" title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key." max-width="20em" show-icon=""></gr-tooltip-content> </div> <div class="headerMiddle"> <span class="collapsedContent">[[comment.message]]</span> </div> - <div hidden$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message"> + <div hidden\$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message"> <div class="runIdInformation"> - <a class="robotRunLink" href$="[[comment.url]]"> + <a class="robotRunLink" href\$="[[comment.url]]"> <span class="robotRun link">Run Details</span> </a> </div> </div> - <gr-button - id="deleteBtn" - link - class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" - hidden$="[[isRobotComment]]" - on-click="_handleCommentDelete"> + <gr-button id="deleteBtn" link="" class\$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" hidden\$="[[isRobotComment]]" on-click="_handleCommentDelete"> <iron-icon id="icon" icon="gr-icons:delete"></iron-icon> </gr-button> <span class="date" on-click="_handleAnchorClick"> - <gr-date-formatter - has-tooltip - date-str="[[comment.updated]]"></gr-date-formatter> + <gr-date-formatter has-tooltip="" date-str="[[comment.updated]]"></gr-date-formatter> </span> <div class="show-hide"> <label class="show-hide"> - <input type="checkbox" class="show-hide" - checked$="[[collapsed]]" - on-change="_handleToggleCollapsed"> - <iron-icon - id="icon" - icon="[[_computeShowHideIcon(collapsed)]]"> + <input type="checkbox" class="show-hide" checked\$="[[collapsed]]" on-change="_handleToggleCollapsed"> + <iron-icon id="icon" icon="[[_computeShowHideIcon(collapsed)]]"> </iron-icon> </label> </div> </div> <div class="body"> <template is="dom-if" if="[[isRobotComment]]"> - <div class="robotId" hidden$="[[collapsed]]"> + <div class="robotId" hidden\$="[[collapsed]]"> [[comment.author.name]] </div> </template> <template is="dom-if" if="[[editing]]"> - <gr-textarea - id="editTextarea" - class="editMessage" - autocomplete="on" - code - disabled="{{disabled}}" - rows="4" - text="{{_messageText}}"></gr-textarea> + <gr-textarea id="editTextarea" class="editMessage" autocomplete="on" code="" disabled="{{disabled}}" rows="4" text="{{_messageText}}"></gr-textarea> <template is="dom-if" if="[[_computeVisibilityOfTip(_showRespectfulTip, _respectfulTipDismissed)]]"> <div class="respectfulReviewTip"> <div> - <gr-tooltip-content - has-tooltip - title="Tips for respectful code reviews."> + <gr-tooltip-content has-tooltip="" title="Tips for respectful code reviews."> <iron-icon class="pointer" icon="gr-icons:lightbulb-outline"></iron-icon> </gr-tooltip-content> [[_respectfulReviewTip]] </div> <div> - <a - tabIndex="-1" - on-click="_onRespectfulReadMoreClick" - href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html" - target="_blank"> + <a tabindex="-1" on-click="_onRespectfulReadMoreClick" href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html" target="_blank"> Read more </a> - <iron-icon - class="close pointer" - on-click="_dismissRespectfulTip" - icon="gr-icons:close"></iron-icon> + <iron-icon class="close pointer" on-click="_dismissRespectfulTip" icon="gr-icons:close"></iron-icon> </div> </div> </template> </template> <!--The message class is needed to ensure selectability from gr-diff-selection.--> - <gr-formatted-text class="message" - content="[[comment.message]]" - no-trailing-margin="[[!comment.__draft]]" - config="[[projectConfig.commentlinks]]"></gr-formatted-text> - <div class="actions humanActions" hidden$="[[!_showHumanActions]]"> + <gr-formatted-text class="message" content="[[comment.message]]" no-trailing-margin="[[!comment.__draft]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text> + <div class="actions humanActions" hidden\$="[[!_showHumanActions]]"> <div class="action resolve hideOnPublished"> <label> - <input type="checkbox" - id="resolvedCheckbox" - checked="[[resolved]]" - on-change="_handleToggleResolved"> + <input type="checkbox" id="resolvedCheckbox" checked="[[resolved]]" on-change="_handleToggleResolved"> Resolved </label> </div> <div class="rightActions"> - <gr-button - link - class="action cancel hideOnPublished" - on-click="_handleCancel">Cancel</gr-button> - <gr-button - link - class="action discard hideOnPublished" - on-click="_handleDiscard">Discard</gr-button> - <gr-button - link - class="action edit hideOnPublished" - on-click="_handleEdit">Edit</gr-button> - <gr-button - link - disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]" - class="action save hideOnPublished" - on-click="_handleSave">Save</gr-button> + <gr-button link="" class="action cancel hideOnPublished" on-click="_handleCancel">Cancel</gr-button> + <gr-button link="" class="action discard hideOnPublished" on-click="_handleDiscard">Discard</gr-button> + <gr-button link="" class="action edit hideOnPublished" on-click="_handleEdit">Edit</gr-button> + <gr-button link="" disabled\$="[[_computeSaveDisabled(_messageText, comment, resolved)]]" class="action save hideOnPublished" on-click="_handleSave">Save</gr-button> </div> </div> - <div class="robotActions" hidden$="[[!_showRobotActions]]"> + <div class="robotActions" hidden\$="[[!_showRobotActions]]"> <template is="dom-if" if="[[isRobotComment]]"> <gr-endpoint-decorator name="robot-comment-controls"> <gr-endpoint-param name="comment" value="[[comment]]"> </gr-endpoint-param> </gr-endpoint-decorator> - <gr-button - link - secondary - class="action show-fix" - hidden$="[[_hasNoFix(comment)]]" - on-click="_handleShowFix"> + <gr-button link="" secondary="" class="action show-fix" hidden\$="[[_hasNoFix(comment)]]" on-click="_handleShowFix"> Show Fix </gr-button> <template is="dom-if" if="[[!_hasHumanReply]]"> - <gr-button - link - class="action fix" - on-click="_handleFix" - disabled="[[robotButtonDisabled]]"> + <gr-button link="" class="action fix" on-click="_handleFix" disabled="[[robotButtonDisabled]]"> Please Fix </gr-button> </template> @@ -403,19 +323,12 @@ </div> </div> <template is="dom-if" if="[[_enableOverlay]]"> - <gr-overlay id="confirmDeleteOverlay" with-backdrop> - <gr-confirm-delete-comment-dialog id="confirmDeleteComment" - on-confirm="_handleConfirmDeleteComment" - on-cancel="_handleCancelDeleteComment"> + <gr-overlay id="confirmDeleteOverlay" with-backdrop=""> + <gr-confirm-delete-comment-dialog id="confirmDeleteComment" on-confirm="_handleConfirmDeleteComment" on-cancel="_handleCancelDeleteComment"> </gr-confirm-delete-comment-dialog> </gr-overlay> - <gr-overlay id="confirmDiscardOverlay" with-backdrop> - <gr-dialog - id="confirmDiscardDialog" - confirm-label="Discard" - confirm-on-enter - on-confirm="_handleConfirmDiscard" - on-cancel="_closeConfirmDiscardOverlay"> + <gr-overlay id="confirmDiscardOverlay" with-backdrop=""> + <gr-dialog id="confirmDiscardDialog" confirm-label="Discard" confirm-on-enter="" on-confirm="_handleConfirmDiscard" on-cancel="_closeConfirmDiscardOverlay"> <div class="header" slot="header"> Discard comment </div> @@ -428,6 +341,4 @@ <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-storage id="storage"></gr-storage> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-comment.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html index 5e9d37a..96d497e 100644 --- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html +++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -19,18 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-comment</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="/bower_components/page/page.js"></script> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script src="/node_modules/page/page.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-comment.html"> +<script type="module" src="./gr-comment.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-comment.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -44,1119 +50,1204 @@ </template> </test-fixture> -<script> - function isVisible(el) { - assert.ok(el); - return getComputedStyle(el).getPropertyValue('display') !== 'none'; - } +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-comment.js'; +function isVisible(el) { + assert.ok(el); + return getComputedStyle(el).getPropertyValue('display') !== 'none'; +} - suite('gr-comment tests', async () => { - await readyToTest(); +suite('gr-comment tests', () => { + suite('basic tests', () => { + let element; + let sandbox; + setup(() => { + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + }); + element = fixture('basic'); + element.comment = { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + id: 'baf0414d_60047215', + line: 5, + message: 'is this a crossover episode!?', + updated: '2015-12-08 19:48:33.843000000', + }; + sandbox = sinon.sandbox.create(); + }); - suite('basic tests', () => { - let element; - let sandbox; + teardown(() => { + sandbox.restore(); + }); + + test('collapsible comments', () => { + // When a comment (not draft) is loaded, it should be collapsed + assert.isTrue(element.collapsed); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are not visible'); + assert.isNotOk(element.textarea, 'textarea is not visible'); + + // The header middle content is only visible when comments are collapsed. + // It shows the message in a condensed way, and limits to a single line. + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is visible'); + + // When the header row is clicked, the comment should expand + MockInteractions.tap(element.$.header); + assert.isFalse(element.collapsed); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are visible'); + assert.isNotOk(element.textarea, 'textarea is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is not visible'); + }); + + test('clicking on date link fires event', () => { + element.side = 'PARENT'; + const stub = sinon.stub(); + element.addEventListener('comment-anchor-tap', stub); + const dateEl = element.shadowRoot + .querySelector('.date'); + assert.ok(dateEl); + MockInteractions.tap(dateEl); + + assert.isTrue(stub.called); + assert.deepEqual(stub.lastCall.args[0].detail, + {side: element.side, number: element.comment.line}); + }); + + test('message is not retrieved from storage when other edits', done => { + const storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); + const loadSpy = sandbox.spy(element, '_loadLocalDraft'); + + element.changeNum = 1; + element.patchNum = 1; + element.comment = { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + line: 5, + __otherEditing: true, + }; + flush(() => { + assert.isTrue(loadSpy.called); + assert.isFalse(storageStub.called); + done(); + }); + }); + + test('message is retrieved from storage when no other edits', done => { + const storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); + const loadSpy = sandbox.spy(element, '_loadLocalDraft'); + + element.changeNum = 1; + element.patchNum = 1; + element.comment = { + author: { + name: 'Mr. Peanutbutter', + email: 'tenn1sballchaser@aol.com', + }, + line: 5, + }; + flush(() => { + assert.isTrue(loadSpy.called); + assert.isTrue(storageStub.called); + done(); + }); + }); + + test('_getPatchNum', () => { + element.side = 'PARENT'; + element.patchNum = 1; + assert.equal(element._getPatchNum(), 'PARENT'); + element.side = 'REVISION'; + assert.equal(element._getPatchNum(), 1); + }); + + test('comment expand and collapse', () => { + element.collapsed = true; + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are not visible'); + assert.isNotOk(element.textarea, 'textarea is not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is visible'); + + element.collapsed = false; + assert.isFalse(element.collapsed); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are visible'); + assert.isNotOk(element.textarea, 'textarea is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is is not visible'); + }); + + suite('while editing', () => { setup(() => { - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(null); }, - }); - element = fixture('basic'); - element.comment = { - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - id: 'baf0414d_60047215', - line: 5, - message: 'is this a crossover episode!?', - updated: '2015-12-08 19:48:33.843000000', - }; - sandbox = sinon.sandbox.create(); + element.editing = true; + element._messageText = 'test'; + sandbox.stub(element, '_handleCancel'); + sandbox.stub(element, '_handleSave'); + flushAsynchronousOperations(); }); - teardown(() => { - sandbox.restore(); - }); - - test('collapsible comments', () => { - // When a comment (not draft) is loaded, it should be collapsed - assert.isTrue(element.collapsed); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are not visible'); - assert.isNotOk(element.textarea, 'textarea is not visible'); - - // The header middle content is only visible when comments are collapsed. - // It shows the message in a condensed way, and limits to a single line. - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is visible'); - - // When the header row is clicked, the comment should expand - MockInteractions.tap(element.$.header); - assert.isFalse(element.collapsed); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are visible'); - assert.isNotOk(element.textarea, 'textarea is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is not visible'); - }); - - test('clicking on date link fires event', () => { - element.side = 'PARENT'; - const stub = sinon.stub(); - element.addEventListener('comment-anchor-tap', stub); - const dateEl = element.shadowRoot - .querySelector('.date'); - assert.ok(dateEl); - MockInteractions.tap(dateEl); - - assert.isTrue(stub.called); - assert.deepEqual(stub.lastCall.args[0].detail, - {side: element.side, number: element.comment.line}); - }); - - test('message is not retrieved from storage when other edits', done => { - const storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); - const loadSpy = sandbox.spy(element, '_loadLocalDraft'); - - element.changeNum = 1; - element.patchNum = 1; - element.comment = { - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - line: 5, - __otherEditing: true, - }; - flush(() => { - assert.isTrue(loadSpy.called); - assert.isFalse(storageStub.called); - done(); - }); - }); - - test('message is retrieved from storage when no other edits', done => { - const storageStub = sandbox.stub(element.$.storage, 'getDraftComment'); - const loadSpy = sandbox.spy(element, '_loadLocalDraft'); - - element.changeNum = 1; - element.patchNum = 1; - element.comment = { - author: { - name: 'Mr. Peanutbutter', - email: 'tenn1sballchaser@aol.com', - }, - line: 5, - }; - flush(() => { - assert.isTrue(loadSpy.called); - assert.isTrue(storageStub.called); - done(); - }); - }); - - test('_getPatchNum', () => { - element.side = 'PARENT'; - element.patchNum = 1; - assert.equal(element._getPatchNum(), 'PARENT'); - element.side = 'REVISION'; - assert.equal(element._getPatchNum(), 1); - }); - - test('comment expand and collapse', () => { - element.collapsed = true; - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are not visible'); - assert.isNotOk(element.textarea, 'textarea is not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is visible'); - - element.collapsed = false; - assert.isFalse(element.collapsed); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are visible'); - assert.isNotOk(element.textarea, 'textarea is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is is not visible'); - }); - - suite('while editing', () => { + suite('when text is empty', () => { setup(() => { - element.editing = true; - element._messageText = 'test'; - sandbox.stub(element, '_handleCancel'); - sandbox.stub(element, '_handleSave'); - flushAsynchronousOperations(); + element._messageText = ''; + element.comment = {}; }); - suite('when text is empty', () => { - setup(() => { - element._messageText = ''; - element.comment = {}; - }); - - test('esc closes comment when text is empty', () => { - MockInteractions.pressAndReleaseKeyOn( - element.textarea, 27); // esc - assert.isTrue(element._handleCancel.called); - }); - - test('ctrl+enter does not save', () => { - MockInteractions.pressAndReleaseKeyOn( - element.textarea, 13, 'ctrl'); // ctrl + enter - assert.isFalse(element._handleSave.called); - }); - - test('meta+enter does not save', () => { - MockInteractions.pressAndReleaseKeyOn( - element.textarea, 13, 'meta'); // meta + enter - assert.isFalse(element._handleSave.called); - }); - - test('ctrl+s does not save', () => { - MockInteractions.pressAndReleaseKeyOn( - element.textarea, 83, 'ctrl'); // ctrl + s - assert.isFalse(element._handleSave.called); - }); - }); - - test('esc does not close comment that has content', () => { + test('esc closes comment when text is empty', () => { MockInteractions.pressAndReleaseKeyOn( element.textarea, 27); // esc - assert.isFalse(element._handleCancel.called); + assert.isTrue(element._handleCancel.called); }); - test('ctrl+enter saves', () => { + test('ctrl+enter does not save', () => { MockInteractions.pressAndReleaseKeyOn( element.textarea, 13, 'ctrl'); // ctrl + enter - assert.isTrue(element._handleSave.called); + assert.isFalse(element._handleSave.called); }); - test('meta+enter saves', () => { + test('meta+enter does not save', () => { MockInteractions.pressAndReleaseKeyOn( element.textarea, 13, 'meta'); // meta + enter - assert.isTrue(element._handleSave.called); + assert.isFalse(element._handleSave.called); }); - test('ctrl+s saves', () => { + test('ctrl+s does not save', () => { MockInteractions.pressAndReleaseKeyOn( element.textarea, 83, 'ctrl'); // ctrl + s - assert.isTrue(element._handleSave.called); - }); - }); - test('delete comment button for non-admins is hidden', () => { - element._isAdmin = false; - assert.isFalse(element.shadowRoot - .querySelector('.action.delete') - .classList.contains('showDeleteButtons')); - }); - - test('delete comment button for admins with draft is hidden', () => { - element._isAdmin = false; - element.draft = true; - assert.isFalse(element.shadowRoot - .querySelector('.action.delete') - .classList.contains('showDeleteButtons')); - }); - - test('delete comment', done => { - sandbox.stub( - element.$.restAPI, 'deleteComment').returns(Promise.resolve({})); - sandbox.spy(element.confirmDeleteOverlay, 'open'); - element.changeNum = 42; - element.patchNum = 0xDEADBEEF; - element._isAdmin = true; - assert.isTrue(element.shadowRoot - .querySelector('.action.delete') - .classList.contains('showDeleteButtons')); - MockInteractions.tap(element.shadowRoot - .querySelector('.action.delete')); - flush(() => { - element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => { - const dialog = - window.confirmDeleteOverlay - .querySelector('#confirmDeleteComment'); - dialog.message = 'removal reason'; - element._handleConfirmDeleteComment(); - assert.isTrue(element.$.restAPI.deleteComment.calledWith( - 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason')); - done(); - }); + assert.isFalse(element._handleSave.called); }); }); - suite('draft update reporting', () => { - let endStub; - let getTimerStub; - let mockEvent; - - setup(() => { - mockEvent = {preventDefault() {}}; - sandbox.stub(element, 'save') - .returns(Promise.resolve({})); - sandbox.stub(element, '_discardDraft') - .returns(Promise.resolve({})); - endStub = sinon.stub(); - getTimerStub = sandbox.stub(element.$.reporting, 'getTimer') - .returns({end: endStub}); - }); - - test('create', () => { - element.comment = {}; - return element._handleSave(mockEvent).then(() => { - assert.isTrue(endStub.calledOnce); - assert.isTrue(getTimerStub.calledOnce); - assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment'); - }); - }); - - test('update', () => { - element.comment = {id: 'abc_123'}; - return element._handleSave(mockEvent).then(() => { - assert.isTrue(endStub.calledOnce); - assert.isTrue(getTimerStub.calledOnce); - assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment'); - }); - }); - - test('discard', () => { - element.comment = {id: 'abc_123'}; - sandbox.stub(element, '_closeConfirmDiscardOverlay'); - return element._handleConfirmDiscard(mockEvent).then(() => { - assert.isTrue(endStub.calledOnce); - assert.isTrue(getTimerStub.calledOnce); - assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment'); - }); - }); + test('esc does not close comment that has content', () => { + MockInteractions.pressAndReleaseKeyOn( + element.textarea, 27); // esc + assert.isFalse(element._handleCancel.called); }); - test('edit reports interaction', () => { - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - MockInteractions.tap(element.shadowRoot - .querySelector('.edit')); - assert.isTrue(reportStub.calledOnce); + test('ctrl+enter saves', () => { + MockInteractions.pressAndReleaseKeyOn( + element.textarea, 13, 'ctrl'); // ctrl + enter + assert.isTrue(element._handleSave.called); }); - test('discard reports interaction', () => { - const reportStub = sandbox.stub(element.$.reporting, - 'recordDraftInteraction'); - element.draft = true; - MockInteractions.tap(element.shadowRoot - .querySelector('.discard')); - assert.isTrue(reportStub.calledOnce); + test('meta+enter saves', () => { + MockInteractions.pressAndReleaseKeyOn( + element.textarea, 13, 'meta'); // meta + enter + assert.isTrue(element._handleSave.called); + }); + + test('ctrl+s saves', () => { + MockInteractions.pressAndReleaseKeyOn( + element.textarea, 83, 'ctrl'); // ctrl + s + assert.isTrue(element._handleSave.called); + }); + }); + test('delete comment button for non-admins is hidden', () => { + element._isAdmin = false; + assert.isFalse(element.shadowRoot + .querySelector('.action.delete') + .classList.contains('showDeleteButtons')); + }); + + test('delete comment button for admins with draft is hidden', () => { + element._isAdmin = false; + element.draft = true; + assert.isFalse(element.shadowRoot + .querySelector('.action.delete') + .classList.contains('showDeleteButtons')); + }); + + test('delete comment', done => { + sandbox.stub( + element.$.restAPI, 'deleteComment').returns(Promise.resolve({})); + sandbox.spy(element.confirmDeleteOverlay, 'open'); + element.changeNum = 42; + element.patchNum = 0xDEADBEEF; + element._isAdmin = true; + assert.isTrue(element.shadowRoot + .querySelector('.action.delete') + .classList.contains('showDeleteButtons')); + MockInteractions.tap(element.shadowRoot + .querySelector('.action.delete')); + flush(() => { + element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => { + const dialog = + window.confirmDeleteOverlay + .querySelector('#confirmDeleteComment'); + dialog.message = 'removal reason'; + element._handleConfirmDeleteComment(); + assert.isTrue(element.$.restAPI.deleteComment.calledWith( + 42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason')); + done(); + }); }); }); - suite('gr-comment draft tests', () => { - let element; - let sandbox; + suite('draft update reporting', () => { + let endStub; + let getTimerStub; + let mockEvent; setup(() => { - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(null); }, - saveDiffDraft() { - return Promise.resolve({ - ok: true, - text() { - return Promise.resolve( - ')]}\'\n{' + - '"id": "baf0414d_40572e03",' + - '"path": "/path/to/file",' + - '"line": 5,' + - '"updated": "2015-12-08 21:52:36.177000000",' + - '"message": "saved!"' + - '}' - ); - }, - }); - }, - removeChangeReviewer() { - return Promise.resolve({ok: true}); - }, - }); - stub('gr-storage', { - getDraftComment() { return null; }, - }); - element = fixture('draft'); - element.changeNum = 42; - element.patchNum = 1; - element.editing = false; - element.comment = { - __commentSide: 'right', - __draft: true, - __draftID: 'temp_draft_id', - path: '/path/to/file', - line: 5, - }; - element.commentSide = 'right'; - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('button visibility states', () => { - element.showActions = false; - assert.isTrue(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isTrue(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - element.showActions = true; - assert.isFalse(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isTrue(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - element.draft = true; - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.edit')), 'edit is visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.discard')), 'discard is visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.save')), 'save is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.cancel')), 'cancel is not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.resolve')), 'resolve is visible'); - assert.isFalse(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isTrue(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - element.editing = true; - flushAsynchronousOperations(); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.edit')), 'edit is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.discard')), 'discard not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.save')), 'save is visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.cancel')), 'cancel is visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.resolve')), 'resolve is visible'); - assert.isFalse(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isTrue(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - element.draft = false; - element.editing = false; - flushAsynchronousOperations(); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.edit')), 'edit is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.discard')), - 'discard is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.save')), 'save is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.cancel')), 'cancel is not visible'); - assert.isFalse(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isTrue(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - element.comment.id = 'foo'; - element.draft = true; - element.editing = true; - flushAsynchronousOperations(); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.cancel')), 'cancel is visible'); - assert.isFalse(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isTrue(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - // Delete button is not hidden by default - assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden); - - element.isRobotComment = true; - element.draft = true; - assert.isTrue(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isFalse(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - // It is not expected to see Robot comment drafts, but if they appear, - // they will behave the same as non-drafts. - element.draft = false; - assert.isTrue(element.shadowRoot - .querySelector('.humanActions').hasAttribute('hidden')); - assert.isFalse(element.shadowRoot - .querySelector('.robotActions').hasAttribute('hidden')); - - // A robot comment with run ID should display plain text. - element.set(['comment', 'robot_run_id'], 'text'); - element.editing = false; - element.collapsed = false; - flushAsynchronousOperations(); - assert.isTrue(element.shadowRoot - .querySelector('.robotRun.link').textContent === 'Run Details'); - - // A robot comment with run ID and url should display a link. - element.set(['comment', 'url'], '/path/to/run'); - flushAsynchronousOperations(); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.robotRun.link')).display, - 'none'); - - // Delete button is hidden for robot comments - assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden); - }); - - test('collapsible drafts', () => { - assert.isTrue(element.collapsed); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are not visible'); - assert.isNotOk(element.textarea, 'textarea is not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is visible'); - - MockInteractions.tap(element.$.header); - assert.isFalse(element.collapsed); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are visible'); - assert.isNotOk(element.textarea, 'textarea is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is is not visible'); - - // When the edit button is pressed, should still see the actions - // and also textarea - MockInteractions.tap(element.shadowRoot - .querySelector('.edit')); - flushAsynchronousOperations(); - assert.isFalse(element.collapsed); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are visible'); - assert.isTrue(isVisible(element.textarea), 'textarea is visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is not visible'); - - // When toggle again, everything should be hidden except for textarea - // and header middle content should be visible - MockInteractions.tap(element.$.header); - assert.isTrue(element.collapsed); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are not visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-textarea')), - 'textarea is not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is visible'); - - // When toggle again, textarea should remain open in the state it was - // before - MockInteractions.tap(element.$.header); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('gr-formatted-text')), - 'gr-formatted-text is not visible'); - assert.isTrue(isVisible(element.shadowRoot - .querySelector('.actions')), - 'actions are visible'); - assert.isTrue(isVisible(element.textarea), 'textarea is visible'); - assert.isFalse(isVisible(element.shadowRoot - .querySelector('.collapsedContent')), - 'header middle content is not visible'); - }); - - test('robot comment layout', () => { - const comment = Object.assign({ - robot_id: 'happy_robot_id', - url: '/robot/comment', - author: { - name: 'Happy Robot', - }, - }, element.comment); - element.comment = comment; - element.collapsed = false; - flushAsynchronousOperations(); - - let runIdMessage; - runIdMessage = element.shadowRoot - .querySelector('.runIdMessage'); - assert.isFalse(runIdMessage.hidden); - - const runDetailsLink = element.shadowRoot - .querySelector('.robotRunLink'); - assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1); - - const robotServiceName = element.shadowRoot - .querySelector('.authorName'); - assert.isTrue(robotServiceName.textContent === 'happy_robot_id'); - - const authorName = element.shadowRoot - .querySelector('.robotId'); - assert.isTrue(authorName.innerText === 'Happy Robot'); - - element.collapsed = true; - flushAsynchronousOperations(); - runIdMessage = element.shadowRoot - .querySelector('.runIdMessage'); - assert.isTrue(runIdMessage.hidden); - }); - - test('draft creation/cancellation', done => { - assert.isFalse(element.editing); - MockInteractions.tap(element.shadowRoot - .querySelector('.edit')); - assert.isTrue(element.editing); - - element._messageText = ''; - const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); - - // Save should be disabled on an empty message. - let disabled = element.shadowRoot - .querySelector('.save').hasAttribute('disabled'); - assert.isTrue(disabled, 'save button should be disabled.'); - element._messageText = ' '; - disabled = element.shadowRoot - .querySelector('.save').hasAttribute('disabled'); - assert.isTrue(disabled, 'save button should be disabled.'); - - const updateStub = sinon.stub(); - element.addEventListener('comment-update', updateStub); - - let numDiscardEvents = 0; - element.addEventListener('comment-discard', e => { - numDiscardEvents++; - assert.isFalse(eraseMessageDraftSpy.called); - if (numDiscardEvents === 2) { - assert.isFalse(updateStub.called); - done(); - } - }); - MockInteractions.tap(element.shadowRoot - .querySelector('.cancel')); - element.flushDebouncer('fire-update'); - element._messageText = ''; - flushAsynchronousOperations(); - MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc - }); - - test('draft discard removes message from storage', done => { - element._messageText = ''; - const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); - sandbox.stub(element, '_closeConfirmDiscardOverlay'); - - element.addEventListener('comment-discard', e => { - assert.isTrue(eraseMessageDraftSpy.called); - done(); - }); - element._handleConfirmDiscard({preventDefault: sinon.stub()}); - }); - - test('storage is cleared only after save success', () => { - element._messageText = 'test'; - const eraseStub = sandbox.stub(element, '_eraseDraftComment'); - sandbox.stub(element.$.restAPI, 'getResponseObject') + mockEvent = {preventDefault() {}}; + sandbox.stub(element, 'save') .returns(Promise.resolve({})); + sandbox.stub(element, '_discardDraft') + .returns(Promise.resolve({})); + endStub = sinon.stub(); + getTimerStub = sandbox.stub(element.$.reporting, 'getTimer') + .returns({end: endStub}); + }); - sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false})); + test('create', () => { + element.comment = {}; + return element._handleSave(mockEvent).then(() => { + assert.isTrue(endStub.calledOnce); + assert.isTrue(getTimerStub.calledOnce); + assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment'); + }); + }); - const savePromise = element.save(); - assert.isFalse(eraseStub.called); - return savePromise.then(() => { - assert.isFalse(eraseStub.called); + test('update', () => { + element.comment = {id: 'abc_123'}; + return element._handleSave(mockEvent).then(() => { + assert.isTrue(endStub.calledOnce); + assert.isTrue(getTimerStub.calledOnce); + assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment'); + }); + }); - element._saveDraft.restore(); - sandbox.stub(element, '_saveDraft') - .returns(Promise.resolve({ok: true})); - return element.save().then(() => { - assert.isTrue(eraseStub.called); + test('discard', () => { + element.comment = {id: 'abc_123'}; + sandbox.stub(element, '_closeConfirmDiscardOverlay'); + return element._handleConfirmDiscard(mockEvent).then(() => { + assert.isTrue(endStub.calledOnce); + assert.isTrue(getTimerStub.calledOnce); + assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment'); + }); + }); + }); + + test('edit reports interaction', () => { + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + MockInteractions.tap(element.shadowRoot + .querySelector('.edit')); + assert.isTrue(reportStub.calledOnce); + }); + + test('discard reports interaction', () => { + const reportStub = sandbox.stub(element.$.reporting, + 'recordDraftInteraction'); + element.draft = true; + MockInteractions.tap(element.shadowRoot + .querySelector('.discard')); + assert.isTrue(reportStub.calledOnce); + }); + }); + + suite('gr-comment draft tests', () => { + let element; + let sandbox; + + setup(() => { + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + saveDiffDraft() { + return Promise.resolve({ + ok: true, + text() { + return Promise.resolve( + ')]}\'\n{' + + '"id": "baf0414d_40572e03",' + + '"path": "/path/to/file",' + + '"line": 5,' + + '"updated": "2015-12-08 21:52:36.177000000",' + + '"message": "saved!"' + + '}' + ); + }, }); - }); + }, + removeChangeReviewer() { + return Promise.resolve({ok: true}); + }, }); - - test('_computeSaveDisabled', () => { - const comment = {unresolved: true}; - const msgComment = {message: 'test', unresolved: true}; - assert.equal(element._computeSaveDisabled('', comment, false), true); - assert.equal(element._computeSaveDisabled('test', comment, false), false); - assert.equal(element._computeSaveDisabled('', msgComment, false), true); - assert.equal( - element._computeSaveDisabled('test', msgComment, false), false); - assert.equal( - element._computeSaveDisabled('test2', msgComment, false), false); - assert.equal(element._computeSaveDisabled('test', comment, true), false); - assert.equal(element._computeSaveDisabled('', comment, true), true); - assert.equal(element._computeSaveDisabled('', comment, false), true); + stub('gr-storage', { + getDraftComment() { return null; }, }); + element = fixture('draft'); + element.changeNum = 42; + element.patchNum = 1; + element.editing = false; + element.comment = { + __commentSide: 'right', + __draft: true, + __draftID: 'temp_draft_id', + path: '/path/to/file', + line: 5, + }; + element.commentSide = 'right'; + sandbox = sinon.sandbox.create(); + }); - suite('confirm discard', () => { - let discardStub; - let overlayStub; - let mockEvent; + teardown(() => { + sandbox.restore(); + }); - setup(() => { - discardStub = sandbox.stub(element, '_discardDraft'); - overlayStub = sandbox.stub(element, '_openOverlay') - .returns(Promise.resolve()); - mockEvent = {preventDefault: sinon.stub()}; - }); + test('button visibility states', () => { + element.showActions = false; + assert.isTrue(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); - test('confirms discard of comments with message text', () => { - element._messageText = 'test'; - element._handleDiscard(mockEvent); - assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay)); - assert.isFalse(discardStub.called); - }); + element.showActions = true; + assert.isFalse(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); - test('no confirmation for comments without message text', () => { - element._messageText = ''; - element._handleDiscard(mockEvent); - assert.isFalse(overlayStub.called); - assert.isTrue(discardStub.calledOnce); - }); - }); + element.draft = true; + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.edit')), 'edit is visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.discard')), 'discard is visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.save')), 'save is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.cancel')), 'cancel is not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.resolve')), 'resolve is visible'); + assert.isFalse(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); - test('ctrl+s saves comment', done => { - const stub = sinon.stub(element, 'save', () => { - assert.isTrue(stub.called); - stub.restore(); + element.editing = true; + flushAsynchronousOperations(); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.edit')), 'edit is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.discard')), 'discard not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.save')), 'save is visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.cancel')), 'cancel is visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.resolve')), 'resolve is visible'); + assert.isFalse(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); + + element.draft = false; + element.editing = false; + flushAsynchronousOperations(); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.edit')), 'edit is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.discard')), + 'discard is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.save')), 'save is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.cancel')), 'cancel is not visible'); + assert.isFalse(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); + + element.comment.id = 'foo'; + element.draft = true; + element.editing = true; + flushAsynchronousOperations(); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.cancel')), 'cancel is visible'); + assert.isFalse(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isTrue(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); + + // Delete button is not hidden by default + assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden); + + element.isRobotComment = true; + element.draft = true; + assert.isTrue(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isFalse(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); + + // It is not expected to see Robot comment drafts, but if they appear, + // they will behave the same as non-drafts. + element.draft = false; + assert.isTrue(element.shadowRoot + .querySelector('.humanActions').hasAttribute('hidden')); + assert.isFalse(element.shadowRoot + .querySelector('.robotActions').hasAttribute('hidden')); + + // A robot comment with run ID should display plain text. + element.set(['comment', 'robot_run_id'], 'text'); + element.editing = false; + element.collapsed = false; + flushAsynchronousOperations(); + assert.isTrue(element.shadowRoot + .querySelector('.robotRun.link').textContent === 'Run Details'); + + // A robot comment with run ID and url should display a link. + element.set(['comment', 'url'], '/path/to/run'); + flushAsynchronousOperations(); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.robotRun.link')).display, + 'none'); + + // Delete button is hidden for robot comments + assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden); + }); + + test('collapsible drafts', () => { + assert.isTrue(element.collapsed); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are not visible'); + assert.isNotOk(element.textarea, 'textarea is not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is visible'); + + MockInteractions.tap(element.$.header); + assert.isFalse(element.collapsed); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are visible'); + assert.isNotOk(element.textarea, 'textarea is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is is not visible'); + + // When the edit button is pressed, should still see the actions + // and also textarea + MockInteractions.tap(element.shadowRoot + .querySelector('.edit')); + flushAsynchronousOperations(); + assert.isFalse(element.collapsed); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are visible'); + assert.isTrue(isVisible(element.textarea), 'textarea is visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is not visible'); + + // When toggle again, everything should be hidden except for textarea + // and header middle content should be visible + MockInteractions.tap(element.$.header); + assert.isTrue(element.collapsed); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are not visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-textarea')), + 'textarea is not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is visible'); + + // When toggle again, textarea should remain open in the state it was + // before + MockInteractions.tap(element.$.header); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('gr-formatted-text')), + 'gr-formatted-text is not visible'); + assert.isTrue(isVisible(element.shadowRoot + .querySelector('.actions')), + 'actions are visible'); + assert.isTrue(isVisible(element.textarea), 'textarea is visible'); + assert.isFalse(isVisible(element.shadowRoot + .querySelector('.collapsedContent')), + 'header middle content is not visible'); + }); + + test('robot comment layout', () => { + const comment = Object.assign({ + robot_id: 'happy_robot_id', + url: '/robot/comment', + author: { + name: 'Happy Robot', + }, + }, element.comment); + element.comment = comment; + element.collapsed = false; + flushAsynchronousOperations(); + + let runIdMessage; + runIdMessage = element.shadowRoot + .querySelector('.runIdMessage'); + assert.isFalse(runIdMessage.hidden); + + const runDetailsLink = element.shadowRoot + .querySelector('.robotRunLink'); + assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1); + + const robotServiceName = element.shadowRoot + .querySelector('.authorName'); + assert.isTrue(robotServiceName.textContent === 'happy_robot_id'); + + const authorName = element.shadowRoot + .querySelector('.robotId'); + assert.isTrue(authorName.innerText === 'Happy Robot'); + + element.collapsed = true; + flushAsynchronousOperations(); + runIdMessage = element.shadowRoot + .querySelector('.runIdMessage'); + assert.isTrue(runIdMessage.hidden); + }); + + test('draft creation/cancellation', done => { + assert.isFalse(element.editing); + MockInteractions.tap(element.shadowRoot + .querySelector('.edit')); + assert.isTrue(element.editing); + + element._messageText = ''; + const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); + + // Save should be disabled on an empty message. + let disabled = element.shadowRoot + .querySelector('.save').hasAttribute('disabled'); + assert.isTrue(disabled, 'save button should be disabled.'); + element._messageText = ' '; + disabled = element.shadowRoot + .querySelector('.save').hasAttribute('disabled'); + assert.isTrue(disabled, 'save button should be disabled.'); + + const updateStub = sinon.stub(); + element.addEventListener('comment-update', updateStub); + + let numDiscardEvents = 0; + element.addEventListener('comment-discard', e => { + numDiscardEvents++; + assert.isFalse(eraseMessageDraftSpy.called); + if (numDiscardEvents === 2) { + assert.isFalse(updateStub.called); done(); - return Promise.resolve(); + } + }); + MockInteractions.tap(element.shadowRoot + .querySelector('.cancel')); + element.flushDebouncer('fire-update'); + element._messageText = ''; + flushAsynchronousOperations(); + MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc + }); + + test('draft discard removes message from storage', done => { + element._messageText = ''; + const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment'); + sandbox.stub(element, '_closeConfirmDiscardOverlay'); + + element.addEventListener('comment-discard', e => { + assert.isTrue(eraseMessageDraftSpy.called); + done(); + }); + element._handleConfirmDiscard({preventDefault: sinon.stub()}); + }); + + test('storage is cleared only after save success', () => { + element._messageText = 'test'; + const eraseStub = sandbox.stub(element, '_eraseDraftComment'); + sandbox.stub(element.$.restAPI, 'getResponseObject') + .returns(Promise.resolve({})); + + sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false})); + + const savePromise = element.save(); + assert.isFalse(eraseStub.called); + return savePromise.then(() => { + assert.isFalse(eraseStub.called); + + element._saveDraft.restore(); + sandbox.stub(element, '_saveDraft') + .returns(Promise.resolve({ok: true})); + return element.save().then(() => { + assert.isTrue(eraseStub.called); }); - element._messageText = 'is that the horse from horsing around??'; - element.editing = true; - flushAsynchronousOperations(); - MockInteractions.pressAndReleaseKeyOn( - element.textarea.$.textarea.textarea, - 83, 'ctrl'); // 'ctrl + s' + }); + }); + + test('_computeSaveDisabled', () => { + const comment = {unresolved: true}; + const msgComment = {message: 'test', unresolved: true}; + assert.equal(element._computeSaveDisabled('', comment, false), true); + assert.equal(element._computeSaveDisabled('test', comment, false), false); + assert.equal(element._computeSaveDisabled('', msgComment, false), true); + assert.equal( + element._computeSaveDisabled('test', msgComment, false), false); + assert.equal( + element._computeSaveDisabled('test2', msgComment, false), false); + assert.equal(element._computeSaveDisabled('test', comment, true), false); + assert.equal(element._computeSaveDisabled('', comment, true), true); + assert.equal(element._computeSaveDisabled('', comment, false), true); + }); + + suite('confirm discard', () => { + let discardStub; + let overlayStub; + let mockEvent; + + setup(() => { + discardStub = sandbox.stub(element, '_discardDraft'); + overlayStub = sandbox.stub(element, '_openOverlay') + .returns(Promise.resolve()); + mockEvent = {preventDefault: sinon.stub()}; }); - test('draft saving/editing', done => { - const fireStub = sinon.stub(element, 'fire'); - const cancelDebounce = sandbox.stub(element, 'cancelDebouncer'); + test('confirms discard of comments with message text', () => { + element._messageText = 'test'; + element._handleDiscard(mockEvent); + assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay)); + assert.isFalse(discardStub.called); + }); - element.draft = true; + test('no confirmation for comments without message text', () => { + element._messageText = ''; + element._handleDiscard(mockEvent); + assert.isFalse(overlayStub.called); + assert.isTrue(discardStub.calledOnce); + }); + }); + + test('ctrl+s saves comment', done => { + const stub = sinon.stub(element, 'save', () => { + assert.isTrue(stub.called); + stub.restore(); + done(); + return Promise.resolve(); + }); + element._messageText = 'is that the horse from horsing around??'; + element.editing = true; + flushAsynchronousOperations(); + MockInteractions.pressAndReleaseKeyOn( + element.textarea.$.textarea.textarea, + 83, 'ctrl'); // 'ctrl + s' + }); + + test('draft saving/editing', done => { + const fireStub = sinon.stub(element, 'fire'); + const cancelDebounce = sandbox.stub(element, 'cancelDebouncer'); + + element.draft = true; + MockInteractions.tap(element.shadowRoot + .querySelector('.edit')); + element._messageText = 'good news, everyone!'; + element.flushDebouncer('fire-update'); + element.flushDebouncer('store'); + assert(fireStub.calledWith('comment-update'), + 'comment-update should be sent'); + assert.isTrue(fireStub.calledOnce); + + element._messageText = 'good news, everyone!'; + element.flushDebouncer('fire-update'); + element.flushDebouncer('store'); + assert.isTrue(fireStub.calledOnce, + 'No events should fire for text editing'); + + MockInteractions.tap(element.shadowRoot + .querySelector('.save')); + + assert.isTrue(element.disabled, + 'Element should be disabled when creating draft.'); + + element._xhrPromise.then(draft => { + assert(fireStub.calledWith('comment-save'), + 'comment-save should be sent'); + assert(cancelDebounce.calledWith('store')); + + assert.deepEqual(fireStub.lastCall.args[1], { + comment: { + __commentSide: 'right', + __draft: true, + __draftID: 'temp_draft_id', + id: 'baf0414d_40572e03', + line: 5, + message: 'saved!', + path: '/path/to/file', + updated: '2015-12-08 21:52:36.177000000', + }, + patchNum: 1, + }); + assert.isFalse(element.disabled, + 'Element should be enabled when done creating draft.'); + assert.equal(draft.message, 'saved!'); + assert.isFalse(element.editing); + }).then(() => { MockInteractions.tap(element.shadowRoot .querySelector('.edit')); - element._messageText = 'good news, everyone!'; - element.flushDebouncer('fire-update'); - element.flushDebouncer('store'); - assert(fireStub.calledWith('comment-update'), - 'comment-update should be sent'); - assert.isTrue(fireStub.calledOnce); - - element._messageText = 'good news, everyone!'; - element.flushDebouncer('fire-update'); - element.flushDebouncer('store'); - assert.isTrue(fireStub.calledOnce, - 'No events should fire for text editing'); - + element._messageText = 'You’ll be delivering a package to Chapek 9, ' + + 'a world where humans are killed on sight.'; MockInteractions.tap(element.shadowRoot .querySelector('.save')); - assert.isTrue(element.disabled, - 'Element should be disabled when creating draft.'); + 'Element should be disabled when updating draft.'); element._xhrPromise.then(draft => { - assert(fireStub.calledWith('comment-save'), - 'comment-save should be sent'); - assert(cancelDebounce.calledWith('store')); - - assert.deepEqual(fireStub.lastCall.args[1], { - comment: { - __commentSide: 'right', - __draft: true, - __draftID: 'temp_draft_id', - id: 'baf0414d_40572e03', - line: 5, - message: 'saved!', - path: '/path/to/file', - updated: '2015-12-08 21:52:36.177000000', - }, - patchNum: 1, - }); assert.isFalse(element.disabled, - 'Element should be enabled when done creating draft.'); + 'Element should be enabled when done updating draft.'); assert.equal(draft.message, 'saved!'); assert.isFalse(element.editing); - }).then(() => { - MockInteractions.tap(element.shadowRoot - .querySelector('.edit')); - element._messageText = 'You’ll be delivering a package to Chapek 9, ' + - 'a world where humans are killed on sight.'; - MockInteractions.tap(element.shadowRoot - .querySelector('.save')); - assert.isTrue(element.disabled, - 'Element should be disabled when updating draft.'); - - element._xhrPromise.then(draft => { - assert.isFalse(element.disabled, - 'Element should be enabled when done updating draft.'); - assert.equal(draft.message, 'saved!'); - assert.isFalse(element.editing); - fireStub.restore(); - done(); - }); - }); - }); - - test('draft prevent save when disabled', () => { - const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve()); - element.showActions = true; - element.draft = true; - MockInteractions.tap(element.$.header); - MockInteractions.tap(element.shadowRoot - .querySelector('.edit')); - element._messageText = 'good news, everyone!'; - element.flushDebouncer('fire-update'); - element.flushDebouncer('store'); - - element.disabled = true; - MockInteractions.tap(element.shadowRoot - .querySelector('.save')); - assert.isFalse(saveStub.called); - - element.disabled = false; - MockInteractions.tap(element.shadowRoot - .querySelector('.save')); - assert.isTrue(saveStub.calledOnce); - }); - - test('proper event fires on resolve, comment is not saved', done => { - const save = sandbox.stub(element, 'save'); - element.addEventListener('comment-update', e => { - assert.isTrue(e.detail.comment.unresolved); - assert.isFalse(save.called); + fireStub.restore(); done(); }); - MockInteractions.tap(element.shadowRoot - .querySelector('.resolve input')); - }); - - test('resolved comment state indicated by checkbox', () => { - sandbox.stub(element, 'save'); - element.comment = {unresolved: false}; - assert.isTrue(element.shadowRoot - .querySelector('.resolve input').checked); - element.comment = {unresolved: true}; - assert.isFalse(element.shadowRoot - .querySelector('.resolve input').checked); - }); - - test('resolved checkbox saves with tap when !editing', () => { - element.editing = false; - const save = sandbox.stub(element, 'save'); - - element.comment = {unresolved: false}; - assert.isTrue(element.shadowRoot - .querySelector('.resolve input').checked); - element.comment = {unresolved: true}; - assert.isFalse(element.shadowRoot - .querySelector('.resolve input').checked); - assert.isFalse(save.called); - MockInteractions.tap(element.$.resolvedCheckbox); - assert.isTrue(element.shadowRoot - .querySelector('.resolve input').checked); - assert.isTrue(save.called); - }); - - suite('draft saving messages', () => { - test('_getSavingMessage', () => { - assert.equal(element._getSavingMessage(0), 'All changes saved'); - assert.equal(element._getSavingMessage(1), 'Saving 1 draft...'); - assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...'); - assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...'); - }); - - test('_show{Start,End}Request', () => { - const updateStub = sandbox.stub(element, '_updateRequestToast'); - element._numPendingDraftRequests.number = 1; - - element._showStartRequest(); - assert.isTrue(updateStub.calledOnce); - assert.equal(updateStub.lastCall.args[0], 2); - assert.equal(element._numPendingDraftRequests.number, 2); - - element._showEndRequest(); - assert.isTrue(updateStub.calledTwice); - assert.equal(updateStub.lastCall.args[0], 1); - assert.equal(element._numPendingDraftRequests.number, 1); - - element._showEndRequest(); - assert.isTrue(updateStub.calledThrice); - assert.equal(updateStub.lastCall.args[0], 0); - assert.equal(element._numPendingDraftRequests.number, 0); - }); - }); - - test('cancelling an unsaved draft discards, persists in storage', () => { - const discardSpy = sandbox.spy(element, '_fireDiscard'); - const storeStub = sandbox.stub(element.$.storage, 'setDraftComment'); - const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment'); - element._messageText = 'test text'; - flushAsynchronousOperations(); - element.flushDebouncer('store'); - - assert.isTrue(storeStub.called); - assert.equal(storeStub.lastCall.args[1], 'test text'); - element._handleCancel({preventDefault: () => {}}); - assert.isTrue(discardSpy.called); - assert.isFalse(eraseStub.called); - }); - - test('cancelling edit on a saved draft does not store', () => { - element.comment.id = 'foo'; - const discardSpy = sandbox.spy(element, '_fireDiscard'); - const storeStub = sandbox.stub(element.$.storage, 'setDraftComment'); - element._messageText = 'test text'; - flushAsynchronousOperations(); - element.flushDebouncer('store'); - - assert.isFalse(storeStub.called); - element._handleCancel({preventDefault: () => {}}); - assert.isTrue(discardSpy.called); - }); - - test('deleting text from saved draft and saving deletes the draft', () => { - element.comment = {id: 'foo', message: 'test'}; - element._messageText = ''; - const discardStub = sandbox.stub(element, '_discardDraft'); - - element.save(); - assert.isTrue(discardStub.called); - }); - - test('_handleFix fires create-fix event', done => { - element.addEventListener('create-fix-comment', e => { - assert.deepEqual(e.detail, element._getEventPayload()); - done(); - }); - element.isRobotComment = true; - element.comments = [element.comment]; - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('.fix')); - }); - - test('do not show Please Fix button if human reply exists', () => { - element.comments = [ - { - robot_id: 'happy_robot_id', - robot_run_id: '5838406743490560', - fix_suggestions: [ - { - fix_id: '478ff847_3bf47aaf', - description: 'Make the smiley happier by giving it a nose.', - replacements: [ - { - path: 'Documentation/config-gerrit.txt', - range: { - start_line: 10, - start_character: 7, - end_line: 10, - end_character: 9, - }, - replacement: ':-)', - }, - ], - }, - ], - author: { - _account_id: 1030912, - name: 'Alice Kober-Sotzek', - email: 'aliceks@google.com', - avatars: [ - { - url: '/s32-p/photo.jpg', - height: 32, - }, - { - url: '/AaAdOFzPlFI/s56-p/photo.jpg', - height: 56, - }, - { - url: '/AaAdOFzPlFI/s100-p/photo.jpg', - height: 100, - }, - { - url: '/AaAdOFzPlFI/s120-p/photo.jpg', - height: 120, - }, - ], - }, - patch_set: 1, - id: 'eb0d03fd_5e95904f', - line: 10, - updated: '2017-04-04 15:36:17.000000000', - message: 'This is a robot comment with a fix.', - unresolved: false, - __commentSide: 'right', - collapsed: false, - }, - { - __draft: true, - __draftID: '0.wbrfbwj89sa', - __date: '2019-12-04T13:41:03.689Z', - path: 'Documentation/config-gerrit.txt', - patchNum: 1, - side: 'REVISION', - __commentSide: 'right', - line: 10, - in_reply_to: 'eb0d03fd_5e95904f', - message: '> This is a robot comment with a fix.\n\nPlease fix.', - unresolved: true, - }, - ]; - element.comment = element.comments[0]; - flushAsynchronousOperations(); - assert.isNull(element.shadowRoot - .querySelector('robotActions gr-button')); - }); - - test('show Please Fix if no human reply', () => { - element.comments = [ - { - robot_id: 'happy_robot_id', - robot_run_id: '5838406743490560', - fix_suggestions: [ - { - fix_id: '478ff847_3bf47aaf', - description: 'Make the smiley happier by giving it a nose.', - replacements: [ - { - path: 'Documentation/config-gerrit.txt', - range: { - start_line: 10, - start_character: 7, - end_line: 10, - end_character: 9, - }, - replacement: ':-)', - }, - ], - }, - ], - author: { - _account_id: 1030912, - name: 'Alice Kober-Sotzek', - email: 'aliceks@google.com', - avatars: [ - { - url: '/s32-p/photo.jpg', - height: 32, - }, - { - url: '/AaAdOFzPlFI/s56-p/photo.jpg', - height: 56, - }, - { - url: '/AaAdOFzPlFI/s100-p/photo.jpg', - height: 100, - }, - { - url: '/AaAdOFzPlFI/s120-p/photo.jpg', - height: 120, - }, - ], - }, - patch_set: 1, - id: 'eb0d03fd_5e95904f', - line: 10, - updated: '2017-04-04 15:36:17.000000000', - message: 'This is a robot comment with a fix.', - unresolved: false, - __commentSide: 'right', - collapsed: false, - }, - ]; - element.comment = element.comments[0]; - flushAsynchronousOperations(); - assert.isNotNull(element.shadowRoot - .querySelector('.robotActions gr-button')); - }); - - test('_handleShowFix fires open-fix-preview event', done => { - element.addEventListener('open-fix-preview', e => { - assert.deepEqual(e.detail, element._getEventPayload()); - done(); - }); - element.comment = {fix_suggestions: [{}]}; - element.isRobotComment = true; - flushAsynchronousOperations(); - - MockInteractions.tap(element.shadowRoot - .querySelector('.show-fix')); }); }); - suite('respectful tips', () => { - let element; - let sandbox; - let clock; - setup(() => { - stub('gr-rest-api-interface', { - getAccount() { return Promise.resolve(null); }, - }); - clock = sinon.useFakeTimers(); - sandbox = sinon.sandbox.create(); + test('draft prevent save when disabled', () => { + const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve()); + element.showActions = true; + element.draft = true; + MockInteractions.tap(element.$.header); + MockInteractions.tap(element.shadowRoot + .querySelector('.edit')); + element._messageText = 'good news, everyone!'; + element.flushDebouncer('fire-update'); + element.flushDebouncer('store'); + + element.disabled = true; + MockInteractions.tap(element.shadowRoot + .querySelector('.save')); + assert.isFalse(saveStub.called); + + element.disabled = false; + MockInteractions.tap(element.shadowRoot + .querySelector('.save')); + assert.isTrue(saveStub.calledOnce); + }); + + test('proper event fires on resolve, comment is not saved', done => { + const save = sandbox.stub(element, 'save'); + element.addEventListener('comment-update', e => { + assert.isTrue(e.detail.comment.unresolved); + assert.isFalse(save.called); + done(); + }); + MockInteractions.tap(element.shadowRoot + .querySelector('.resolve input')); + }); + + test('resolved comment state indicated by checkbox', () => { + sandbox.stub(element, 'save'); + element.comment = {unresolved: false}; + assert.isTrue(element.shadowRoot + .querySelector('.resolve input').checked); + element.comment = {unresolved: true}; + assert.isFalse(element.shadowRoot + .querySelector('.resolve input').checked); + }); + + test('resolved checkbox saves with tap when !editing', () => { + element.editing = false; + const save = sandbox.stub(element, 'save'); + + element.comment = {unresolved: false}; + assert.isTrue(element.shadowRoot + .querySelector('.resolve input').checked); + element.comment = {unresolved: true}; + assert.isFalse(element.shadowRoot + .querySelector('.resolve input').checked); + assert.isFalse(save.called); + MockInteractions.tap(element.$.resolvedCheckbox); + assert.isTrue(element.shadowRoot + .querySelector('.resolve input').checked); + assert.isTrue(save.called); + }); + + suite('draft saving messages', () => { + test('_getSavingMessage', () => { + assert.equal(element._getSavingMessage(0), 'All changes saved'); + assert.equal(element._getSavingMessage(1), 'Saving 1 draft...'); + assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...'); + assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...'); }); - teardown(() => { - clock.restore(); - sandbox.restore(); - }); + test('_show{Start,End}Request', () => { + const updateStub = sandbox.stub(element, '_updateRequestToast'); + element._numPendingDraftRequests.number = 1; - test('show tip when no cached record', done => { - // fake stub for storage - const respectfulGetStub = sinon.stub(); - const respectfulSetStub = sinon.stub(); - stub('gr-storage', { - getRespectfulTipVisibility() { return respectfulGetStub(); }, - setRespectfulTipVisibility() { return respectfulSetStub(); }, - }); - respectfulGetStub.returns(null); - element = fixture('draft'); - // fake random - element.getRandomNum = () => 0; - element.comment = {__editing: true}; + element._showStartRequest(); + assert.isTrue(updateStub.calledOnce); + assert.equal(updateStub.lastCall.args[0], 2); + assert.equal(element._numPendingDraftRequests.number, 2); + + element._showEndRequest(); + assert.isTrue(updateStub.calledTwice); + assert.equal(updateStub.lastCall.args[0], 1); + assert.equal(element._numPendingDraftRequests.number, 1); + + element._showEndRequest(); + assert.isTrue(updateStub.calledThrice); + assert.equal(updateStub.lastCall.args[0], 0); + assert.equal(element._numPendingDraftRequests.number, 0); + }); + }); + + test('cancelling an unsaved draft discards, persists in storage', () => { + const discardSpy = sandbox.spy(element, '_fireDiscard'); + const storeStub = sandbox.stub(element.$.storage, 'setDraftComment'); + const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment'); + element._messageText = 'test text'; + flushAsynchronousOperations(); + element.flushDebouncer('store'); + + assert.isTrue(storeStub.called); + assert.equal(storeStub.lastCall.args[1], 'test text'); + element._handleCancel({preventDefault: () => {}}); + assert.isTrue(discardSpy.called); + assert.isFalse(eraseStub.called); + }); + + test('cancelling edit on a saved draft does not store', () => { + element.comment.id = 'foo'; + const discardSpy = sandbox.spy(element, '_fireDiscard'); + const storeStub = sandbox.stub(element.$.storage, 'setDraftComment'); + element._messageText = 'test text'; + flushAsynchronousOperations(); + element.flushDebouncer('store'); + + assert.isFalse(storeStub.called); + element._handleCancel({preventDefault: () => {}}); + assert.isTrue(discardSpy.called); + }); + + test('deleting text from saved draft and saving deletes the draft', () => { + element.comment = {id: 'foo', message: 'test'}; + element._messageText = ''; + const discardStub = sandbox.stub(element, '_discardDraft'); + + element.save(); + assert.isTrue(discardStub.called); + }); + + test('_handleFix fires create-fix event', done => { + element.addEventListener('create-fix-comment', e => { + assert.deepEqual(e.detail, element._getEventPayload()); + done(); + }); + element.isRobotComment = true; + element.comments = [element.comment]; + flushAsynchronousOperations(); + + MockInteractions.tap(element.shadowRoot + .querySelector('.fix')); + }); + + test('do not show Please Fix button if human reply exists', () => { + element.comments = [ + { + robot_id: 'happy_robot_id', + robot_run_id: '5838406743490560', + fix_suggestions: [ + { + fix_id: '478ff847_3bf47aaf', + description: 'Make the smiley happier by giving it a nose.', + replacements: [ + { + path: 'Documentation/config-gerrit.txt', + range: { + start_line: 10, + start_character: 7, + end_line: 10, + end_character: 9, + }, + replacement: ':-)', + }, + ], + }, + ], + author: { + _account_id: 1030912, + name: 'Alice Kober-Sotzek', + email: 'aliceks@google.com', + avatars: [ + { + url: '/s32-p/photo.jpg', + height: 32, + }, + { + url: '/AaAdOFzPlFI/s56-p/photo.jpg', + height: 56, + }, + { + url: '/AaAdOFzPlFI/s100-p/photo.jpg', + height: 100, + }, + { + url: '/AaAdOFzPlFI/s120-p/photo.jpg', + height: 120, + }, + ], + }, + patch_set: 1, + id: 'eb0d03fd_5e95904f', + line: 10, + updated: '2017-04-04 15:36:17.000000000', + message: 'This is a robot comment with a fix.', + unresolved: false, + __commentSide: 'right', + collapsed: false, + }, + { + __draft: true, + __draftID: '0.wbrfbwj89sa', + __date: '2019-12-04T13:41:03.689Z', + path: 'Documentation/config-gerrit.txt', + patchNum: 1, + side: 'REVISION', + __commentSide: 'right', + line: 10, + in_reply_to: 'eb0d03fd_5e95904f', + message: '> This is a robot comment with a fix.\n\nPlease fix.', + unresolved: true, + }, + ]; + element.comment = element.comments[0]; + flushAsynchronousOperations(); + assert.isNull(element.shadowRoot + .querySelector('robotActions gr-button')); + }); + + test('show Please Fix if no human reply', () => { + element.comments = [ + { + robot_id: 'happy_robot_id', + robot_run_id: '5838406743490560', + fix_suggestions: [ + { + fix_id: '478ff847_3bf47aaf', + description: 'Make the smiley happier by giving it a nose.', + replacements: [ + { + path: 'Documentation/config-gerrit.txt', + range: { + start_line: 10, + start_character: 7, + end_line: 10, + end_character: 9, + }, + replacement: ':-)', + }, + ], + }, + ], + author: { + _account_id: 1030912, + name: 'Alice Kober-Sotzek', + email: 'aliceks@google.com', + avatars: [ + { + url: '/s32-p/photo.jpg', + height: 32, + }, + { + url: '/AaAdOFzPlFI/s56-p/photo.jpg', + height: 56, + }, + { + url: '/AaAdOFzPlFI/s100-p/photo.jpg', + height: 100, + }, + { + url: '/AaAdOFzPlFI/s120-p/photo.jpg', + height: 120, + }, + ], + }, + patch_set: 1, + id: 'eb0d03fd_5e95904f', + line: 10, + updated: '2017-04-04 15:36:17.000000000', + message: 'This is a robot comment with a fix.', + unresolved: false, + __commentSide: 'right', + collapsed: false, + }, + ]; + element.comment = element.comments[0]; + flushAsynchronousOperations(); + assert.isNotNull(element.shadowRoot + .querySelector('.robotActions gr-button')); + }); + + test('_handleShowFix fires open-fix-preview event', done => { + element.addEventListener('open-fix-preview', e => { + assert.deepEqual(e.detail, element._getEventPayload()); + done(); + }); + element.comment = {fix_suggestions: [{}]}; + element.isRobotComment = true; + flushAsynchronousOperations(); + + MockInteractions.tap(element.shadowRoot + .querySelector('.show-fix')); + }); + }); + + suite('respectful tips', () => { + let element; + let sandbox; + let clock; + setup(() => { + stub('gr-rest-api-interface', { + getAccount() { return Promise.resolve(null); }, + }); + clock = sinon.useFakeTimers(); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + clock.restore(); + sandbox.restore(); + }); + + test('show tip when no cached record', done => { + // fake stub for storage + const respectfulGetStub = sinon.stub(); + const respectfulSetStub = sinon.stub(); + stub('gr-storage', { + getRespectfulTipVisibility() { return respectfulGetStub(); }, + setRespectfulTipVisibility() { return respectfulSetStub(); }, + }); + respectfulGetStub.returns(null); + element = fixture('draft'); + // fake random + element.getRandomNum = () => 0; + element.comment = {__editing: true}; + flush(() => { + assert.isTrue(respectfulGetStub.called); + assert.isTrue(respectfulSetStub.called); + assert.isTrue( + !!element.shadowRoot.querySelector('.respectfulReviewTip') + ); + done(); + }); + }); + + test('add 3 day delays once dismissed', done => { + // fake stub for storage + const respectfulGetStub = sinon.stub(); + const respectfulSetStub = sinon.stub(); + stub('gr-storage', { + getRespectfulTipVisibility() { return respectfulGetStub(); }, + setRespectfulTipVisibility(days) { return respectfulSetStub(days); }, + }); + respectfulGetStub.returns(null); + element = fixture('draft'); + // fake random + element.getRandomNum = () => 0; + element.comment = {__editing: true}; + flush(() => { + assert.isTrue(respectfulGetStub.called); + assert.isTrue(respectfulSetStub.called); + assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined); + assert.isTrue( + !!element.shadowRoot.querySelector('.respectfulReviewTip') + ); + + MockInteractions.tap(element.shadowRoot + .querySelector('.respectfulReviewTip .close')); + flushAsynchronousOperations(); + assert.isTrue(respectfulSetStub.lastCall.args[0] === 3); + done(); + }); + }); + + test('do not show tip when fall out of probability', done => { + // fake stub for storage + const respectfulGetStub = sinon.stub(); + const respectfulSetStub = sinon.stub(); + stub('gr-storage', { + getRespectfulTipVisibility() { return respectfulGetStub(); }, + setRespectfulTipVisibility() { return respectfulSetStub(); }, + }); + respectfulGetStub.returns(null); + element = fixture('draft'); + // fake random + element.getRandomNum = () => 3; + element.comment = {__editing: true}; + flush(() => { + assert.isTrue(respectfulGetStub.called); + assert.isFalse(respectfulSetStub.called); + assert.isFalse( + !!element.shadowRoot.querySelector('.respectfulReviewTip') + ); + done(); + }); + }); + + test('show tip when editing changed to true', done => { + // fake stub for storage + const respectfulGetStub = sinon.stub(); + const respectfulSetStub = sinon.stub(); + stub('gr-storage', { + getRespectfulTipVisibility() { return respectfulGetStub(); }, + setRespectfulTipVisibility() { return respectfulSetStub(); }, + }); + respectfulGetStub.returns(null); + element = fixture('draft'); + // fake random + element.getRandomNum = () => 0; + element.comment = {__editing: false}; + flush(() => { + assert.isFalse(respectfulGetStub.called); + assert.isFalse(respectfulSetStub.called); + assert.isFalse( + !!element.shadowRoot.querySelector('.respectfulReviewTip') + ); + + element.editing = true; flush(() => { assert.isTrue(respectfulGetStub.called); assert.isTrue(respectfulSetStub.called); @@ -1166,113 +1257,30 @@ done(); }); }); + }); - test('add 3 day delays once dismissed', done => { - // fake stub for storage - const respectfulGetStub = sinon.stub(); - const respectfulSetStub = sinon.stub(); - stub('gr-storage', { - getRespectfulTipVisibility() { return respectfulGetStub(); }, - setRespectfulTipVisibility(days) { return respectfulSetStub(days); }, - }); - respectfulGetStub.returns(null); - element = fixture('draft'); - // fake random - element.getRandomNum = () => 0; - element.comment = {__editing: true}; - flush(() => { - assert.isTrue(respectfulGetStub.called); - assert.isTrue(respectfulSetStub.called); - assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined); - assert.isTrue( - !!element.shadowRoot.querySelector('.respectfulReviewTip') - ); - - MockInteractions.tap(element.shadowRoot - .querySelector('.respectfulReviewTip .close')); - flushAsynchronousOperations(); - assert.isTrue(respectfulSetStub.lastCall.args[0] === 3); - done(); - }); + test('no tip when cached record', done => { + // fake stub for storage + const respectfulGetStub = sinon.stub(); + const respectfulSetStub = sinon.stub(); + stub('gr-storage', { + getRespectfulTipVisibility() { return respectfulGetStub(); }, + setRespectfulTipVisibility() { return respectfulSetStub(); }, }); - - test('do not show tip when fall out of probability', done => { - // fake stub for storage - const respectfulGetStub = sinon.stub(); - const respectfulSetStub = sinon.stub(); - stub('gr-storage', { - getRespectfulTipVisibility() { return respectfulGetStub(); }, - setRespectfulTipVisibility() { return respectfulSetStub(); }, - }); - respectfulGetStub.returns(null); - element = fixture('draft'); - // fake random - element.getRandomNum = () => 3; - element.comment = {__editing: true}; - flush(() => { - assert.isTrue(respectfulGetStub.called); - assert.isFalse(respectfulSetStub.called); - assert.isFalse( - !!element.shadowRoot.querySelector('.respectfulReviewTip') - ); - done(); - }); - }); - - test('show tip when editing changed to true', done => { - // fake stub for storage - const respectfulGetStub = sinon.stub(); - const respectfulSetStub = sinon.stub(); - stub('gr-storage', { - getRespectfulTipVisibility() { return respectfulGetStub(); }, - setRespectfulTipVisibility() { return respectfulSetStub(); }, - }); - respectfulGetStub.returns(null); - element = fixture('draft'); - // fake random - element.getRandomNum = () => 0; - element.comment = {__editing: false}; - flush(() => { - assert.isFalse(respectfulGetStub.called); - assert.isFalse(respectfulSetStub.called); - assert.isFalse( - !!element.shadowRoot.querySelector('.respectfulReviewTip') - ); - - element.editing = true; - flush(() => { - assert.isTrue(respectfulGetStub.called); - assert.isTrue(respectfulSetStub.called); - assert.isTrue( - !!element.shadowRoot.querySelector('.respectfulReviewTip') - ); - done(); - }); - }); - }); - - test('no tip when cached record', done => { - // fake stub for storage - const respectfulGetStub = sinon.stub(); - const respectfulSetStub = sinon.stub(); - stub('gr-storage', { - getRespectfulTipVisibility() { return respectfulGetStub(); }, - setRespectfulTipVisibility() { return respectfulSetStub(); }, - }); - respectfulGetStub.returns({}); - element = fixture('draft'); - // fake random - element.getRandomNum = () => 0; - element.comment = {__editing: true}; - flush(() => { - assert.isTrue(respectfulGetStub.called); - assert.isFalse(respectfulSetStub.called); - assert.isFalse( - !!element.shadowRoot.querySelector('.respectfulReviewTip') - ); - done(); - }); + respectfulGetStub.returns({}); + element = fixture('draft'); + // fake random + element.getRandomNum = () => 0; + element.comment = {__editing: true}; + flush(() => { + assert.isTrue(respectfulGetStub.called); + assert.isFalse(respectfulSetStub.called); + assert.isFalse( + !!element.shadowRoot.querySelector('.respectfulReviewTip') + ); + done(); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js index 8d50fe0..7db24c2 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js +++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js
@@ -14,54 +14,64 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; + +import '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-dialog/gr-dialog.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-confirm-delete-comment-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrConfirmDeleteCommentDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-confirm-delete-comment-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrConfirmDeleteCommentDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-confirm-delete-comment-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - message: String, - }; - } - - resetFocus() { - this.$.messageInput.textarea.focus(); - } - - _handleConfirmTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', {reason: this.message}, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } + static get properties() { + return { + message: String, + }; } - customElements.define(GrConfirmDeleteCommentDialog.is, - GrConfirmDeleteCommentDialog); -})(); + resetFocus() { + this.$.messageInput.textarea.focus(); + } + + _handleConfirmTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', {reason: this.message}, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } +} + +customElements.define(GrConfirmDeleteCommentDialog.is, + GrConfirmDeleteCommentDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js index e92bddb..f6caaa1 100644 --- a/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js +++ b/polygerrit-ui/app/elements/shared/gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../shared/gr-dialog/gr-dialog.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-confirm-delete-comment-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -51,22 +45,12 @@ width: 73ch; /* Add a char to account for the border. */ } </style> - <gr-dialog - confirm-label="Delete" - on-confirm="_handleConfirmTap" - on-cancel="_handleCancelTap"> + <gr-dialog confirm-label="Delete" on-confirm="_handleConfirmTap" on-cancel="_handleCancelTap"> <div class="header" slot="header">Delete Comment</div> <div class="main" slot="main"> <p>This is an admin function. Please only use in exceptional circumstances.</p> <label for="messageInput">Enter comment delete reason</label> - <iron-autogrow-textarea - id="messageInput" - class="message" - autocomplete="on" - placeholder="<Insert reasoning here>" - bind-value="{{message}}"></iron-autogrow-textarea> + <iron-autogrow-textarea id="messageInput" class="message" autocomplete="on" placeholder="<Insert reasoning here>" bind-value="{{message}}"></iron-autogrow-textarea> </div> </gr-dialog> - </template> - <script src="gr-confirm-delete-comment-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js index 9be6852..0f6168e 100644 --- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard.js
@@ -14,64 +14,74 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const COPY_TIMEOUT_MS = 1000; +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/shared-styles.js'; +import '../gr-button/gr-button.js'; +import '../gr-icons/gr-icons.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-copy-clipboard_html.js'; - /** @extends Polymer.Element */ - class GrCopyClipboard extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-copy-clipboard'; } +const COPY_TIMEOUT_MS = 1000; - static get properties() { - return { - text: String, - buttonTitle: String, - hasTooltip: { - type: Boolean, - value: false, - }, - hideInput: { - type: Boolean, - value: false, - }, - }; - } +/** @extends Polymer.Element */ +class GrCopyClipboard extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - focusOnCopy() { - this.$.button.focus(); - } + static get is() { return 'gr-copy-clipboard'; } - _computeInputClass(hideInput) { - return hideInput ? 'hideInput' : ''; - } - - _handleInputClick(e) { - e.preventDefault(); - Polymer.dom(e).rootTarget.select(); - } - - _copyToClipboard(e) { - e.preventDefault(); - e.stopPropagation(); - - if (this.hideInput) { - this.$.input.style.display = 'block'; - } - this.$.input.focus(); - this.$.input.select(); - document.execCommand('copy'); - if (this.hideInput) { - this.$.input.style.display = 'none'; - } - this.$.icon.icon = 'gr-icons:check'; - this.async( - () => this.$.icon.icon = 'gr-icons:content-copy', - COPY_TIMEOUT_MS); - } + static get properties() { + return { + text: String, + buttonTitle: String, + hasTooltip: { + type: Boolean, + value: false, + }, + hideInput: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrCopyClipboard.is, GrCopyClipboard); -})(); + focusOnCopy() { + this.$.button.focus(); + } + + _computeInputClass(hideInput) { + return hideInput ? 'hideInput' : ''; + } + + _handleInputClick(e) { + e.preventDefault(); + dom(e).rootTarget.select(); + } + + _copyToClipboard(e) { + e.preventDefault(); + e.stopPropagation(); + + if (this.hideInput) { + this.$.input.style.display = 'block'; + } + this.$.input.focus(); + this.$.input.select(); + document.execCommand('copy'); + if (this.hideInput) { + this.$.input.style.display = 'none'; + } + this.$.icon.icon = 'gr-icons:check'; + this.async( + () => this.$.icon.icon = 'gr-icons:content-copy', + COPY_TIMEOUT_MS); + } +} + +customElements.define(GrCopyClipboard.is, GrCopyClipboard);
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js index c344bf64..29becbb 100644 --- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_html.js
@@ -1,28 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> - -<dom-module id="gr-copy-clipboard"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .text { align-items: center; @@ -61,30 +55,11 @@ } </style> <div class="text"> - <iron-input - class="copyText" - type="text" - bind-value="[[text]]" - on-tap="_handleInputClick" - readonly> - <input - id="input" - is="iron-input" - class$="[[_computeInputClass(hideInput)]]" - type="text" - bind-value="[[text]]" - on-click="_handleInputClick" - readonly> + <iron-input class="copyText" type="text" bind-value="[[text]]" on-tap="_handleInputClick" readonly=""> + <input id="input" is="iron-input" class\$="[[_computeInputClass(hideInput)]]" type="text" bind-value="[[text]]" on-click="_handleInputClick" readonly=""> </iron-input> - <gr-button id="button" - link - has-tooltip="[[hasTooltip]]" - class="copyToClipboard" - title="[[buttonTitle]]" - on-click="_copyToClipboard"> + <gr-button id="button" link="" has-tooltip="[[hasTooltip]]" class="copyToClipboard" title="[[buttonTitle]]" on-click="_copyToClipboard"> <iron-icon id="icon" icon="gr-icons:content-copy"></iron-icon> </gr-button> </div> - </template> - <script src="gr-copy-clipboard.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html index 45ade85..a39e4c7 100644 --- a/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html +++ b/polygerrit-ui/app/elements/shared/gr-copy-clipboard/gr-copy-clipboard_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-copy-clipboard</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-copy-clipboard.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-copy-clipboard.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-copy-clipboard.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,73 +40,76 @@ </template> </test-fixture> -<script> - suite('gr-copy-clipboard tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-copy-clipboard.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-copy-clipboard tests', () => { + let element; + let sandbox; - setup(done => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.text = `git fetch http://gerrit@localhost:8080/a/test-project - refs/changes/05/5/1 && git checkout FETCH_HEAD`; - flushAsynchronousOperations(); - flush(done); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('copy to clipboard', () => { - const clipboardSpy = sandbox.spy(element, '_copyToClipboard'); - const copyBtn = element.shadowRoot - .querySelector('.copyToClipboard'); - MockInteractions.tap(copyBtn); - assert.isTrue(clipboardSpy.called); - }); - - test('focusOnCopy', () => { - element.focusOnCopy(); - assert.deepEqual(Polymer.dom(element.root).activeElement, - element.shadowRoot - .querySelector('.copyToClipboard')); - }); - - test('_handleInputClick', () => { - // iron-input as parent should never be hidden as copy won't work - // on nested hidden elements - const ironInputElement = element.shadowRoot.querySelector('iron-input'); - assert.notEqual(getComputedStyle(ironInputElement).display, 'none'); - - const inputElement = element.shadowRoot.querySelector('input'); - MockInteractions.tap(inputElement); - assert.equal(inputElement.selectionStart, 0); - assert.equal(inputElement.selectionEnd, element.text.length - 1); - }); - - test('hideInput', () => { - // iron-input as parent should never be hidden as copy won't work - // on nested hidden elements - const ironInputElement = element.shadowRoot.querySelector('iron-input'); - assert.notEqual(getComputedStyle(ironInputElement).display, 'none'); - - assert.notEqual(getComputedStyle(element.$.input).display, 'none'); - element.hideInput = true; - flushAsynchronousOperations(); - assert.equal(getComputedStyle(element.$.input).display, 'none'); - }); - - test('stop events propagation', () => { - const divParent = document.createElement('div'); - divParent.appendChild(element); - const clickStub = sinon.stub(); - divParent.addEventListener('click', clickStub); - element.stopPropagation = true; - const copyBtn = element.shadowRoot.querySelector('.copyToClipboard'); - MockInteractions.tap(copyBtn); - assert.isFalse(clickStub.called); - }); + setup(done => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.text = `git fetch http://gerrit@localhost:8080/a/test-project + refs/changes/05/5/1 && git checkout FETCH_HEAD`; + flushAsynchronousOperations(); + flush(done); }); + + teardown(() => { + sandbox.restore(); + }); + + test('copy to clipboard', () => { + const clipboardSpy = sandbox.spy(element, '_copyToClipboard'); + const copyBtn = element.shadowRoot + .querySelector('.copyToClipboard'); + MockInteractions.tap(copyBtn); + assert.isTrue(clipboardSpy.called); + }); + + test('focusOnCopy', () => { + element.focusOnCopy(); + assert.deepEqual(dom(element.root).activeElement, + element.shadowRoot + .querySelector('.copyToClipboard')); + }); + + test('_handleInputClick', () => { + // iron-input as parent should never be hidden as copy won't work + // on nested hidden elements + const ironInputElement = element.shadowRoot.querySelector('iron-input'); + assert.notEqual(getComputedStyle(ironInputElement).display, 'none'); + + const inputElement = element.shadowRoot.querySelector('input'); + MockInteractions.tap(inputElement); + assert.equal(inputElement.selectionStart, 0); + assert.equal(inputElement.selectionEnd, element.text.length - 1); + }); + + test('hideInput', () => { + // iron-input as parent should never be hidden as copy won't work + // on nested hidden elements + const ironInputElement = element.shadowRoot.querySelector('iron-input'); + assert.notEqual(getComputedStyle(ironInputElement).display, 'none'); + + assert.notEqual(getComputedStyle(element.$.input).display, 'none'); + element.hideInput = true; + flushAsynchronousOperations(); + assert.equal(getComputedStyle(element.$.input).display, 'none'); + }); + + test('stop events propagation', () => { + const divParent = document.createElement('div'); + divParent.appendChild(element); + const clickStub = sinon.stub(); + divParent.addEventListener('click', clickStub); + element.stopPropagation = true; + const copyBtn = element.shadowRoot.querySelector('.copyToClipboard'); + MockInteractions.tap(copyBtn); + assert.isFalse(clickStub.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js index b69c61aa..02c57e0 100644 --- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js +++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter.js
@@ -1,58 +1,56 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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. + */ +(function(window) { + 'use strict'; + const GrCountStringFormatter = window.GrCountStringFormatter || {}; -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 + /** + * Returns a count plus string that is pluralized when necessary. + * + * @param {number} count + * @param {string} noun + * @return {string} + */ + GrCountStringFormatter.computePluralString = function(count, noun) { + return this.computeString(count, noun) + (count > 1 ? 's' : ''); + }; -http://www.apache.org/licenses/LICENSE-2.0 + /** + * Returns a count plus string that is not pluralized. + * + * @param {number} count + * @param {string} noun + * @return {string} + */ + GrCountStringFormatter.computeString = function(count, noun) { + if (count === 0) { return ''; } + return count + ' ' + noun; + }; -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. ---> -<script> - (function(window) { - 'use strict'; - const GrCountStringFormatter = window.GrCountStringFormatter || {}; - - /** - * Returns a count plus string that is pluralized when necessary. - * - * @param {number} count - * @param {string} noun - * @return {string} - */ - GrCountStringFormatter.computePluralString = function(count, noun) { - return this.computeString(count, noun) + (count > 1 ? 's' : ''); - }; - - /** - * Returns a count plus string that is not pluralized. - * - * @param {number} count - * @param {string} noun - * @return {string} - */ - GrCountStringFormatter.computeString = function(count, noun) { - if (count === 0) { return ''; } - return count + ' ' + noun; - }; - - /** - * Returns a count plus arbitrary text. - * - * @param {number} count - * @param {string} text - * @return {string} - */ - GrCountStringFormatter.computeShortString = function(count, text) { - if (count === 0) { return ''; } - return count + text; - }; - window.GrCountStringFormatter = GrCountStringFormatter; - })(window); -</script> + /** + * Returns a count plus arbitrary text. + * + * @param {number} count + * @param {string} text + * @return {string} + */ + GrCountStringFormatter.computeShortString = function(count, text) { + if (count === 0) { return ''; } + return count + text; + }; + window.GrCountStringFormatter = GrCountStringFormatter; +})(window);
diff --git a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html index 64dff6a..9981d45 100644 --- a/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html +++ b/polygerrit-ui/app/elements/shared/gr-count-string-formatter/gr-count-string-formatter_test.html
@@ -19,41 +19,43 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-count-string-formatter</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-count-string-formatter.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-count-string-formatter.js"></script> -<script> - suite('gr-count-string-formatter tests', async () => { - await readyToTest(); - test('computeString', () => { - const noun = 'unresolved'; - assert.equal(GrCountStringFormatter.computeString(0, noun), ''); - assert.equal(GrCountStringFormatter.computeString(1, noun), - '1 unresolved'); - assert.equal(GrCountStringFormatter.computeString(2, noun), - '2 unresolved'); - }); - - test('computeShortString', () => { - const noun = 'c'; - assert.equal(GrCountStringFormatter.computeShortString(0, noun), ''); - assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c'); - assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c'); - }); - - test('computePluralString', () => { - const noun = 'comment'; - assert.equal(GrCountStringFormatter.computePluralString(0, noun), ''); - assert.equal(GrCountStringFormatter.computePluralString(1, noun), - '1 comment'); - assert.equal(GrCountStringFormatter.computePluralString(2, noun), - '2 comments'); - }); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-count-string-formatter.js'; +suite('gr-count-string-formatter tests', () => { + test('computeString', () => { + const noun = 'unresolved'; + assert.equal(GrCountStringFormatter.computeString(0, noun), ''); + assert.equal(GrCountStringFormatter.computeString(1, noun), + '1 unresolved'); + assert.equal(GrCountStringFormatter.computeString(2, noun), + '2 unresolved'); }); + + test('computeShortString', () => { + const noun = 'c'; + assert.equal(GrCountStringFormatter.computeShortString(0, noun), ''); + assert.equal(GrCountStringFormatter.computeShortString(1, noun), '1c'); + assert.equal(GrCountStringFormatter.computeShortString(2, noun), '2c'); + }); + + test('computePluralString', () => { + const noun = 'comment'; + assert.equal(GrCountStringFormatter.computePluralString(0, noun), ''); + assert.equal(GrCountStringFormatter.computePluralString(1, noun), + '1 comment'); + assert.equal(GrCountStringFormatter.computePluralString(2, noun), + '2 comments'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js index 4f98e88..f184b6c 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager.js
@@ -14,422 +14,426 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-cursor-manager_html.js'; - const ScrollBehavior = { - NEVER: 'never', - KEEP_VISIBLE: 'keep-visible', - }; +const ScrollBehavior = { + NEVER: 'never', + KEEP_VISIBLE: 'keep-visible', +}; - /** @extends Polymer.Element */ - class GrCursorManager extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-cursor-manager'; } +/** @extends Polymer.Element */ +class GrCursorManager extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - static get properties() { - return { - stops: { - type: Array, - value() { - return []; - }, - observer: '_updateIndex', + static get is() { return 'gr-cursor-manager'; } + + static get properties() { + return { + stops: { + type: Array, + value() { + return []; }, - /** - * @type {?Object} - */ - target: { - type: Object, - notify: true, - observer: '_scrollToTarget', - }, - /** - * The height of content intended to be included with the target. - * - * @type {?number} - */ - _targetHeight: Number, + observer: '_updateIndex', + }, + /** + * @type {?Object} + */ + target: { + type: Object, + notify: true, + observer: '_scrollToTarget', + }, + /** + * The height of content intended to be included with the target. + * + * @type {?number} + */ + _targetHeight: Number, - /** - * The index of the current target (if any). -1 otherwise. - */ - index: { - type: Number, - value: -1, - notify: true, - }, + /** + * The index of the current target (if any). -1 otherwise. + */ + index: { + type: Number, + value: -1, + notify: true, + }, - /** - * The class to apply to the current target. Use null for no class. - */ - cursorTargetClass: { - type: String, - value: null, - }, + /** + * The class to apply to the current target. Use null for no class. + */ + cursorTargetClass: { + type: String, + value: null, + }, - /** - * The scroll behavior for the cursor. Values are 'never' and - * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond - * the viewport. - * TODO (beckysiegel) figure out why it can be undefined - * - * @type {string|undefined} - */ - scrollBehavior: { - type: String, - value: ScrollBehavior.NEVER, - }, + /** + * The scroll behavior for the cursor. Values are 'never' and + * 'keep-visible'. 'keep-visible' will only scroll if the cursor is beyond + * the viewport. + * TODO (beckysiegel) figure out why it can be undefined + * + * @type {string|undefined} + */ + scrollBehavior: { + type: String, + value: ScrollBehavior.NEVER, + }, - /** - * When true, will call element.focus() during scrolling. - */ - focusOnMove: { - type: Boolean, - value: false, - }, + /** + * When true, will call element.focus() during scrolling. + */ + focusOnMove: { + type: Boolean, + value: false, + }, - /** - * The scrollTopMargin defines height of invisible area at the top - * of the page. If cursor locates inside this margin - it is - * not visible, because it is covered by some other element. - */ - scrollTopMargin: { - type: Number, - value: 0, - }, - }; + /** + * The scrollTopMargin defines height of invisible area at the top + * of the page. If cursor locates inside this margin - it is + * not visible, because it is covered by some other element. + */ + scrollTopMargin: { + type: Number, + value: 0, + }, + }; + } + + /** @override */ + detached() { + super.detached(); + this.unsetCursor(); + } + + /** + * Move the cursor forward. Clipped to the ends of the stop list. + * + * @param {!Function=} opt_condition Optional stop condition. If a condition + * is passed the cursor will continue to move in the specified direction + * until the condition is met. + * @param {!Function=} opt_getTargetHeight Optional function to calculate the + * height of the target's 'section'. The height of the target itself is + * sometimes different, used by the diff cursor. + * @param {boolean=} opt_clipToTop When none of the next indices match, move + * back to first instead of to last. + * @private + */ + + next(opt_condition, opt_getTargetHeight, opt_clipToTop) { + this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop); + } + + previous(opt_condition) { + this._moveCursor(-1, opt_condition); + } + + /** + * Move the cursor to the row which is the closest to the viewport center + * in vertical direction. + * The method uses IntersectionObservers API. If browser + * doesn't support this API the method does nothing + * + * @param {!Function=} opt_condition Optional condition. If a condition + * is passed only stops which meet conditions are taken into account. + */ + moveToVisibleArea(opt_condition) { + if (!this.stops || !this._isIntersectionObserverSupported()) { + return; } + const filteredStops = opt_condition ? this.stops.filter(opt_condition) + : this.stops; + const dims = this._getWindowDims(); + const windowCenter = + Math.round((dims.innerHeight + this.scrollTopMargin) / 2); - /** @override */ - detached() { - super.detached(); - this.unsetCursor(); - } + let closestToTheCenter = null; + let minDistanceToCenter = null; + let unobservedCount = filteredStops.length; - /** - * Move the cursor forward. Clipped to the ends of the stop list. - * - * @param {!Function=} opt_condition Optional stop condition. If a condition - * is passed the cursor will continue to move in the specified direction - * until the condition is met. - * @param {!Function=} opt_getTargetHeight Optional function to calculate the - * height of the target's 'section'. The height of the target itself is - * sometimes different, used by the diff cursor. - * @param {boolean=} opt_clipToTop When none of the next indices match, move - * back to first instead of to last. - * @private - */ + const observer = new IntersectionObserver(entries => { + // This callback is called for the first time immediately. + // Typically it gets all observed stops at once, but + // sometimes can get them in several chunks. + entries.forEach(entry => { + observer.unobserve(entry.target); - next(opt_condition, opt_getTargetHeight, opt_clipToTop) { - this._moveCursor(1, opt_condition, opt_getTargetHeight, opt_clipToTop); - } - - previous(opt_condition) { - this._moveCursor(-1, opt_condition); - } - - /** - * Move the cursor to the row which is the closest to the viewport center - * in vertical direction. - * The method uses IntersectionObservers API. If browser - * doesn't support this API the method does nothing - * - * @param {!Function=} opt_condition Optional condition. If a condition - * is passed only stops which meet conditions are taken into account. - */ - moveToVisibleArea(opt_condition) { - if (!this.stops || !this._isIntersectionObserverSupported()) { - return; - } - const filteredStops = opt_condition ? this.stops.filter(opt_condition) - : this.stops; - const dims = this._getWindowDims(); - const windowCenter = - Math.round((dims.innerHeight + this.scrollTopMargin) / 2); - - let closestToTheCenter = null; - let minDistanceToCenter = null; - let unobservedCount = filteredStops.length; - - const observer = new IntersectionObserver(entries => { - // This callback is called for the first time immediately. - // Typically it gets all observed stops at once, but - // sometimes can get them in several chunks. - entries.forEach(entry => { - observer.unobserve(entry.target); - - // In Edge it is recommended to use intersectionRatio instead of - // isIntersecting. - const isInsideViewport = - entry.isIntersecting || entry.intersectionRatio > 0; - if (!isInsideViewport) { - return; - } - const center = entry.boundingClientRect.top + Math.round( - entry.boundingClientRect.height / 2); - const distanceToWindowCenter = Math.abs(center - windowCenter); - if (minDistanceToCenter === null || - distanceToWindowCenter < minDistanceToCenter) { - closestToTheCenter = entry.target; - minDistanceToCenter = distanceToWindowCenter; - } - }); - unobservedCount -= entries.length; - if (unobservedCount == 0 && closestToTheCenter) { - // set cursor when all stops were observed. - // In most cases the target is visible, so scroll is not - // needed. But in rare cases the target can become invisible - // at this point (due to some scrolling in window). - // To avoid jumps set noScroll options. - this.setCursor(closestToTheCenter, true); - } - }); - filteredStops.forEach(stop => { - observer.observe(stop); - }); - } - - _isIntersectionObserverSupported() { - // The copy of this method exists in gr-app-element.js under the - // name _isCursorManagerSupportMoveToVisibleLine - // If you update this method, you must update gr-app-element.js - // as well. - return 'IntersectionObserver' in window; - } - - /** - * Set the cursor to an arbitrary element. - * - * @param {!HTMLElement} element - * @param {boolean=} opt_noScroll prevent any potential scrolling in response - * setting the cursor. - */ - setCursor(element, opt_noScroll) { - let behavior; - if (opt_noScroll) { - behavior = this.scrollBehavior; - this.scrollBehavior = ScrollBehavior.NEVER; - } - - this.unsetCursor(); - this.target = element; - this._updateIndex(); - this._decorateTarget(); - - if (opt_noScroll) { this.scrollBehavior = behavior; } - } - - unsetCursor() { - this._unDecorateTarget(); - this.index = -1; - this.target = null; - this._targetHeight = null; - } - - isAtStart() { - return this.index === 0; - } - - isAtEnd() { - return this.index === this.stops.length - 1; - } - - moveToStart() { - if (this.stops.length) { - this.setCursor(this.stops[0]); - } - } - - moveToEnd() { - if (this.stops.length) { - this.setCursor(this.stops[this.stops.length - 1]); - } - } - - setCursorAtIndex(index, opt_noScroll) { - this.setCursor(this.stops[index], opt_noScroll); - } - - /** - * Move the cursor forward or backward by delta. Clipped to the beginning or - * end of stop list. - * - * @param {number} delta either -1 or 1. - * @param {!Function=} opt_condition Optional stop condition. If a condition - * is passed the cursor will continue to move in the specified direction - * until the condition is met. - * @param {!Function=} opt_getTargetHeight Optional function to calculate the - * height of the target's 'section'. The height of the target itself is - * sometimes different, used by the diff cursor. - * @param {boolean=} opt_clipToTop When none of the next indices match, move - * back to first instead of to last. - * @private - */ - _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) { - if (!this.stops.length) { - this.unsetCursor(); - return; - } - - this._unDecorateTarget(); - - const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop); - - let newTarget = null; - if (newIndex !== -1) { - newTarget = this.stops[newIndex]; - } - - this.index = newIndex; - this.target = newTarget; - - if (!this.target) { return; } - - if (opt_getTargetHeight) { - this._targetHeight = opt_getTargetHeight(newTarget); - } else { - this._targetHeight = newTarget.scrollHeight; - } - - if (this.focusOnMove) { this.target.focus(); } - - this._decorateTarget(); - } - - _decorateTarget() { - if (this.target && this.cursorTargetClass) { - this.target.classList.add(this.cursorTargetClass); - } - } - - _unDecorateTarget() { - if (this.target && this.cursorTargetClass) { - this.target.classList.remove(this.cursorTargetClass); - } - } - - /** - * Get the next stop index indicated by the delta direction. - * - * @param {number} delta either -1 or 1. - * @param {!Function=} opt_condition Optional stop condition. - * @param {boolean=} opt_clipToTop When none of the next indices match, move - * back to first instead of to last. - * @return {number} the new index. - * @private - */ - _getNextindex(delta, opt_condition, opt_clipToTop) { - if (!this.stops.length || this.index === -1) { - return -1; - } - - let newIndex = this.index; - do { - newIndex = newIndex + delta; - } while (newIndex > 0 && - newIndex < this.stops.length - 1 && - opt_condition && !opt_condition(this.stops[newIndex])); - - newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex)); - - // If we failed to satisfy the condition: - if (opt_condition && !opt_condition(this.stops[newIndex])) { - if (delta < 0 || opt_clipToTop) { - return 0; - } else if (delta > 0) { - return this.stops.length - 1; - } - return this.index; - } - - return newIndex; - } - - _updateIndex() { - if (!this.target) { - this.index = -1; - return; - } - - const newIndex = Array.prototype.indexOf.call(this.stops, this.target); - if (newIndex === -1) { - this.unsetCursor(); - } else { - this.index = newIndex; - } - } - - /** - * Calculate where the element is relative to the window. - * - * @param {!Object} target Target to scroll to. - * @return {number} Distance to top of the target. - */ - _getTop(target) { - let top = target.offsetTop; - for (let offsetParent = target.offsetParent; - offsetParent; - offsetParent = offsetParent.offsetParent) { - top += offsetParent.offsetTop; - } - return top; - } - - /** - * @return {boolean} - */ - _targetIsVisible(top) { - const dims = this._getWindowDims(); - return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE && - top > (dims.pageYOffset + this.scrollTopMargin) && - top < dims.pageYOffset + dims.innerHeight; - } - - _calculateScrollToValue(top, target) { - const dims = this._getWindowDims(); - return top + this.scrollTopMargin - (dims.innerHeight / 3) + - (target.offsetHeight / 2); - } - - _scrollToTarget() { - if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) { - return; - } - - const dims = this._getWindowDims(); - const top = this._getTop(this.target); - const bottomIsVisible = this._targetHeight ? - this._targetIsVisible(top + this._targetHeight) : true; - const scrollToValue = this._calculateScrollToValue(top, this.target); - - if (this._targetIsVisible(top)) { - // Don't scroll if either the bottom is visible or if the position that - // would get scrolled to is higher up than the current position. this - // woulld cause less of the target content to be displayed than is - // already. - if (bottomIsVisible || scrollToValue < dims.scrollY) { + // In Edge it is recommended to use intersectionRatio instead of + // isIntersecting. + const isInsideViewport = + entry.isIntersecting || entry.intersectionRatio > 0; + if (!isInsideViewport) { return; } + const center = entry.boundingClientRect.top + Math.round( + entry.boundingClientRect.height / 2); + const distanceToWindowCenter = Math.abs(center - windowCenter); + if (minDistanceToCenter === null || + distanceToWindowCenter < minDistanceToCenter) { + closestToTheCenter = entry.target; + minDistanceToCenter = distanceToWindowCenter; + } + }); + unobservedCount -= entries.length; + if (unobservedCount == 0 && closestToTheCenter) { + // set cursor when all stops were observed. + // In most cases the target is visible, so scroll is not + // needed. But in rare cases the target can become invisible + // at this point (due to some scrolling in window). + // To avoid jumps set noScroll options. + this.setCursor(closestToTheCenter, true); } + }); + filteredStops.forEach(stop => { + observer.observe(stop); + }); + } - // Scroll the element to the middle of the window. Dividing by a third - // instead of half the inner height feels a bit better otherwise the - // element appears to be below the center of the window even when it - // isn't. - window.scrollTo(dims.scrollX, scrollToValue); + _isIntersectionObserverSupported() { + // The copy of this method exists in gr-app-element.js under the + // name _isCursorManagerSupportMoveToVisibleLine + // If you update this method, you must update gr-app-element.js + // as well. + return 'IntersectionObserver' in window; + } + + /** + * Set the cursor to an arbitrary element. + * + * @param {!HTMLElement} element + * @param {boolean=} opt_noScroll prevent any potential scrolling in response + * setting the cursor. + */ + setCursor(element, opt_noScroll) { + let behavior; + if (opt_noScroll) { + behavior = this.scrollBehavior; + this.scrollBehavior = ScrollBehavior.NEVER; } - _getWindowDims() { - return { - scrollX: window.scrollX, - scrollY: window.scrollY, - innerHeight: window.innerHeight, - pageYOffset: window.pageYOffset, - }; + this.unsetCursor(); + this.target = element; + this._updateIndex(); + this._decorateTarget(); + + if (opt_noScroll) { this.scrollBehavior = behavior; } + } + + unsetCursor() { + this._unDecorateTarget(); + this.index = -1; + this.target = null; + this._targetHeight = null; + } + + isAtStart() { + return this.index === 0; + } + + isAtEnd() { + return this.index === this.stops.length - 1; + } + + moveToStart() { + if (this.stops.length) { + this.setCursor(this.stops[0]); } } - customElements.define(GrCursorManager.is, GrCursorManager); -})(); + moveToEnd() { + if (this.stops.length) { + this.setCursor(this.stops[this.stops.length - 1]); + } + } + + setCursorAtIndex(index, opt_noScroll) { + this.setCursor(this.stops[index], opt_noScroll); + } + + /** + * Move the cursor forward or backward by delta. Clipped to the beginning or + * end of stop list. + * + * @param {number} delta either -1 or 1. + * @param {!Function=} opt_condition Optional stop condition. If a condition + * is passed the cursor will continue to move in the specified direction + * until the condition is met. + * @param {!Function=} opt_getTargetHeight Optional function to calculate the + * height of the target's 'section'. The height of the target itself is + * sometimes different, used by the diff cursor. + * @param {boolean=} opt_clipToTop When none of the next indices match, move + * back to first instead of to last. + * @private + */ + _moveCursor(delta, opt_condition, opt_getTargetHeight, opt_clipToTop) { + if (!this.stops.length) { + this.unsetCursor(); + return; + } + + this._unDecorateTarget(); + + const newIndex = this._getNextindex(delta, opt_condition, opt_clipToTop); + + let newTarget = null; + if (newIndex !== -1) { + newTarget = this.stops[newIndex]; + } + + this.index = newIndex; + this.target = newTarget; + + if (!this.target) { return; } + + if (opt_getTargetHeight) { + this._targetHeight = opt_getTargetHeight(newTarget); + } else { + this._targetHeight = newTarget.scrollHeight; + } + + if (this.focusOnMove) { this.target.focus(); } + + this._decorateTarget(); + } + + _decorateTarget() { + if (this.target && this.cursorTargetClass) { + this.target.classList.add(this.cursorTargetClass); + } + } + + _unDecorateTarget() { + if (this.target && this.cursorTargetClass) { + this.target.classList.remove(this.cursorTargetClass); + } + } + + /** + * Get the next stop index indicated by the delta direction. + * + * @param {number} delta either -1 or 1. + * @param {!Function=} opt_condition Optional stop condition. + * @param {boolean=} opt_clipToTop When none of the next indices match, move + * back to first instead of to last. + * @return {number} the new index. + * @private + */ + _getNextindex(delta, opt_condition, opt_clipToTop) { + if (!this.stops.length || this.index === -1) { + return -1; + } + + let newIndex = this.index; + do { + newIndex = newIndex + delta; + } while (newIndex > 0 && + newIndex < this.stops.length - 1 && + opt_condition && !opt_condition(this.stops[newIndex])); + + newIndex = Math.max(0, Math.min(this.stops.length - 1, newIndex)); + + // If we failed to satisfy the condition: + if (opt_condition && !opt_condition(this.stops[newIndex])) { + if (delta < 0 || opt_clipToTop) { + return 0; + } else if (delta > 0) { + return this.stops.length - 1; + } + return this.index; + } + + return newIndex; + } + + _updateIndex() { + if (!this.target) { + this.index = -1; + return; + } + + const newIndex = Array.prototype.indexOf.call(this.stops, this.target); + if (newIndex === -1) { + this.unsetCursor(); + } else { + this.index = newIndex; + } + } + + /** + * Calculate where the element is relative to the window. + * + * @param {!Object} target Target to scroll to. + * @return {number} Distance to top of the target. + */ + _getTop(target) { + let top = target.offsetTop; + for (let offsetParent = target.offsetParent; + offsetParent; + offsetParent = offsetParent.offsetParent) { + top += offsetParent.offsetTop; + } + return top; + } + + /** + * @return {boolean} + */ + _targetIsVisible(top) { + const dims = this._getWindowDims(); + return this.scrollBehavior === ScrollBehavior.KEEP_VISIBLE && + top > (dims.pageYOffset + this.scrollTopMargin) && + top < dims.pageYOffset + dims.innerHeight; + } + + _calculateScrollToValue(top, target) { + const dims = this._getWindowDims(); + return top + this.scrollTopMargin - (dims.innerHeight / 3) + + (target.offsetHeight / 2); + } + + _scrollToTarget() { + if (!this.target || this.scrollBehavior === ScrollBehavior.NEVER) { + return; + } + + const dims = this._getWindowDims(); + const top = this._getTop(this.target); + const bottomIsVisible = this._targetHeight ? + this._targetIsVisible(top + this._targetHeight) : true; + const scrollToValue = this._calculateScrollToValue(top, this.target); + + if (this._targetIsVisible(top)) { + // Don't scroll if either the bottom is visible or if the position that + // would get scrolled to is higher up than the current position. this + // woulld cause less of the target content to be displayed than is + // already. + if (bottomIsVisible || scrollToValue < dims.scrollY) { + return; + } + } + + // Scroll the element to the middle of the window. Dividing by a third + // instead of half the inner height feels a bit better otherwise the + // element appears to be below the center of the window even when it + // isn't. + window.scrollTo(dims.scrollX, scrollToValue); + } + + _getWindowDims() { + return { + scrollX: window.scrollX, + scrollY: window.scrollY, + innerHeight: window.innerHeight, + pageYOffset: window.pageYOffset, + }; + } +} + +customElements.define(GrCursorManager.is, GrCursorManager);
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js index 94d7aaa..29757e5 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_html.js
@@ -1,23 +1,21 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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 +export const htmlTemplate = html` -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-cursor-manager"> - <template></template> - <script src="gr-cursor-manager.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html index e7d5d74..5264464 100644 --- a/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html +++ b/polygerrit-ui/app/elements/shared/gr-cursor-manager/gr-cursor-manager_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-cursor-manager</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-cursor-manager.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-cursor-manager.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-cursor-manager.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -41,243 +46,245 @@ </template> </test-fixture> -<script> - suite('gr-cursor-manager tests', async () => { - await readyToTest(); - let sandbox; - let element; - let list; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-cursor-manager.js'; +suite('gr-cursor-manager tests', () => { + let sandbox; + let element; + let list; + setup(() => { + sandbox = sinon.sandbox.create(); + const fixtureElements = fixture('basic'); + element = fixtureElements[0]; + list = fixtureElements[1]; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('core cursor functionality', () => { + // The element is initialized into the proper state. + assert.isArray(element.stops); + assert.equal(element.stops.length, 0); + assert.equal(element.index, -1); + assert.isNotOk(element.target); + + // Initialize the cursor with its stops. + element.stops = list.querySelectorAll('li'); + + // It should have the stops but it should not be targeting any of them. + assert.isNotNull(element.stops); + assert.equal(element.stops.length, 4); + assert.equal(element.index, -1); + assert.isNotOk(element.target); + + // Select the third stop. + element.setCursor(list.children[2]); + + // It should update its internal state and update the element's class. + assert.equal(element.index, 2); + assert.equal(element.target, list.children[2]); + assert.isTrue(list.children[2].classList.contains('targeted')); + assert.isFalse(element.isAtStart()); + assert.isFalse(element.isAtEnd()); + + // Progress the cursor. + element.next(); + + // Confirm that the next stop is selected and that the previous stop is + // unselected. + assert.equal(element.index, 3); + assert.equal(element.target, list.children[3]); + assert.isTrue(element.isAtEnd()); + assert.isFalse(list.children[2].classList.contains('targeted')); + assert.isTrue(list.children[3].classList.contains('targeted')); + + // Progress the cursor. + element.next(); + + // We should still be at the end. + assert.equal(element.index, 3); + assert.equal(element.target, list.children[3]); + assert.isTrue(element.isAtEnd()); + + // Wind the cursor all the way back to the first stop. + element.previous(); + element.previous(); + element.previous(); + + // The element state should reflect the end of the list. + assert.equal(element.index, 0); + assert.equal(element.target, list.children[0]); + assert.isTrue(element.isAtStart()); + assert.isTrue(list.children[0].classList.contains('targeted')); + + const newLi = document.createElement('li'); + newLi.textContent = 'Z'; + list.insertBefore(newLi, list.children[0]); + element.stops = list.querySelectorAll('li'); + + assert.equal(element.index, 1); + + // De-select all targets. + element.unsetCursor(); + + // There should now be no cursor target. + assert.isFalse(list.children[1].classList.contains('targeted')); + assert.isNotOk(element.target); + assert.equal(element.index, -1); + }); + + test('_moveCursor', () => { + // Initialize the cursor with its stops. + element.stops = list.querySelectorAll('li'); + // Select the first stop. + element.setCursor(list.children[0]); + const getTargetHeight = sinon.stub(); + + // Move the cursor without an optional get target height function. + element._moveCursor(1); + assert.isFalse(getTargetHeight.called); + + // Move the cursor with an optional get target height function. + element._moveCursor(1, null, getTargetHeight); + assert.isTrue(getTargetHeight.called); + }); + + test('_moveCursor from -1 does not check height', () => { + element.stops = list.querySelectorAll('li'); + const getTargetHeight = sinon.stub(); + element._moveCursor(1, () => false, getTargetHeight); + assert.isFalse(getTargetHeight.called); + }); + + test('opt_noScroll', () => { + sandbox.stub(element, '_targetIsVisible', () => false); + const scrollStub = sandbox.stub(window, 'scrollTo'); + element.stops = list.querySelectorAll('li'); + element.scrollBehavior = 'keep-visible'; + + element.setCursorAtIndex(1, true); + assert.isFalse(scrollStub.called); + + element.setCursorAtIndex(2); + assert.isTrue(scrollStub.called); + }); + + test('_getNextindex', () => { + const isLetterB = function(row) { + return row.textContent === 'B'; + }; + element.stops = list.querySelectorAll('li'); + // Start cursor at the first stop. + element.setCursor(list.children[0]); + + // Move forward to meet the next condition. + assert.equal(element._getNextindex(1, isLetterB), 1); + element.index = 1; + + // Nothing else meets the condition, should be at last stop. + assert.equal(element._getNextindex(1, isLetterB), 3); + element.index = 3; + + // Should stay at last stop if try to proceed. + assert.equal(element._getNextindex(1, isLetterB), 3); + + // Go back to the previous condition met. Should be back at. + // stop 1. + assert.equal(element._getNextindex(-1, isLetterB), 1); + element.index = 1; + + // Go back. No more meet the condition. Should be at stop 0. + assert.equal(element._getNextindex(-1, isLetterB), 0); + }); + + test('focusOnMove prop', () => { + const listEls = list.querySelectorAll('li'); + for (let i = 0; i < listEls.length; i++) { + sandbox.spy(listEls[i], 'focus'); + } + element.stops = listEls; + element.setCursor(list.children[0]); + + element.focusOnMove = false; + element.next(); + assert.isFalse(element.target.focus.called); + + element.focusOnMove = true; + element.next(); + assert.isTrue(element.target.focus.called); + }); + + suite('_scrollToTarget', () => { + let scrollStub; setup(() => { - sandbox = sinon.sandbox.create(); - const fixtureElements = fixture('basic'); - element = fixtureElements[0]; - list = fixtureElements[1]; - }); - - teardown(() => { - sandbox.restore(); - }); - - test('core cursor functionality', () => { - // The element is initialized into the proper state. - assert.isArray(element.stops); - assert.equal(element.stops.length, 0); - assert.equal(element.index, -1); - assert.isNotOk(element.target); - - // Initialize the cursor with its stops. - element.stops = list.querySelectorAll('li'); - - // It should have the stops but it should not be targeting any of them. - assert.isNotNull(element.stops); - assert.equal(element.stops.length, 4); - assert.equal(element.index, -1); - assert.isNotOk(element.target); - - // Select the third stop. - element.setCursor(list.children[2]); - - // It should update its internal state and update the element's class. - assert.equal(element.index, 2); - assert.equal(element.target, list.children[2]); - assert.isTrue(list.children[2].classList.contains('targeted')); - assert.isFalse(element.isAtStart()); - assert.isFalse(element.isAtEnd()); - - // Progress the cursor. - element.next(); - - // Confirm that the next stop is selected and that the previous stop is - // unselected. - assert.equal(element.index, 3); - assert.equal(element.target, list.children[3]); - assert.isTrue(element.isAtEnd()); - assert.isFalse(list.children[2].classList.contains('targeted')); - assert.isTrue(list.children[3].classList.contains('targeted')); - - // Progress the cursor. - element.next(); - - // We should still be at the end. - assert.equal(element.index, 3); - assert.equal(element.target, list.children[3]); - assert.isTrue(element.isAtEnd()); - - // Wind the cursor all the way back to the first stop. - element.previous(); - element.previous(); - element.previous(); - - // The element state should reflect the end of the list. - assert.equal(element.index, 0); - assert.equal(element.target, list.children[0]); - assert.isTrue(element.isAtStart()); - assert.isTrue(list.children[0].classList.contains('targeted')); - - const newLi = document.createElement('li'); - newLi.textContent = 'Z'; - list.insertBefore(newLi, list.children[0]); - element.stops = list.querySelectorAll('li'); - - assert.equal(element.index, 1); - - // De-select all targets. - element.unsetCursor(); - - // There should now be no cursor target. - assert.isFalse(list.children[1].classList.contains('targeted')); - assert.isNotOk(element.target); - assert.equal(element.index, -1); - }); - - test('_moveCursor', () => { - // Initialize the cursor with its stops. - element.stops = list.querySelectorAll('li'); - // Select the first stop. - element.setCursor(list.children[0]); - const getTargetHeight = sinon.stub(); - - // Move the cursor without an optional get target height function. - element._moveCursor(1); - assert.isFalse(getTargetHeight.called); - - // Move the cursor with an optional get target height function. - element._moveCursor(1, null, getTargetHeight); - assert.isTrue(getTargetHeight.called); - }); - - test('_moveCursor from -1 does not check height', () => { - element.stops = list.querySelectorAll('li'); - const getTargetHeight = sinon.stub(); - element._moveCursor(1, () => false, getTargetHeight); - assert.isFalse(getTargetHeight.called); - }); - - test('opt_noScroll', () => { - sandbox.stub(element, '_targetIsVisible', () => false); - const scrollStub = sandbox.stub(window, 'scrollTo'); element.stops = list.querySelectorAll('li'); element.scrollBehavior = 'keep-visible'; - element.setCursorAtIndex(1, true); - assert.isFalse(scrollStub.called); + // There is a target which has a targetNext + element.setCursor(list.children[0]); + element._moveCursor(1); + scrollStub = sandbox.stub(window, 'scrollTo'); + window.innerHeight = 60; + }); - element.setCursorAtIndex(2); + test('Called when top and bottom not visible', () => { + sandbox.stub(element, '_targetIsVisible').returns(false); + element._scrollToTarget(); assert.isTrue(scrollStub.called); }); - test('_getNextindex', () => { - const isLetterB = function(row) { - return row.textContent === 'B'; - }; - element.stops = list.querySelectorAll('li'); - // Start cursor at the first stop. - element.setCursor(list.children[0]); - - // Move forward to meet the next condition. - assert.equal(element._getNextindex(1, isLetterB), 1); - element.index = 1; - - // Nothing else meets the condition, should be at last stop. - assert.equal(element._getNextindex(1, isLetterB), 3); - element.index = 3; - - // Should stay at last stop if try to proceed. - assert.equal(element._getNextindex(1, isLetterB), 3); - - // Go back to the previous condition met. Should be back at. - // stop 1. - assert.equal(element._getNextindex(-1, isLetterB), 1); - element.index = 1; - - // Go back. No more meet the condition. Should be at stop 0. - assert.equal(element._getNextindex(-1, isLetterB), 0); + test('Not called when top and bottom visible', () => { + sandbox.stub(element, '_targetIsVisible').returns(true); + element._scrollToTarget(); + assert.isFalse(scrollStub.called); }); - test('focusOnMove prop', () => { - const listEls = list.querySelectorAll('li'); - for (let i = 0; i < listEls.length; i++) { - sandbox.spy(listEls[i], 'focus'); - } - element.stops = listEls; - element.setCursor(list.children[0]); - - element.focusOnMove = false; - element.next(); - assert.isFalse(element.target.focus.called); - - element.focusOnMove = true; - element.next(); - assert.isTrue(element.target.focus.called); + test('Called when top is visible, bottom is not, scroll is lower', () => { + const visibleStub = sandbox.stub(element, '_targetIsVisible', + () => visibleStub.callCount === 2); + sandbox.stub(element, '_getWindowDims').returns({ + scrollX: 123, + scrollY: 15, + innerHeight: 1000, + pageYOffset: 0, + }); + sandbox.stub(element, '_calculateScrollToValue').returns(20); + element._scrollToTarget(); + assert.isTrue(scrollStub.called); + assert.isTrue(scrollStub.calledWithExactly(123, 20)); + assert.equal(visibleStub.callCount, 2); }); - suite('_scrollToTarget', () => { - let scrollStub; - setup(() => { - element.stops = list.querySelectorAll('li'); - element.scrollBehavior = 'keep-visible'; - - // There is a target which has a targetNext - element.setCursor(list.children[0]); - element._moveCursor(1); - scrollStub = sandbox.stub(window, 'scrollTo'); - window.innerHeight = 60; + test('Called when top is visible, bottom not, scroll is higher', () => { + const visibleStub = sandbox.stub(element, '_targetIsVisible', + () => visibleStub.callCount === 2); + sandbox.stub(element, '_getWindowDims').returns({ + scrollX: 123, + scrollY: 25, + innerHeight: 1000, + pageYOffset: 0, }); + sandbox.stub(element, '_calculateScrollToValue').returns(20); + element._scrollToTarget(); + assert.isFalse(scrollStub.called); + assert.equal(visibleStub.callCount, 2); + }); - test('Called when top and bottom not visible', () => { - sandbox.stub(element, '_targetIsVisible').returns(false); - element._scrollToTarget(); - assert.isTrue(scrollStub.called); + test('_calculateScrollToValue', () => { + sandbox.stub(element, '_getWindowDims').returns({ + scrollX: 123, + scrollY: 25, + innerHeight: 300, + pageYOffset: 0, }); - - test('Not called when top and bottom visible', () => { - sandbox.stub(element, '_targetIsVisible').returns(true); - element._scrollToTarget(); - assert.isFalse(scrollStub.called); - }); - - test('Called when top is visible, bottom is not, scroll is lower', () => { - const visibleStub = sandbox.stub(element, '_targetIsVisible', - () => visibleStub.callCount === 2); - sandbox.stub(element, '_getWindowDims').returns({ - scrollX: 123, - scrollY: 15, - innerHeight: 1000, - pageYOffset: 0, - }); - sandbox.stub(element, '_calculateScrollToValue').returns(20); - element._scrollToTarget(); - assert.isTrue(scrollStub.called); - assert.isTrue(scrollStub.calledWithExactly(123, 20)); - assert.equal(visibleStub.callCount, 2); - }); - - test('Called when top is visible, bottom not, scroll is higher', () => { - const visibleStub = sandbox.stub(element, '_targetIsVisible', - () => visibleStub.callCount === 2); - sandbox.stub(element, '_getWindowDims').returns({ - scrollX: 123, - scrollY: 25, - innerHeight: 1000, - pageYOffset: 0, - }); - sandbox.stub(element, '_calculateScrollToValue').returns(20); - element._scrollToTarget(); - assert.isFalse(scrollStub.called); - assert.equal(visibleStub.callCount, 2); - }); - - test('_calculateScrollToValue', () => { - sandbox.stub(element, '_getWindowDims').returns({ - scrollX: 123, - scrollY: 25, - innerHeight: 300, - pageYOffset: 0, - }); - assert.equal(element._calculateScrollToValue(1000, {offsetHeight: 10}), - 905); - }); + assert.equal(element._calculateScrollToValue(1000, {offsetHeight: 10}), + 905); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js index 7be041b..c46bb17 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,243 +14,253 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const Duration = { - HOUR: 1000 * 60 * 60, - DAY: 1000 * 60 * 60 * 24, - }; +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import '../../../scripts/util.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-date-formatter_html.js'; - const TimeFormats = { - TIME_12: 'h:mm A', // 2:14 PM - TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM - TIME_24: 'HH:mm', // 14:14 - TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00 - }; +const Duration = { + HOUR: 1000 * 60 * 60, + DAY: 1000 * 60 * 60 * 24, +}; - const DateFormats = { - STD: { - short: 'MMM DD', // Aug 29 - full: 'MMM DD, YYYY', // Aug 29, 1997 - }, - US: { - short: 'MM/DD', // 08/29 - full: 'MM/DD/YY', // 08/29/97 - }, - ISO: { - short: 'MM-DD', // 08-29 - full: 'YYYY-MM-DD', // 1997-08-29 - }, - EURO: { - short: 'DD. MMM', // 29. Aug - full: 'DD.MM.YYYY', // 29.08.1997 - }, - UK: { - short: 'DD/MM', // 29/08 - full: 'DD/MM/YYYY', // 29/08/1997 - }, - }; +const TimeFormats = { + TIME_12: 'h:mm A', // 2:14 PM + TIME_12_WITH_SEC: 'h:mm:ss A', // 2:14:00 PM + TIME_24: 'HH:mm', // 14:14 + TIME_24_WITH_SEC: 'HH:mm:ss', // 14:14:00 +}; - /** - * @appliesMixin Gerrit.TooltipMixin - * @extends Polymer.Element - */ - class GrDateFormatter extends Polymer.mixinBehaviors( [ - Gerrit.TooltipBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-date-formatter'; } +const DateFormats = { + STD: { + short: 'MMM DD', // Aug 29 + full: 'MMM DD, YYYY', // Aug 29, 1997 + }, + US: { + short: 'MM/DD', // 08/29 + full: 'MM/DD/YY', // 08/29/97 + }, + ISO: { + short: 'MM-DD', // 08-29 + full: 'YYYY-MM-DD', // 1997-08-29 + }, + EURO: { + short: 'DD. MMM', // 29. Aug + full: 'DD.MM.YYYY', // 29.08.1997 + }, + UK: { + short: 'DD/MM', // 29/08 + full: 'DD/MM/YYYY', // 29/08/1997 + }, +}; - static get properties() { - return { - dateStr: { - type: String, - value: null, - notify: true, - }, - showDateAndTime: { - type: Boolean, - value: false, - }, +/** + * @appliesMixin Gerrit.TooltipMixin + * @extends Polymer.Element + */ +class GrDateFormatter extends mixinBehaviors( [ + Gerrit.TooltipBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** - * When true, the detailed date appears in a GR-TOOLTIP rather than in the - * native browser tooltip. - */ - hasTooltip: Boolean, + static get is() { return 'gr-date-formatter'; } - /** - * The title to be used as the native tooltip or by the tooltip behavior. - */ - title: { - type: String, - reflectToAttribute: true, - computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)', - }, + static get properties() { + return { + dateStr: { + type: String, + value: null, + notify: true, + }, + showDateAndTime: { + type: Boolean, + value: false, + }, - /** @type {?{short: string, full: string}} */ - _dateFormat: Object, - _timeFormat: String, // No default value to prevent flickering. - _relative: Boolean, // No default value to prevent flickering. - }; - } + /** + * When true, the detailed date appears in a GR-TOOLTIP rather than in the + * native browser tooltip. + */ + hasTooltip: Boolean, - /** @override */ - attached() { - super.attached(); - this._loadPreferences(); - } + /** + * The title to be used as the native tooltip or by the tooltip behavior. + */ + title: { + type: String, + reflectToAttribute: true, + computed: '_computeFullDateStr(dateStr, _timeFormat, _dateFormat)', + }, - _getUtcOffsetString() { - return ' UTC' + moment().format('Z'); - } + /** @type {?{short: string, full: string}} */ + _dateFormat: Object, + _timeFormat: String, // No default value to prevent flickering. + _relative: Boolean, // No default value to prevent flickering. + }; + } - _loadPreferences() { - return this._getLoggedIn().then(loggedIn => { - if (!loggedIn) { - this._timeFormat = TimeFormats.TIME_24; - this._dateFormat = DateFormats.STD; - this._relative = false; - return; - } - return Promise.all([ - this._loadTimeFormat(), - this._loadRelative(), - ]); - }); - } + /** @override */ + attached() { + super.attached(); + this._loadPreferences(); + } - _loadTimeFormat() { - return this._getPreferences().then(preferences => { - const timeFormat = preferences && preferences.time_format; - const dateFormat = preferences && preferences.date_format; - this._decideTimeFormat(timeFormat); - this._decideDateFormat(dateFormat); - }); - } + _getUtcOffsetString() { + return ' UTC' + moment().format('Z'); + } - _decideTimeFormat(timeFormat) { - switch (timeFormat) { - case 'HHMM_12': - this._timeFormat = TimeFormats.TIME_12; - break; - case 'HHMM_24': - this._timeFormat = TimeFormats.TIME_24; - break; - default: - throw Error('Invalid time format: ' + timeFormat); + _loadPreferences() { + return this._getLoggedIn().then(loggedIn => { + if (!loggedIn) { + this._timeFormat = TimeFormats.TIME_24; + this._dateFormat = DateFormats.STD; + this._relative = false; + return; } - } + return Promise.all([ + this._loadTimeFormat(), + this._loadRelative(), + ]); + }); + } - _decideDateFormat(dateFormat) { - switch (dateFormat) { - case 'STD': - this._dateFormat = DateFormats.STD; - break; - case 'US': - this._dateFormat = DateFormats.US; - break; - case 'ISO': - this._dateFormat = DateFormats.ISO; - break; - case 'EURO': - this._dateFormat = DateFormats.EURO; - break; - case 'UK': - this._dateFormat = DateFormats.UK; - break; - default: - throw Error('Invalid date format: ' + dateFormat); - } - } + _loadTimeFormat() { + return this._getPreferences().then(preferences => { + const timeFormat = preferences && preferences.time_format; + const dateFormat = preferences && preferences.date_format; + this._decideTimeFormat(timeFormat); + this._decideDateFormat(dateFormat); + }); + } - _loadRelative() { - return this._getPreferences().then(prefs => { - // prefs.relative_date_in_change_table is not set when false. - this._relative = !!(prefs && prefs.relative_date_in_change_table); - }); - } - - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } - - _getPreferences() { - return this.$.restAPI.getPreferences(); - } - - /** - * Return true if date is within 24 hours and on the same day. - */ - _isWithinDay(now, date) { - const diff = -date.diff(now); - return diff < Duration.DAY && date.day() === now.getDay(); - } - - /** - * Returns true if date is from one to six months. - */ - _isWithinHalfYear(now, date) { - const diff = -date.diff(now); - return (date.day() !== now.getDay() || diff >= Duration.DAY) && - diff < 180 * Duration.DAY; - } - - _computeDateStr( - dateStr, timeFormat, dateFormat, relative, showDateAndTime - ) { - if (!dateStr || !timeFormat || !dateFormat) { return ''; } - const date = moment(util.parseDate(dateStr)); - if (!date.isValid()) { return ''; } - if (relative) { - const dateFromNow = date.fromNow(); - if (dateFromNow === 'a few seconds ago') { - return 'just now'; - } else { - return dateFromNow; - } - } - const now = new Date(); - let format = dateFormat.full; - if (this._isWithinDay(now, date)) { - format = timeFormat; - } else { - if (this._isWithinHalfYear(now, date)) { - format = dateFormat.short; - } - if (this.showDateAndTime) { - format = `${format} ${timeFormat}`; - } - } - return date.format(format); - } - - _timeToSecondsFormat(timeFormat) { - return timeFormat === TimeFormats.TIME_12 ? - TimeFormats.TIME_12_WITH_SEC : - TimeFormats.TIME_24_WITH_SEC; - } - - _computeFullDateStr(dateStr, timeFormat, dateFormat) { - // Polymer 2: check for undefined - if ([ - dateStr, - timeFormat, - dateFormat, - ].some(arg => arg === undefined)) { - return undefined; - } - - if (!dateStr) { return ''; } - const date = moment(util.parseDate(dateStr)); - if (!date.isValid()) { return ''; } - let format = dateFormat.full + ', '; - format += this._timeToSecondsFormat(timeFormat); - return date.format(format) + this._getUtcOffsetString(); + _decideTimeFormat(timeFormat) { + switch (timeFormat) { + case 'HHMM_12': + this._timeFormat = TimeFormats.TIME_12; + break; + case 'HHMM_24': + this._timeFormat = TimeFormats.TIME_24; + break; + default: + throw Error('Invalid time format: ' + timeFormat); } } - customElements.define(GrDateFormatter.is, GrDateFormatter); -})(); + _decideDateFormat(dateFormat) { + switch (dateFormat) { + case 'STD': + this._dateFormat = DateFormats.STD; + break; + case 'US': + this._dateFormat = DateFormats.US; + break; + case 'ISO': + this._dateFormat = DateFormats.ISO; + break; + case 'EURO': + this._dateFormat = DateFormats.EURO; + break; + case 'UK': + this._dateFormat = DateFormats.UK; + break; + default: + throw Error('Invalid date format: ' + dateFormat); + } + } + + _loadRelative() { + return this._getPreferences().then(prefs => { + // prefs.relative_date_in_change_table is not set when false. + this._relative = !!(prefs && prefs.relative_date_in_change_table); + }); + } + + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _getPreferences() { + return this.$.restAPI.getPreferences(); + } + + /** + * Return true if date is within 24 hours and on the same day. + */ + _isWithinDay(now, date) { + const diff = -date.diff(now); + return diff < Duration.DAY && date.day() === now.getDay(); + } + + /** + * Returns true if date is from one to six months. + */ + _isWithinHalfYear(now, date) { + const diff = -date.diff(now); + return (date.day() !== now.getDay() || diff >= Duration.DAY) && + diff < 180 * Duration.DAY; + } + + _computeDateStr( + dateStr, timeFormat, dateFormat, relative, showDateAndTime + ) { + if (!dateStr || !timeFormat || !dateFormat) { return ''; } + const date = moment(util.parseDate(dateStr)); + if (!date.isValid()) { return ''; } + if (relative) { + const dateFromNow = date.fromNow(); + if (dateFromNow === 'a few seconds ago') { + return 'just now'; + } else { + return dateFromNow; + } + } + const now = new Date(); + let format = dateFormat.full; + if (this._isWithinDay(now, date)) { + format = timeFormat; + } else { + if (this._isWithinHalfYear(now, date)) { + format = dateFormat.short; + } + if (this.showDateAndTime) { + format = `${format} ${timeFormat}`; + } + } + return date.format(format); + } + + _timeToSecondsFormat(timeFormat) { + return timeFormat === TimeFormats.TIME_12 ? + TimeFormats.TIME_12_WITH_SEC : + TimeFormats.TIME_24_WITH_SEC; + } + + _computeFullDateStr(dateStr, timeFormat, dateFormat) { + // Polymer 2: check for undefined + if ([ + dateStr, + timeFormat, + dateFormat, + ].some(arg => arg === undefined)) { + return undefined; + } + + if (!dateStr) { return ''; } + const date = moment(util.parseDate(dateStr)); + if (!date.isValid()) { return ''; } + let format = dateFormat.full + ', '; + format += this._timeToSecondsFormat(timeFormat); + return date.format(format) + this._getUtcOffsetString(); + } +} + +customElements.define(GrDateFormatter.is, GrDateFormatter);
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js index ae5a945..19aa143 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<script src="../../../scripts/util.js"></script> - -<dom-module id="gr-date-formatter"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { color: inherit; @@ -34,6 +27,4 @@ [[_computeDateStr(dateStr, _timeFormat, _dateFormat, _relative, showDateAndTime)]] </span> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-date-formatter.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html index 0b572bf..65f2248 100644 --- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html +++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-date-formatter</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-date-formatter.html"> +<script type="module" src="./gr-date-formatter.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-date-formatter.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,416 +43,419 @@ </template> </test-fixture> -<script> - suite('gr-date-formatter tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-date-formatter.js'; +suite('gr-date-formatter tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + /** + * Parse server-formatter date and normalize into current timezone. + */ + function normalizedDate(dateStr) { + const d = util.parseDate(dateStr); + d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); + return d; + } + + function testDates(nowStr, dateStr, expected, expectedWithDateAndTime, + expectedTooltip, done) { + // Normalize and convert the date to mimic server response. + dateStr = normalizedDate(dateStr) + .toJSON() + .replace('T', ' ') + .slice(0, -1); + sandbox.useFakeTimers(normalizedDate(nowStr).getTime()); + element.dateStr = dateStr; + flush(() => { + const span = element.shadowRoot + .querySelector('span'); + assert.equal(span.textContent.trim(), expected); + assert.equal(element.title, expectedTooltip); + element.showDateAndTime = true; + flushAsynchronousOperations(); + assert.equal(span.textContent.trim(), expectedWithDateAndTime); + done(); + }); + } + + function stubRestAPI(preferences) { + const loggedInPromise = Promise.resolve(preferences !== null); + const preferencesPromise = Promise.resolve(preferences); + stub('gr-rest-api-interface', { + getLoggedIn: sinon.stub().returns(loggedInPromise), + getPreferences: sinon.stub().returns(preferencesPromise), + }); + return Promise.all([loggedInPromise, preferencesPromise]); + } + + suite('STD + 24 hours time format preference', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_24', + date_format: 'STD', + relative_date_in_change_table: false, + }).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + })); + + test('invalid dates are quietly rejected', () => { + assert.notOk((new Date('foo')).valueOf()); + assert.equal(element._computeDateStr('foo', 'h:mm A'), ''); }); - teardown(() => { - sandbox.restore(); + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '15:34', + '15:34', + 'Jul 29, 2015, 15:34:14', done); }); - /** - * Parse server-formatter date and normalize into current timezone. - */ - function normalizedDate(dateStr) { - const d = util.parseDate(dateStr); - d.setMinutes(d.getMinutes() + d.getTimezoneOffset()); - return d; - } - - function testDates(nowStr, dateStr, expected, expectedWithDateAndTime, - expectedTooltip, done) { - // Normalize and convert the date to mimic server response. - dateStr = normalizedDate(dateStr) - .toJSON() - .replace('T', ' ') - .slice(0, -1); - sandbox.useFakeTimers(normalizedDate(nowStr).getTime()); - element.dateStr = dateStr; - flush(() => { - const span = element.shadowRoot - .querySelector('span'); - assert.equal(span.textContent.trim(), expected); - assert.equal(element.title, expectedTooltip); - element.showDateAndTime = true; - flushAsynchronousOperations(); - assert.equal(span.textContent.trim(), expectedWithDateAndTime); - done(); - }); - } - - function stubRestAPI(preferences) { - const loggedInPromise = Promise.resolve(preferences !== null); - const preferencesPromise = Promise.resolve(preferences); - stub('gr-rest-api-interface', { - getLoggedIn: sinon.stub().returns(loggedInPromise), - getPreferences: sinon.stub().returns(preferencesPromise), - }); - return Promise.all([loggedInPromise, preferencesPromise]); - } - - suite('STD + 24 hours time format preference', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_24', - date_format: 'STD', - relative_date_in_change_table: false, - }).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - })); - - test('invalid dates are quietly rejected', () => { - assert.notOk((new Date('foo')).valueOf()); - assert.equal(element._computeDateStr('foo', 'h:mm A'), ''); - }); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '15:34', - '15:34', - 'Jul 29, 2015, 15:34:14', done); - }); - - test('Within 24 hours on different days', done => { - testDates('2015-07-29 03:34:14.985000000', - '2015-07-28 20:25:14.985000000', - 'Jul 28', - 'Jul 28 20:25', - 'Jul 28, 2015, 20:25:14', done); - }); - - test('More than 24 hours but less than six months', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-06-15 03:25:14.985000000', - 'Jun 15', - 'Jun 15 03:25', - 'Jun 15, 2015, 03:25:14', done); - }); - - test('More than six months', done => { - testDates('2015-09-15 20:34:00.000000000', - '2015-01-15 03:25:00.000000000', - 'Jan 15, 2015', - 'Jan 15, 2015 03:25', - 'Jan 15, 2015, 03:25:00', done); - }); + test('Within 24 hours on different days', done => { + testDates('2015-07-29 03:34:14.985000000', + '2015-07-28 20:25:14.985000000', + 'Jul 28', + 'Jul 28 20:25', + 'Jul 28, 2015, 20:25:14', done); }); - suite('US + 24 hours time format preference', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_24', - date_format: 'US', - relative_date_in_change_table: false, - }).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - })); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '15:34', - '15:34', - '07/29/15, 15:34:14', done); - }); - - test('Within 24 hours on different days', done => { - testDates('2015-07-29 03:34:14.985000000', - '2015-07-28 20:25:14.985000000', - '07/28', - '07/28 20:25', - '07/28/15, 20:25:14', done); - }); - - test('More than 24 hours but less than six months', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-06-15 03:25:14.985000000', - '06/15', - '06/15 03:25', - '06/15/15, 03:25:14', done); - }); + test('More than 24 hours but less than six months', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-06-15 03:25:14.985000000', + 'Jun 15', + 'Jun 15 03:25', + 'Jun 15, 2015, 03:25:14', done); }); - suite('ISO + 24 hours time format preference', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_24', - date_format: 'ISO', - relative_date_in_change_table: false, - }).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - })); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '15:34', - '15:34', - '2015-07-29, 15:34:14', done); - }); - - test('Within 24 hours on different days', done => { - testDates('2015-07-29 03:34:14.985000000', - '2015-07-28 20:25:14.985000000', - '07-28', - '07-28 20:25', - '2015-07-28, 20:25:14', done); - }); - - test('More than 24 hours but less than six months', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-06-15 03:25:14.985000000', - '06-15', - '06-15 03:25', - '2015-06-15, 03:25:14', done); - }); - }); - - suite('EURO + 24 hours time format preference', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_24', - date_format: 'EURO', - relative_date_in_change_table: false, - }).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - })); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '15:34', - '15:34', - '29.07.2015, 15:34:14', done); - }); - - test('Within 24 hours on different days', done => { - testDates('2015-07-29 03:34:14.985000000', - '2015-07-28 20:25:14.985000000', - '28. Jul', - '28. Jul 20:25', - '28.07.2015, 20:25:14', done); - }); - - test('More than 24 hours but less than six months', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-06-15 03:25:14.985000000', - '15. Jun', - '15. Jun 03:25', - '15.06.2015, 03:25:14', done); - }); - }); - - suite('UK + 24 hours time format preference', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_24', - date_format: 'UK', - relative_date_in_change_table: false, - }).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - })); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '15:34', - '15:34', - '29/07/2015, 15:34:14', done); - }); - - test('Within 24 hours on different days', done => { - testDates('2015-07-29 03:34:14.985000000', - '2015-07-28 20:25:14.985000000', - '28/07', - '28/07 20:25', - '28/07/2015, 20:25:14', done); - }); - - test('More than 24 hours but less than six months', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-06-15 03:25:14.985000000', - '15/06', - '15/06 03:25', - '15/06/2015, 03:25:14', done); - }); - }); - - suite('STD + 12 hours time format preference', () => { - setup(() => - // relative_date_in_change_table is not set when false. - stubRestAPI( - {time_format: 'HHMM_12', date_format: 'STD'} - ).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - }) - ); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '3:34 PM', - '3:34 PM', - 'Jul 29, 2015, 3:34:14 PM', done); - }); - }); - - suite('US + 12 hours time format preference', () => { - setup(() => - // relative_date_in_change_table is not set when false. - stubRestAPI( - {time_format: 'HHMM_12', date_format: 'US'} - ).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - }) - ); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '3:34 PM', - '3:34 PM', - '07/29/15, 3:34:14 PM', done); - }); - }); - - suite('ISO + 12 hours time format preference', () => { - setup(() => - // relative_date_in_change_table is not set when false. - stubRestAPI( - {time_format: 'HHMM_12', date_format: 'ISO'} - ).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - }) - ); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '3:34 PM', - '3:34 PM', - '2015-07-29, 3:34:14 PM', done); - }); - }); - - suite('EURO + 12 hours time format preference', () => { - setup(() => - // relative_date_in_change_table is not set when false. - stubRestAPI( - {time_format: 'HHMM_12', date_format: 'EURO'} - ).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - }) - ); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '3:34 PM', - '3:34 PM', - '29.07.2015, 3:34:14 PM', done); - }); - }); - - suite('UK + 12 hours time format preference', () => { - setup(() => - // relative_date_in_change_table is not set when false. - stubRestAPI( - {time_format: 'HHMM_12', date_format: 'UK'} - ).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - }) - ); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '3:34 PM', - '3:34 PM', - '29/07/2015, 3:34:14 PM', done); - }); - }); - - suite('relative date preference', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_12', - date_format: 'STD', - relative_date_in_change_table: true, - }).then(() => { - element = fixture('basic'); - sandbox.stub(element, '_getUtcOffsetString').returns(''); - return element._loadPreferences(); - })); - - test('Within 24 hours on same day', done => { - testDates('2015-07-29 20:34:14.985000000', - '2015-07-29 15:34:14.985000000', - '5 hours ago', - '5 hours ago', - 'Jul 29, 2015, 3:34:14 PM', done); - }); - - test('More than six months', done => { - testDates('2015-09-15 20:34:00.000000000', - '2015-01-15 03:25:00.000000000', - '8 months ago', - '8 months ago', - 'Jan 15, 2015, 3:25:00 AM', done); - }); - }); - - suite('logged in', () => { - setup(() => stubRestAPI({ - time_format: 'HHMM_12', - date_format: 'US', - relative_date_in_change_table: true, - }).then(() => { - element = fixture('basic'); - return element._loadPreferences(); - })); - - test('Preferences are respected', () => { - assert.equal(element._timeFormat, 'h:mm A'); - assert.equal(element._dateFormat.short, 'MM/DD'); - assert.equal(element._dateFormat.full, 'MM/DD/YY'); - assert.isTrue(element._relative); - }); - }); - - suite('logged out', () => { - setup(() => stubRestAPI(null).then(() => { - element = fixture('basic'); - return element._loadPreferences(); - })); - - test('Default preferences are respected', () => { - assert.equal(element._timeFormat, 'HH:mm'); - assert.equal(element._dateFormat.short, 'MMM DD'); - assert.equal(element._dateFormat.full, 'MMM DD, YYYY'); - assert.isFalse(element._relative); - }); + test('More than six months', done => { + testDates('2015-09-15 20:34:00.000000000', + '2015-01-15 03:25:00.000000000', + 'Jan 15, 2015', + 'Jan 15, 2015 03:25', + 'Jan 15, 2015, 03:25:00', done); }); }); + + suite('US + 24 hours time format preference', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_24', + date_format: 'US', + relative_date_in_change_table: false, + }).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + })); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '15:34', + '15:34', + '07/29/15, 15:34:14', done); + }); + + test('Within 24 hours on different days', done => { + testDates('2015-07-29 03:34:14.985000000', + '2015-07-28 20:25:14.985000000', + '07/28', + '07/28 20:25', + '07/28/15, 20:25:14', done); + }); + + test('More than 24 hours but less than six months', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-06-15 03:25:14.985000000', + '06/15', + '06/15 03:25', + '06/15/15, 03:25:14', done); + }); + }); + + suite('ISO + 24 hours time format preference', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_24', + date_format: 'ISO', + relative_date_in_change_table: false, + }).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + })); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '15:34', + '15:34', + '2015-07-29, 15:34:14', done); + }); + + test('Within 24 hours on different days', done => { + testDates('2015-07-29 03:34:14.985000000', + '2015-07-28 20:25:14.985000000', + '07-28', + '07-28 20:25', + '2015-07-28, 20:25:14', done); + }); + + test('More than 24 hours but less than six months', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-06-15 03:25:14.985000000', + '06-15', + '06-15 03:25', + '2015-06-15, 03:25:14', done); + }); + }); + + suite('EURO + 24 hours time format preference', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_24', + date_format: 'EURO', + relative_date_in_change_table: false, + }).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + })); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '15:34', + '15:34', + '29.07.2015, 15:34:14', done); + }); + + test('Within 24 hours on different days', done => { + testDates('2015-07-29 03:34:14.985000000', + '2015-07-28 20:25:14.985000000', + '28. Jul', + '28. Jul 20:25', + '28.07.2015, 20:25:14', done); + }); + + test('More than 24 hours but less than six months', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-06-15 03:25:14.985000000', + '15. Jun', + '15. Jun 03:25', + '15.06.2015, 03:25:14', done); + }); + }); + + suite('UK + 24 hours time format preference', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_24', + date_format: 'UK', + relative_date_in_change_table: false, + }).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + })); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '15:34', + '15:34', + '29/07/2015, 15:34:14', done); + }); + + test('Within 24 hours on different days', done => { + testDates('2015-07-29 03:34:14.985000000', + '2015-07-28 20:25:14.985000000', + '28/07', + '28/07 20:25', + '28/07/2015, 20:25:14', done); + }); + + test('More than 24 hours but less than six months', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-06-15 03:25:14.985000000', + '15/06', + '15/06 03:25', + '15/06/2015, 03:25:14', done); + }); + }); + + suite('STD + 12 hours time format preference', () => { + setup(() => + // relative_date_in_change_table is not set when false. + stubRestAPI( + {time_format: 'HHMM_12', date_format: 'STD'} + ).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + }) + ); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '3:34 PM', + '3:34 PM', + 'Jul 29, 2015, 3:34:14 PM', done); + }); + }); + + suite('US + 12 hours time format preference', () => { + setup(() => + // relative_date_in_change_table is not set when false. + stubRestAPI( + {time_format: 'HHMM_12', date_format: 'US'} + ).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + }) + ); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '3:34 PM', + '3:34 PM', + '07/29/15, 3:34:14 PM', done); + }); + }); + + suite('ISO + 12 hours time format preference', () => { + setup(() => + // relative_date_in_change_table is not set when false. + stubRestAPI( + {time_format: 'HHMM_12', date_format: 'ISO'} + ).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + }) + ); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '3:34 PM', + '3:34 PM', + '2015-07-29, 3:34:14 PM', done); + }); + }); + + suite('EURO + 12 hours time format preference', () => { + setup(() => + // relative_date_in_change_table is not set when false. + stubRestAPI( + {time_format: 'HHMM_12', date_format: 'EURO'} + ).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + }) + ); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '3:34 PM', + '3:34 PM', + '29.07.2015, 3:34:14 PM', done); + }); + }); + + suite('UK + 12 hours time format preference', () => { + setup(() => + // relative_date_in_change_table is not set when false. + stubRestAPI( + {time_format: 'HHMM_12', date_format: 'UK'} + ).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + }) + ); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '3:34 PM', + '3:34 PM', + '29/07/2015, 3:34:14 PM', done); + }); + }); + + suite('relative date preference', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_12', + date_format: 'STD', + relative_date_in_change_table: true, + }).then(() => { + element = fixture('basic'); + sandbox.stub(element, '_getUtcOffsetString').returns(''); + return element._loadPreferences(); + })); + + test('Within 24 hours on same day', done => { + testDates('2015-07-29 20:34:14.985000000', + '2015-07-29 15:34:14.985000000', + '5 hours ago', + '5 hours ago', + 'Jul 29, 2015, 3:34:14 PM', done); + }); + + test('More than six months', done => { + testDates('2015-09-15 20:34:00.000000000', + '2015-01-15 03:25:00.000000000', + '8 months ago', + '8 months ago', + 'Jan 15, 2015, 3:25:00 AM', done); + }); + }); + + suite('logged in', () => { + setup(() => stubRestAPI({ + time_format: 'HHMM_12', + date_format: 'US', + relative_date_in_change_table: true, + }).then(() => { + element = fixture('basic'); + return element._loadPreferences(); + })); + + test('Preferences are respected', () => { + assert.equal(element._timeFormat, 'h:mm A'); + assert.equal(element._dateFormat.short, 'MM/DD'); + assert.equal(element._dateFormat.full, 'MM/DD/YY'); + assert.isTrue(element._relative); + }); + }); + + suite('logged out', () => { + setup(() => stubRestAPI(null).then(() => { + element = fixture('basic'); + return element._loadPreferences(); + })); + + test('Default preferences are respected', () => { + assert.equal(element._timeFormat, 'HH:mm'); + assert.equal(element._dateFormat.short, 'MMM DD'); + assert.equal(element._dateFormat.full, 'MMM DD, YYYY'); + assert.isFalse(element._relative); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js index 8d00452..8141863 100644 --- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js +++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog.js
@@ -14,85 +14,94 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../gr-button/gr-button.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-dialog_html.js'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrDialog extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-dialog'; } + /** + * Fired when the confirm button is pressed. + * + * @event confirm + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event cancel */ - class GrDialog extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-dialog'; } - /** - * Fired when the confirm button is pressed. - * - * @event confirm - */ - /** - * Fired when the cancel button is pressed. - * - * @event cancel - */ - - static get properties() { - return { - confirmLabel: { - type: String, - value: 'Confirm', - }, - // Supplying an empty cancel label will hide the button completely. - cancelLabel: { - type: String, - value: 'Cancel', - }, - disabled: { - type: Boolean, - value: false, - }, - confirmOnEnter: { - type: Boolean, - value: false, - }, - }; - } - - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('role', 'dialog'); - } - - _handleConfirm(e) { - if (this.disabled) { return; } - - e.preventDefault(); - e.stopPropagation(); - this.fire('confirm', null, {bubbles: false}); - } - - _handleCancelTap(e) { - e.preventDefault(); - e.stopPropagation(); - this.fire('cancel', null, {bubbles: false}); - } - - _handleKeydown(e) { - if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); } - } - - resetFocus() { - this.$.confirm.focus(); - } - - _computeCancelClass(cancelLabel) { - return cancelLabel.length ? '' : 'hidden'; - } + static get properties() { + return { + confirmLabel: { + type: String, + value: 'Confirm', + }, + // Supplying an empty cancel label will hide the button completely. + cancelLabel: { + type: String, + value: 'Cancel', + }, + disabled: { + type: Boolean, + value: false, + }, + confirmOnEnter: { + type: Boolean, + value: false, + }, + }; } - customElements.define(GrDialog.is, GrDialog); -})(); + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('role', 'dialog'); + } + + _handleConfirm(e) { + if (this.disabled) { return; } + + e.preventDefault(); + e.stopPropagation(); + this.fire('confirm', null, {bubbles: false}); + } + + _handleCancelTap(e) { + e.preventDefault(); + e.stopPropagation(); + this.fire('cancel', null, {bubbles: false}); + } + + _handleKeydown(e) { + if (this.confirmOnEnter && e.keyCode === 13) { this._handleConfirm(e); } + } + + resetFocus() { + this.$.confirm.focus(); + } + + _computeCancelClass(cancelLabel) { + return cancelLabel.length ? '' : 'hidden'; + } +} + +customElements.define(GrDialog.is, GrDialog);
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js index 475f6e2..ba85fd4 100644 --- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js +++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../gr-button/gr-button.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-dialog"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { color: var(--primary-text-color); @@ -73,14 +68,12 @@ </main> <footer> <slot name="footer"></slot> - <gr-button id="cancel" class$="[[_computeCancelClass(cancelLabel)]]" link on-click="_handleCancelTap"> + <gr-button id="cancel" class\$="[[_computeCancelClass(cancelLabel)]]" link="" on-click="_handleCancelTap"> [[cancelLabel]] </gr-button> - <gr-button id="confirm" link primary on-click="_handleConfirm" disabled="[[disabled]]"> + <gr-button id="confirm" link="" primary="" on-click="_handleConfirm" disabled="[[disabled]]"> [[confirmLabel]] </gr-button> </footer> </div> - </template> - <script src="gr-dialog.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html index ad93962..87e31a0 100644 --- a/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dialog/gr-dialog_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dialog</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-dialog.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,65 +40,67 @@ </template> </test-fixture> -<script> - suite('gr-dialog tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dialog.js'; +suite('gr-dialog tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('events', done => { - let numEvents = 0; - function handler() { if (++numEvents == 2) { done(); } } - - element.addEventListener('confirm', handler); - element.addEventListener('cancel', handler); - - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button[primary]')); - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button:not([primary])')); - }); - - test('confirmOnEnter', () => { - element.confirmOnEnter = false; - const handleConfirmStub = sandbox.stub(element, '_handleConfirm'); - const handleKeydownSpy = sandbox.spy(element, '_handleKeydown'); - MockInteractions.pressAndReleaseKeyOn(element.shadowRoot - .querySelector('main'), - 13, null, 'enter'); - flushAsynchronousOperations(); - - assert.isTrue(handleKeydownSpy.called); - assert.isFalse(handleConfirmStub.called); - - element.confirmOnEnter = true; - MockInteractions.pressAndReleaseKeyOn(element.shadowRoot - .querySelector('main'), - 13, null, 'enter'); - flushAsynchronousOperations(); - - assert.isTrue(handleConfirmStub.called); - }); - - test('resetFocus', () => { - const focusStub = sandbox.stub(element.$.confirm, 'focus'); - element.resetFocus(); - assert.isTrue(focusStub.calledOnce); - }); - - test('empty cancel label hides cancel btn', () => { - assert.isFalse(isHidden(element.$.cancel)); - element.cancelLabel = ''; - flushAsynchronousOperations(); - - assert.isTrue(isHidden(element.$.cancel)); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { sandbox.restore(); }); + + test('events', done => { + let numEvents = 0; + function handler() { if (++numEvents == 2) { done(); } } + + element.addEventListener('confirm', handler); + element.addEventListener('cancel', handler); + + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button[primary]')); + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button:not([primary])')); + }); + + test('confirmOnEnter', () => { + element.confirmOnEnter = false; + const handleConfirmStub = sandbox.stub(element, '_handleConfirm'); + const handleKeydownSpy = sandbox.spy(element, '_handleKeydown'); + MockInteractions.pressAndReleaseKeyOn(element.shadowRoot + .querySelector('main'), + 13, null, 'enter'); + flushAsynchronousOperations(); + + assert.isTrue(handleKeydownSpy.called); + assert.isFalse(handleConfirmStub.called); + + element.confirmOnEnter = true; + MockInteractions.pressAndReleaseKeyOn(element.shadowRoot + .querySelector('main'), + 13, null, 'enter'); + flushAsynchronousOperations(); + + assert.isTrue(handleConfirmStub.called); + }); + + test('resetFocus', () => { + const focusStub = sandbox.stub(element.$.confirm, 'focus'); + element.resetFocus(); + assert.isTrue(focusStub.calledOnce); + }); + + test('empty cancel label hides cancel btn', () => { + assert.isFalse(isHidden(element.$.cancel)); + element.cancelLabel = ''; + flushAsynchronousOperations(); + + assert.isTrue(isHidden(element.$.cancel)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js index c408e5a..00f9078 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences.js
@@ -14,72 +14,82 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrDiffPreferences extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-diff-preferences'; } +import '@polymer/iron-input/iron-input.js'; +import '../../../styles/shared-styles.js'; +import '../gr-button/gr-button.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-select/gr-select.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-diff-preferences_html.js'; - static get properties() { - return { - hasUnsavedChanges: { - type: Boolean, - notify: true, - value: false, - }, +/** @extends Polymer.Element */ +class GrDiffPreferences extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** @type {?} */ - diffPrefs: Object, - }; - } + static get is() { return 'gr-diff-preferences'; } - loadData() { - return this.$.restAPI.getDiffPreferences().then(prefs => { - this.diffPrefs = prefs; - }); - } + static get properties() { + return { + hasUnsavedChanges: { + type: Boolean, + notify: true, + value: false, + }, - _handleDiffPrefsChanged() { - this.hasUnsavedChanges = true; - } - - _handleLineWrappingTap() { - this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked); - this._handleDiffPrefsChanged(); - } - - _handleShowTabsTap() { - this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked); - this._handleDiffPrefsChanged(); - } - - _handleShowTrailingWhitespaceTap() { - this.set('diffPrefs.show_whitespace_errors', - this.$.showTrailingWhitespaceInput.checked); - this._handleDiffPrefsChanged(); - } - - _handleSyntaxHighlightTap() { - this.set('diffPrefs.syntax_highlighting', - this.$.syntaxHighlightInput.checked); - this._handleDiffPrefsChanged(); - } - - _handleAutomaticReviewTap() { - this.set('diffPrefs.manual_review', - !this.$.automaticReviewInput.checked); - this._handleDiffPrefsChanged(); - } - - save() { - return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => { - this.hasUnsavedChanges = false; - }); - } + /** @type {?} */ + diffPrefs: Object, + }; } - customElements.define(GrDiffPreferences.is, GrDiffPreferences); -})(); + loadData() { + return this.$.restAPI.getDiffPreferences().then(prefs => { + this.diffPrefs = prefs; + }); + } + + _handleDiffPrefsChanged() { + this.hasUnsavedChanges = true; + } + + _handleLineWrappingTap() { + this.set('diffPrefs.line_wrapping', this.$.lineWrappingInput.checked); + this._handleDiffPrefsChanged(); + } + + _handleShowTabsTap() { + this.set('diffPrefs.show_tabs', this.$.showTabsInput.checked); + this._handleDiffPrefsChanged(); + } + + _handleShowTrailingWhitespaceTap() { + this.set('diffPrefs.show_whitespace_errors', + this.$.showTrailingWhitespaceInput.checked); + this._handleDiffPrefsChanged(); + } + + _handleSyntaxHighlightTap() { + this.set('diffPrefs.syntax_highlighting', + this.$.syntaxHighlightInput.checked); + this._handleDiffPrefsChanged(); + } + + _handleAutomaticReviewTap() { + this.set('diffPrefs.manual_review', + !this.$.automaticReviewInput.checked); + this._handleDiffPrefsChanged(); + } + + save() { + return this.$.restAPI.saveDiffPreferences(this.diffPrefs).then(res => { + this.hasUnsavedChanges = false; + }); + } +} + +customElements.define(GrDiffPreferences.is, GrDiffPreferences);
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js index 367e30c..7869c2c 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-button/gr-button.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../gr-select/gr-select.html"> - -<dom-module id="gr-diff-preferences"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -34,12 +27,8 @@ <section> <span class="title">Context</span> <span class="value"> - <gr-select - id="contextSelect" - bind-value="{{diffPrefs.context}}"> - <select - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> + <gr-select id="contextSelect" bind-value="{{diffPrefs.context}}"> + <select on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> <option value="3">3 lines</option> <option value="10">10 lines</option> <option value="25">25 lines</option> @@ -54,117 +43,55 @@ <section> <span class="title">Fit to screen</span> <span class="value"> - <input - id="lineWrappingInput" - type="checkbox" - checked$="[[diffPrefs.line_wrapping]]" - on-change="_handleLineWrappingTap"> + <input id="lineWrappingInput" type="checkbox" checked\$="[[diffPrefs.line_wrapping]]" on-change="_handleLineWrappingTap"> </span> </section> <section> <span class="title">Diff width</span> <span class="value"> - <iron-input - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{diffPrefs.line_length}}" - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> - <input - is="iron-input" - type="number" - id="columnsInput" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{diffPrefs.line_length}}" - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> + <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.line_length}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> + <input is="iron-input" type="number" id="columnsInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.line_length}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> </iron-input> </span> </section> <section> <span class="title">Tab width</span> <span class="value"> - <iron-input - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{diffPrefs.tab_size}}" - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> - <input - is="iron-input" - type="number" - id="tabSizeInput" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{diffPrefs.tab_size}}" - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> + <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.tab_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> + <input is="iron-input" type="number" id="tabSizeInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.tab_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> </iron-input> </span> </section> - <section hidden$="[[!diffPrefs.font_size]]"> + <section hidden\$="[[!diffPrefs.font_size]]"> <span class="title">Font size</span> <span class="value"> - <iron-input - type="number" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{diffPrefs.font_size}}" - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> - <input - is="iron-input" - type="number" - id="fontSizeInput" - prevent-invalid-input - allowed-pattern="[0-9]" - bind-value="{{diffPrefs.font_size}}" - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> + <iron-input type="number" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.font_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> + <input is="iron-input" type="number" id="fontSizeInput" prevent-invalid-input="" allowed-pattern="[0-9]" bind-value="{{diffPrefs.font_size}}" on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> </iron-input> </span> </section> <section> <span class="title">Show tabs</span> <span class="value"> - <input - id="showTabsInput" - type="checkbox" - checked$="[[diffPrefs.show_tabs]]" - on-change="_handleShowTabsTap"> + <input id="showTabsInput" type="checkbox" checked\$="[[diffPrefs.show_tabs]]" on-change="_handleShowTabsTap"> </span> </section> <section> <span class="title">Show trailing whitespace</span> <span class="value"> - <input - id="showTrailingWhitespaceInput" - type="checkbox" - checked$="[[diffPrefs.show_whitespace_errors]]" - on-change="_handleShowTrailingWhitespaceTap"> + <input id="showTrailingWhitespaceInput" type="checkbox" checked\$="[[diffPrefs.show_whitespace_errors]]" on-change="_handleShowTrailingWhitespaceTap"> </span> </section> <section> <span class="title">Syntax highlighting</span> <span class="value"> - <input - id="syntaxHighlightInput" - type="checkbox" - checked$="[[diffPrefs.syntax_highlighting]]" - on-change="_handleSyntaxHighlightTap"> + <input id="syntaxHighlightInput" type="checkbox" checked\$="[[diffPrefs.syntax_highlighting]]" on-change="_handleSyntaxHighlightTap"> </span> </section> <section> <span class="title">Automatically mark viewed files reviewed</span> <span class="value"> - <input - id="automaticReviewInput" - type="checkbox" - checked$="[[!diffPrefs.manual_review]]" - on-change="_handleAutomaticReviewTap"> + <input id="automaticReviewInput" type="checkbox" checked\$="[[!diffPrefs.manual_review]]" on-change="_handleAutomaticReviewTap"> </span> </section> <section> @@ -172,12 +99,10 @@ <span class="title">Ignore Whitespace</span> <span class="value"> <gr-select bind-value="{{diffPrefs.ignore_whitespace}}"> - <select - on-keypress="_handleDiffPrefsChanged" - on-change="_handleDiffPrefsChanged"> + <select on-keypress="_handleDiffPrefsChanged" on-change="_handleDiffPrefsChanged"> <option value="IGNORE_NONE">None</option> <option value="IGNORE_TRAILING">Trailing</option> - <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option> + <option value="IGNORE_LEADING_AND_TRAILING">Leading & trailing</option> <option value="IGNORE_ALL">All</option> </select> </gr-select> @@ -186,6 +111,4 @@ </section> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-diff-preferences.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html index 3c2a7d1..0387b89 100644 --- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html +++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-diff-preferences</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-diff-preferences.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-diff-preferences.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-preferences.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,93 +40,95 @@ </template> </test-fixture> -<script> - suite('gr-diff-preferences tests', async () => { - await readyToTest(); - let element; - let sandbox; - let diffPreferences; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-diff-preferences.js'; +suite('gr-diff-preferences tests', () => { + let element; + let sandbox; + let diffPreferences; - function valueOf(title, fieldsetid) { - const sections = element.$[fieldsetid].querySelectorAll('section'); - let titleEl; - for (let i = 0; i < sections.length; i++) { - titleEl = sections[i].querySelector('.title'); - if (titleEl.textContent.trim() === title) { - return sections[i].querySelector('.value'); - } + function valueOf(title, fieldsetid) { + const sections = element.$[fieldsetid].querySelectorAll('section'); + let titleEl; + for (let i = 0; i < sections.length; i++) { + titleEl = sections[i].querySelector('.title'); + if (titleEl.textContent.trim() === title) { + return sections[i].querySelector('.value'); } } + } - setup(() => { - diffPreferences = { - context: 10, - line_wrapping: false, - line_length: 100, - tab_size: 8, - font_size: 12, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - manual_review: false, - ignore_whitespace: 'IGNORE_NONE', - }; + setup(() => { + diffPreferences = { + context: 10, + line_wrapping: false, + line_length: 100, + tab_size: 8, + font_size: 12, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + manual_review: false, + ignore_whitespace: 'IGNORE_NONE', + }; - stub('gr-rest-api-interface', { - getDiffPreferences() { - return Promise.resolve(diffPreferences); - }, - }); - - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - return element.loadData(); + stub('gr-rest-api-interface', { + getDiffPreferences() { + return Promise.resolve(diffPreferences); + }, }); - teardown(() => { sandbox.restore(); }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + return element.loadData(); + }); - test('renders', () => { - // Rendered with the expected preferences selected. - assert.equal(valueOf('Context', 'diffPreferences') - .firstElementChild.bindValue, diffPreferences.context); - assert.equal(valueOf('Fit to screen', 'diffPreferences') - .firstElementChild.checked, diffPreferences.line_wrapping); - assert.equal(valueOf('Diff width', 'diffPreferences') - .firstElementChild.bindValue, diffPreferences.line_length); - assert.equal(valueOf('Tab width', 'diffPreferences') - .firstElementChild.bindValue, diffPreferences.tab_size); - assert.equal(valueOf('Font size', 'diffPreferences') - .firstElementChild.bindValue, diffPreferences.font_size); - assert.equal(valueOf('Show tabs', 'diffPreferences') - .firstElementChild.checked, diffPreferences.show_tabs); - assert.equal(valueOf('Show trailing whitespace', 'diffPreferences') - .firstElementChild.checked, diffPreferences.show_whitespace_errors); - assert.equal(valueOf('Syntax highlighting', 'diffPreferences') - .firstElementChild.checked, diffPreferences.syntax_highlighting); - assert.equal( - valueOf('Automatically mark viewed files reviewed', 'diffPreferences') - .firstElementChild.checked, !diffPreferences.manual_review); - assert.equal(valueOf('Ignore Whitespace', 'diffPreferences') - .firstElementChild.bindValue, diffPreferences.ignore_whitespace); + teardown(() => { sandbox.restore(); }); + test('renders', () => { + // Rendered with the expected preferences selected. + assert.equal(valueOf('Context', 'diffPreferences') + .firstElementChild.bindValue, diffPreferences.context); + assert.equal(valueOf('Fit to screen', 'diffPreferences') + .firstElementChild.checked, diffPreferences.line_wrapping); + assert.equal(valueOf('Diff width', 'diffPreferences') + .firstElementChild.bindValue, diffPreferences.line_length); + assert.equal(valueOf('Tab width', 'diffPreferences') + .firstElementChild.bindValue, diffPreferences.tab_size); + assert.equal(valueOf('Font size', 'diffPreferences') + .firstElementChild.bindValue, diffPreferences.font_size); + assert.equal(valueOf('Show tabs', 'diffPreferences') + .firstElementChild.checked, diffPreferences.show_tabs); + assert.equal(valueOf('Show trailing whitespace', 'diffPreferences') + .firstElementChild.checked, diffPreferences.show_whitespace_errors); + assert.equal(valueOf('Syntax highlighting', 'diffPreferences') + .firstElementChild.checked, diffPreferences.syntax_highlighting); + assert.equal( + valueOf('Automatically mark viewed files reviewed', 'diffPreferences') + .firstElementChild.checked, !diffPreferences.manual_review); + assert.equal(valueOf('Ignore Whitespace', 'diffPreferences') + .firstElementChild.bindValue, diffPreferences.ignore_whitespace); + + assert.isFalse(element.hasUnsavedChanges); + }); + + test('save changes', () => { + sandbox.stub(element.$.restAPI, 'saveDiffPreferences') + .returns(Promise.resolve()); + const showTrailingWhitespaceCheckbox = + valueOf('Show trailing whitespace', 'diffPreferences') + .firstElementChild; + showTrailingWhitespaceCheckbox.checked = false; + element._handleShowTrailingWhitespaceTap(); + + assert.isTrue(element.hasUnsavedChanges); + + // Save the change. + return element.save().then(() => { assert.isFalse(element.hasUnsavedChanges); }); - - test('save changes', () => { - sandbox.stub(element.$.restAPI, 'saveDiffPreferences') - .returns(Promise.resolve()); - const showTrailingWhitespaceCheckbox = - valueOf('Show trailing whitespace', 'diffPreferences') - .firstElementChild; - showTrailingWhitespaceCheckbox.checked = false; - element._handleShowTrailingWhitespaceTap(); - - assert.isTrue(element.hasUnsavedChanges); - - // Save the change. - return element.save().then(() => { - assert.isFalse(element.hasUnsavedChanges); - }); - }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js index 04df531..e9befaf 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands.js
@@ -14,82 +14,93 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element - */ - class GrDownloadCommands extends Polymer.mixinBehaviors( [ - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-download-commands'; } +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import '@polymer/paper-tabs/paper-tabs.js'; +import '../gr-shell-command/gr-shell-command.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-download-commands_html.js'; - static get properties() { - return { - commands: Array, - _loggedIn: { - type: Boolean, - value: false, - observer: '_loggedInChanged', - }, - schemes: Array, - selectedScheme: { - type: String, - notify: true, - }, - }; - } +/** + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrDownloadCommands extends mixinBehaviors( [ + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this._getLoggedIn().then(loggedIn => { - this._loggedIn = loggedIn; - }); - } + static get is() { return 'gr-download-commands'; } - focusOnCopy() { - this.shadowRoot.querySelector('gr-shell-command').focusOnCopy(); - } + static get properties() { + return { + commands: Array, + _loggedIn: { + type: Boolean, + value: false, + observer: '_loggedInChanged', + }, + schemes: Array, + selectedScheme: { + type: String, + notify: true, + }, + }; + } - _getLoggedIn() { - return this.$.restAPI.getLoggedIn(); - } + /** @override */ + attached() { + super.attached(); + this._getLoggedIn().then(loggedIn => { + this._loggedIn = loggedIn; + }); + } - _loggedInChanged(loggedIn) { - if (!loggedIn) { return; } - return this.$.restAPI.getPreferences().then(prefs => { - if (prefs.download_scheme) { - // Note (issue 5180): normalize the download scheme with lower-case. - this.selectedScheme = prefs.download_scheme.toLowerCase(); - } - }); - } + focusOnCopy() { + this.shadowRoot.querySelector('gr-shell-command').focusOnCopy(); + } - _handleTabChange(e) { - const scheme = this.schemes[e.detail.value]; - if (scheme && scheme !== this.selectedScheme) { - this.set('selectedScheme', scheme); - if (this._loggedIn) { - this.$.restAPI.savePreferences( - {download_scheme: this.selectedScheme}); - } + _getLoggedIn() { + return this.$.restAPI.getLoggedIn(); + } + + _loggedInChanged(loggedIn) { + if (!loggedIn) { return; } + return this.$.restAPI.getPreferences().then(prefs => { + if (prefs.download_scheme) { + // Note (issue 5180): normalize the download scheme with lower-case. + this.selectedScheme = prefs.download_scheme.toLowerCase(); } - } + }); + } - _computeSelected(schemes, selectedScheme) { - return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) + - ''; - } - - _computeShowTabs(schemes) { - return schemes.length > 1 ? '' : 'hidden'; + _handleTabChange(e) { + const scheme = this.schemes[e.detail.value]; + if (scheme && scheme !== this.selectedScheme) { + this.set('selectedScheme', scheme); + if (this._loggedIn) { + this.$.restAPI.savePreferences( + {download_scheme: this.selectedScheme}); + } } } - customElements.define(GrDownloadCommands.is, GrDownloadCommands); -})(); + _computeSelected(schemes, selectedScheme) { + return (schemes.findIndex(scheme => scheme === selectedScheme) || 0) + + ''; + } + + _computeShowTabs(schemes) { + return schemes.length > 1 ? '' : 'hidden'; + } +} + +customElements.define(GrDownloadCommands.is, GrDownloadCommands);
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js index 14a65b2..12a8d01 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - - -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="/bower_components/paper-tabs/paper-tabs.html"> -<link rel="import" href="../../shared/gr-shell-command/gr-shell-command.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-download-commands"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> paper-tabs { height: 3rem; @@ -61,26 +52,16 @@ } </style> <div class="schemes"> - <paper-tabs - id="downloadTabs" - class$="[[_computeShowTabs(schemes)]]" - selected="[[_computeSelected(schemes, selectedScheme)]]" - on-selected-changed="_handleTabChange"> + <paper-tabs id="downloadTabs" class\$="[[_computeShowTabs(schemes)]]" selected="[[_computeSelected(schemes, selectedScheme)]]" on-selected-changed="_handleTabChange"> <template is="dom-repeat" items="[[schemes]]" as="scheme"> - <paper-tab data-scheme$="[[scheme]]">[[scheme]]</paper-tab> + <paper-tab data-scheme\$="[[scheme]]">[[scheme]]</paper-tab> </template> </paper-tabs> </div> - <div class="commands" hidden$="[[!schemes.length]]" hidden> - <template is="dom-repeat" - items="[[commands]]" - as="command"> - <gr-shell-command - label=[[command.title]] - command=[[command.command]]></gr-shell-command> + <div class="commands" hidden\$="[[!schemes.length]]" hidden=""> + <template is="dom-repeat" items="[[commands]]" as="command"> + <gr-shell-command label="[[command.title]]" command="[[command.command]]"></gr-shell-command> </template> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-download-commands.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html index 4e37f9e..a39a433 100644 --- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html +++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-download-commands</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-download-commands.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-download-commands.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-download-commands.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,123 +40,125 @@ </template> </test-fixture> -<script> - suite('gr-download-commands', async () => { - await readyToTest(); - let element; - let sandbox; - const SCHEMES = ['http', 'repo', 'ssh']; - const COMMANDS = [{ - title: 'Checkout', - command: `git fetch http://andybons@localhost:8080/a/test-project - refs/changes/05/5/1 && git checkout FETCH_HEAD`, - }, { - title: 'Cherry Pick', - command: `git fetch http://andybons@localhost:8080/a/test-project - refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`, - }, { - title: 'Format Patch', - command: `git fetch http://andybons@localhost:8080/a/test-project - refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`, - }, { - title: 'Pull', - command: `git pull http://andybons@localhost:8080/a/test-project - refs/changes/05/5/1`, - }]; - const SELECTED_SCHEME = 'http'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-download-commands.js'; +suite('gr-download-commands', () => { + let element; + let sandbox; + const SCHEMES = ['http', 'repo', 'ssh']; + const COMMANDS = [{ + title: 'Checkout', + command: `git fetch http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1 && git checkout FETCH_HEAD`, + }, { + title: 'Cherry Pick', + command: `git fetch http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1 && git cherry-pick FETCH_HEAD`, + }, { + title: 'Format Patch', + command: `git fetch http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1 && git format-patch -1 --stdout FETCH_HEAD`, + }, { + title: 'Pull', + command: `git pull http://andybons@localhost:8080/a/test-project + refs/changes/05/5/1`, + }]; + const SELECTED_SCHEME = 'http'; - setup(() => { - sandbox = sinon.sandbox.create(); + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('unauthenticated', () => { + setup(done => { + element = fixture('basic'); + element.schemes = SCHEMES; + element.commands = COMMANDS; + element.selectedScheme = SELECTED_SCHEME; + flushAsynchronousOperations(); + flush(done); }); - teardown(() => { - sandbox.restore(); + test('focusOnCopy', () => { + const focusStub = sandbox.stub(element.shadowRoot + .querySelector('gr-shell-command'), + 'focusOnCopy'); + element.focusOnCopy(); + assert.isTrue(focusStub.called); }); - suite('unauthenticated', () => { - setup(done => { - element = fixture('basic'); - element.schemes = SCHEMES; - element.commands = COMMANDS; - element.selectedScheme = SELECTED_SCHEME; - flushAsynchronousOperations(); - flush(done); + test('element visibility', () => { + assert.isFalse(isHidden(element.shadowRoot + .querySelector('paper-tabs'))); + assert.isFalse(isHidden(element.shadowRoot + .querySelector('.commands'))); + + element.schemes = []; + assert.isTrue(isHidden(element.shadowRoot + .querySelector('paper-tabs'))); + assert.isTrue(isHidden(element.shadowRoot + .querySelector('.commands'))); + }); + + test('tab selection', done => { + assert.equal(element.$.downloadTabs.selected, '0'); + MockInteractions.tap(element.shadowRoot + .querySelector('[data-scheme="ssh"]')); + flushAsynchronousOperations(); + assert.equal(element.selectedScheme, 'ssh'); + assert.equal(element.$.downloadTabs.selected, '2'); + done(); + }); + + test('loads scheme from preferences', done => { + stub('gr-rest-api-interface', { + getPreferences() { + return Promise.resolve({download_scheme: 'repo'}); + }, }); - - test('focusOnCopy', () => { - const focusStub = sandbox.stub(element.shadowRoot - .querySelector('gr-shell-command'), - 'focusOnCopy'); - element.focusOnCopy(); - assert.isTrue(focusStub.called); - }); - - test('element visibility', () => { - assert.isFalse(isHidden(element.shadowRoot - .querySelector('paper-tabs'))); - assert.isFalse(isHidden(element.shadowRoot - .querySelector('.commands'))); - - element.schemes = []; - assert.isTrue(isHidden(element.shadowRoot - .querySelector('paper-tabs'))); - assert.isTrue(isHidden(element.shadowRoot - .querySelector('.commands'))); - }); - - test('tab selection', done => { - assert.equal(element.$.downloadTabs.selected, '0'); - MockInteractions.tap(element.shadowRoot - .querySelector('[data-scheme="ssh"]')); - flushAsynchronousOperations(); - assert.equal(element.selectedScheme, 'ssh'); - assert.equal(element.$.downloadTabs.selected, '2'); + element._loggedIn = true; + assert.isTrue(element.$.restAPI.getPreferences.called); + element.$.restAPI.getPreferences.lastCall.returnValue.then(() => { + assert.equal(element.selectedScheme, 'repo'); done(); }); + }); - test('loads scheme from preferences', done => { - stub('gr-rest-api-interface', { - getPreferences() { - return Promise.resolve({download_scheme: 'repo'}); - }, - }); - element._loggedIn = true; - assert.isTrue(element.$.restAPI.getPreferences.called); - element.$.restAPI.getPreferences.lastCall.returnValue.then(() => { - assert.equal(element.selectedScheme, 'repo'); - done(); - }); + test('normalize scheme from preferences', done => { + stub('gr-rest-api-interface', { + getPreferences() { + return Promise.resolve({download_scheme: 'REPO'}); + }, }); - - test('normalize scheme from preferences', done => { - stub('gr-rest-api-interface', { - getPreferences() { - return Promise.resolve({download_scheme: 'REPO'}); - }, - }); - element._loggedIn = true; - element.$.restAPI.getPreferences.lastCall.returnValue.then(() => { - assert.equal(element.selectedScheme, 'repo'); - done(); - }); - }); - - test('saves scheme to preferences', () => { - element._loggedIn = true; - const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences', - () => Promise.resolve()); - - flushAsynchronousOperations(); - - const repoTab = element.shadowRoot - .querySelector('paper-tab[data-scheme="repo"]'); - - MockInteractions.tap(repoTab); - - assert.isTrue(savePrefsStub.called); - assert.equal(savePrefsStub.lastCall.args[0].download_scheme, - repoTab.getAttribute('data-scheme')); + element._loggedIn = true; + element.$.restAPI.getPreferences.lastCall.returnValue.then(() => { + assert.equal(element.selectedScheme, 'repo'); + done(); }); }); + + test('saves scheme to preferences', () => { + element._loggedIn = true; + const savePrefsStub = sandbox.stub(element.$.restAPI, 'savePreferences', + () => Promise.resolve()); + + flushAsynchronousOperations(); + + const repoTab = element.shadowRoot + .querySelector('paper-tab[data-scheme="repo"]'); + + MockInteractions.tap(repoTab); + + assert.isTrue(savePrefsStub.called); + assert.equal(savePrefsStub.lastCall.args[0].download_scheme, + repoTab.getAttribute('data-scheme')); + }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js index 06b4a72..6b250de 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list.js
@@ -14,123 +14,135 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-dropdown/iron-dropdown.js'; +import '@polymer/paper-item/paper-item.js'; +import '@polymer/paper-listbox/paper-listbox.js'; +import '../../../styles/shared-styles.js'; +import '../gr-button/gr-button.js'; +import '../gr-date-formatter/gr-date-formatter.js'; +import '../gr-select/gr-select.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-dropdown-list_html.js'; + +/** + * fired when the selected value of the dropdown changes + * + * @event {change} + */ + +const Defs = {}; + +/** + * Requred values are text and value. mobileText and triggerText will + * fall back to text if not provided. + * + * If bottomText is not provided, nothing will display on the second + * line. + * + * If date is not provided, nothing will be displayed in its place. + * + * @typedef {{ + * text: string, + * value: (string|number), + * bottomText: (string|undefined), + * triggerText: (string|undefined), + * mobileText: (string|undefined), + * date: (!Date|undefined), + * }} + */ +Defs.item; + +/** @extends Polymer.Element */ +class GrDropdownList extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-dropdown-list'; } /** - * fired when the selected value of the dropdown changes + * Fired when the selected value changes * - * @event {change} + * @event value-change + * + * @property {string|number} value */ - const Defs = {}; - - /** - * Requred values are text and value. mobileText and triggerText will - * fall back to text if not provided. - * - * If bottomText is not provided, nothing will display on the second - * line. - * - * If date is not provided, nothing will be displayed in its place. - * - * @typedef {{ - * text: string, - * value: (string|number), - * bottomText: (string|undefined), - * triggerText: (string|undefined), - * mobileText: (string|undefined), - * date: (!Date|undefined), - * }} - */ - Defs.item; - - /** @extends Polymer.Element */ - class GrDropdownList extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-dropdown-list'; } - /** - * Fired when the selected value changes - * - * @event value-change - * - * @property {string|number} value - */ - - static get properties() { - return { - initialCount: Number, - /** @type {!Array<!Defs.item>} */ - items: Object, - text: String, - disabled: { - type: Boolean, - value: false, - }, - value: { - type: String, - notify: true, - }, - }; - } - - static get observers() { - return [ - '_handleValueChange(value, items)', - ]; - } - - /** - * Handle a click on the iron-dropdown element. - * - * @param {!Event} e - */ - _handleDropdownClick(e) { - // async is needed so that that the click event is fired before the - // dropdown closes (This was a bug for touch devices). - this.async(() => { - this.$.dropdown.close(); - }, 1); - } - - /** - * Handle a click on the button to open the dropdown. - * - * @param {!Event} e - */ - _showDropdownTapHandler(e) { - this._open(); - } - - /** - * Open the dropdown. - */ - _open() { - this.$.dropdown.open(); - } - - _computeMobileText(item) { - return item.mobileText ? item.mobileText : item.text; - } - - _handleValueChange(value, items) { - // Polymer 2: check for undefined - if ([value, items].some(arg => arg === undefined)) { - return; - } - - if (!value) { return; } - const selectedObj = items.find(item => item.value + '' === value + ''); - if (!selectedObj) { return; } - this.text = selectedObj.triggerText? selectedObj.triggerText : - selectedObj.text; - this.dispatchEvent(new CustomEvent('value-change', { - detail: {value}, - bubbles: false, - })); - } + static get properties() { + return { + initialCount: Number, + /** @type {!Array<!Defs.item>} */ + items: Object, + text: String, + disabled: { + type: Boolean, + value: false, + }, + value: { + type: String, + notify: true, + }, + }; } - customElements.define(GrDropdownList.is, GrDropdownList); -})(); + static get observers() { + return [ + '_handleValueChange(value, items)', + ]; + } + + /** + * Handle a click on the iron-dropdown element. + * + * @param {!Event} e + */ + _handleDropdownClick(e) { + // async is needed so that that the click event is fired before the + // dropdown closes (This was a bug for touch devices). + this.async(() => { + this.$.dropdown.close(); + }, 1); + } + + /** + * Handle a click on the button to open the dropdown. + * + * @param {!Event} e + */ + _showDropdownTapHandler(e) { + this._open(); + } + + /** + * Open the dropdown. + */ + _open() { + this.$.dropdown.open(); + } + + _computeMobileText(item) { + return item.mobileText ? item.mobileText : item.text; + } + + _handleValueChange(value, items) { + // Polymer 2: check for undefined + if ([value, items].some(arg => arg === undefined)) { + return; + } + + if (!value) { return; } + const selectedObj = items.find(item => item.value + '' === value + ''); + if (!selectedObj) { return; } + this.text = selectedObj.triggerText? selectedObj.triggerText : + selectedObj.text; + this.dispatchEvent(new CustomEvent('value-change', { + detail: {value}, + bubbles: false, + })); + } +} + +customElements.define(GrDropdownList.is, GrDropdownList);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js index 7586876..3b454c2 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_html.js
@@ -1,33 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="/bower_components/paper-item/paper-item.html"> -<link rel="import" href="/bower_components/paper-listbox/paper-listbox.html"> - -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html"> -<link rel="import" href="../../shared/gr-select/gr-select.html"> - - -<dom-module id="gr-dropdown-list"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: inline-block; @@ -128,38 +117,17 @@ } } </style> - <gr-button - disabled="[[disabled]]" - down-arrow - link - id="trigger" - class="dropdown-trigger" - on-click="_showDropdownTapHandler" - slot="dropdown-trigger"> + <gr-button disabled="[[disabled]]" down-arrow="" link="" id="trigger" class="dropdown-trigger" on-click="_showDropdownTapHandler" slot="dropdown-trigger"> <span id="triggerText">[[text]]</span> </gr-button> - <iron-dropdown - id="dropdown" - vertical-align="top" - allow-outside-scroll="true" - on-click="_handleDropdownClick"> - <paper-listbox - class="dropdown-content" - slot="dropdown-content" - attr-for-selected="data-value" - selected="{{value}}" - on-tap="_handleDropdownTap"> - <template is="dom-repeat" - items="[[items]]" - initial-count="[[initialCount]]"> - <paper-item - disabled="[[item.disabled]]" - data-value$="[[item.value]]"> + <iron-dropdown id="dropdown" vertical-align="top" allow-outside-scroll="true" on-click="_handleDropdownClick"> + <paper-listbox class="dropdown-content" slot="dropdown-content" attr-for-selected="data-value" selected="{{value}}" on-tap="_handleDropdownTap"> + <template is="dom-repeat" items="[[items]]" initial-count="[[initialCount]]"> + <paper-item disabled="[[item.disabled]]" data-value\$="[[item.value]]"> <div class="topContent"> <div>[[item.text]]</div> <template is="dom-if" if="[[item.date]]"> - <gr-date-formatter - date-str="[[item.date]]"></gr-date-formatter> + <gr-date-formatter date-str="[[item.date]]"></gr-date-formatter> </template> </div> <template is="dom-if" if="[[item.bottomText]]"> @@ -174,14 +142,10 @@ <gr-select bind-value="{{value}}"> <select> <template is="dom-repeat" items="[[items]]"> - <option - disabled$="[[item.disabled]]" - value="[[item.value]]"> + <option disabled\$="[[item.disabled]]" value="[[item.value]]"> [[_computeMobileText(item)]] </option> </template> </select> </gr-select> - </template> - <script src="gr-dropdown-list.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html index 40e43fd..b3fda65 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dropdown-list</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-dropdown-list.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-dropdown-list.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dropdown-list.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,140 +40,143 @@ </template> </test-fixture> -<script> - suite('gr-dropdown-list tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dropdown-list.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-dropdown-list tests', () => { + let element; + let sandbox; - setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - sandbox = sinon.sandbox.create(); + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('tap on trigger opens menu', () => { - sandbox.stub(element, '_open', () => { element.$.dropdown.open(); }); - assert.isFalse(element.$.dropdown.opened); - MockInteractions.tap(element.$.trigger); - assert.isTrue(element.$.dropdown.opened); - }); + test('tap on trigger opens menu', () => { + sandbox.stub(element, '_open', () => { element.$.dropdown.open(); }); + assert.isFalse(element.$.dropdown.opened); + MockInteractions.tap(element.$.trigger); + assert.isTrue(element.$.dropdown.opened); + }); - test('_computeMobileText', () => { - const item = { + test('_computeMobileText', () => { + const item = { + value: 1, + text: 'text', + }; + assert.equal(element._computeMobileText(item), item.text); + item.mobileText = 'mobile text'; + assert.equal(element._computeMobileText(item), item.mobileText); + }); + + test('options are selected and laid out correctly', done => { + element.value = 2; + element.items = [ + { value: 1, - text: 'text', - }; - assert.equal(element._computeMobileText(item), item.text); - item.mobileText = 'mobile text'; - assert.equal(element._computeMobileText(item), item.mobileText); - }); + text: 'Top Text 1', + }, + { + value: 2, + bottomText: 'Bottom Text 2', + triggerText: 'Button Text 2', + text: 'Top Text 2', + mobileText: 'Mobile Text 2', + }, + { + value: 3, + disabled: true, + bottomText: 'Bottom Text 3', + triggerText: 'Button Text 3', + date: '2017-08-18 23:11:42.569000000', + text: 'Top Text 3', + mobileText: 'Mobile Text 3', + }, + ]; + assert.equal(element.shadowRoot + .querySelector('paper-listbox').selected, element.value); + assert.equal(element.text, 'Button Text 2'); + flush(() => { + const items = dom(element.root).querySelectorAll('paper-item'); + const mobileItems = dom(element.root).querySelectorAll('option'); + assert.equal(items.length, 3); + assert.equal(mobileItems.length, 3); - test('options are selected and laid out correctly', done => { - element.value = 2; - element.items = [ - { - value: 1, - text: 'Top Text 1', - }, - { - value: 2, - bottomText: 'Bottom Text 2', - triggerText: 'Button Text 2', - text: 'Top Text 2', - mobileText: 'Mobile Text 2', - }, - { - value: 3, - disabled: true, - bottomText: 'Bottom Text 3', - triggerText: 'Button Text 3', - date: '2017-08-18 23:11:42.569000000', - text: 'Top Text 3', - mobileText: 'Mobile Text 3', - }, - ]; - assert.equal(element.shadowRoot - .querySelector('paper-listbox').selected, element.value); - assert.equal(element.text, 'Button Text 2'); - flush(() => { - const items = Polymer.dom(element.root).querySelectorAll('paper-item'); - const mobileItems = Polymer.dom(element.root).querySelectorAll('option'); - assert.equal(items.length, 3); - assert.equal(mobileItems.length, 3); + // First Item + // The first item should be disabled, has no bottom text, and no date. + assert.isFalse(!!items[0].disabled); + assert.isFalse(mobileItems[0].disabled); + assert.isFalse(items[0].classList.contains('iron-selected')); + assert.isFalse(mobileItems[0].selected); - // First Item - // The first item should be disabled, has no bottom text, and no date. - assert.isFalse(!!items[0].disabled); - assert.isFalse(mobileItems[0].disabled); - assert.isFalse(items[0].classList.contains('iron-selected')); - assert.isFalse(mobileItems[0].selected); + assert.isNotOk(dom(items[0]).querySelector('gr-date-formatter')); + assert.isNotOk(dom(items[0]).querySelector('.bottomContent')); + assert.equal(items[0].dataset.value, element.items[0].value); + assert.equal(mobileItems[0].value, element.items[0].value); + assert.equal(dom(items[0]).querySelector('.topContent div') + .innerText, element.items[0].text); - assert.isNotOk(Polymer.dom(items[0]).querySelector('gr-date-formatter')); - assert.isNotOk(Polymer.dom(items[0]).querySelector('.bottomContent')); - assert.equal(items[0].dataset.value, element.items[0].value); - assert.equal(mobileItems[0].value, element.items[0].value); - assert.equal(Polymer.dom(items[0]).querySelector('.topContent div') - .innerText, element.items[0].text); + // Since no mobile specific text, it should fall back to text. + assert.equal(mobileItems[0].text, element.items[0].text); - // Since no mobile specific text, it should fall back to text. - assert.equal(mobileItems[0].text, element.items[0].text); + // Second Item + // The second item should have top text, bottom text, and no date. + assert.isFalse(!!items[1].disabled); + assert.isFalse(mobileItems[1].disabled); + assert.isTrue(items[1].classList.contains('iron-selected')); + assert.isTrue(mobileItems[1].selected); - // Second Item - // The second item should have top text, bottom text, and no date. - assert.isFalse(!!items[1].disabled); - assert.isFalse(mobileItems[1].disabled); - assert.isTrue(items[1].classList.contains('iron-selected')); - assert.isTrue(mobileItems[1].selected); + assert.isNotOk(dom(items[1]).querySelector('gr-date-formatter')); + assert.isOk(dom(items[1]).querySelector('.bottomContent')); + assert.equal(items[1].dataset.value, element.items[1].value); + assert.equal(mobileItems[1].value, element.items[1].value); + assert.equal(dom(items[1]).querySelector('.topContent div') + .innerText, element.items[1].text); - assert.isNotOk(Polymer.dom(items[1]).querySelector('gr-date-formatter')); - assert.isOk(Polymer.dom(items[1]).querySelector('.bottomContent')); - assert.equal(items[1].dataset.value, element.items[1].value); - assert.equal(mobileItems[1].value, element.items[1].value); - assert.equal(Polymer.dom(items[1]).querySelector('.topContent div') - .innerText, element.items[1].text); + // Since there is mobile specific text, it should that. + assert.equal(mobileItems[1].text, element.items[1].mobileText); - // Since there is mobile specific text, it should that. - assert.equal(mobileItems[1].text, element.items[1].mobileText); + // Since this item is selected, and it has triggerText defined, that + // should be used. + assert.equal(element.text, element.items[1].triggerText); - // Since this item is selected, and it has triggerText defined, that - // should be used. - assert.equal(element.text, element.items[1].triggerText); + // Third item + // The third item should be disabled, and have a date, and bottom content. + assert.isTrue(!!items[2].disabled); + assert.isTrue(mobileItems[2].disabled); + assert.isFalse(items[2].classList.contains('iron-selected')); + assert.isFalse(mobileItems[2].selected); - // Third item - // The third item should be disabled, and have a date, and bottom content. - assert.isTrue(!!items[2].disabled); - assert.isTrue(mobileItems[2].disabled); - assert.isFalse(items[2].classList.contains('iron-selected')); - assert.isFalse(mobileItems[2].selected); + assert.isOk(dom(items[2]).querySelector('gr-date-formatter')); + assert.isOk(dom(items[2]).querySelector('.bottomContent')); + assert.equal(items[2].dataset.value, element.items[2].value); + assert.equal(mobileItems[2].value, element.items[2].value); + assert.equal(dom(items[2]).querySelector('.topContent div') + .innerText, element.items[2].text); - assert.isOk(Polymer.dom(items[2]).querySelector('gr-date-formatter')); - assert.isOk(Polymer.dom(items[2]).querySelector('.bottomContent')); - assert.equal(items[2].dataset.value, element.items[2].value); - assert.equal(mobileItems[2].value, element.items[2].value); - assert.equal(Polymer.dom(items[2]).querySelector('.topContent div') - .innerText, element.items[2].text); + // Since there is mobile specific text, it should that. + assert.equal(mobileItems[2].text, element.items[2].mobileText); - // Since there is mobile specific text, it should that. - assert.equal(mobileItems[2].text, element.items[2].mobileText); + // Select a new item. + MockInteractions.tap(items[0]); + flushAsynchronousOperations(); + assert.equal(element.value, 1); + assert.isTrue(items[0].classList.contains('iron-selected')); + assert.isTrue(mobileItems[0].selected); - // Select a new item. - MockInteractions.tap(items[0]); - flushAsynchronousOperations(); - assert.equal(element.value, 1); - assert.isTrue(items[0].classList.contains('iron-selected')); - assert.isTrue(mobileItems[0].selected); - - // Since no triggerText, the fallback is used. - assert.equal(element.text, element.items[0].text); - done(); - }); + // Since no triggerText, the fallback is used. + assert.equal(element.text, element.items[0].text); + done(); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js index 531f2e3..b4190dc 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown.js
@@ -14,309 +14,324 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; - const REL_NOOPENER = 'noopener'; - const REL_EXTERNAL = 'external'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../../../scripts/bundled-polymer.js'; +import '@polymer/iron-dropdown/iron-dropdown.js'; +import '../gr-button/gr-button.js'; +import '../gr-cursor-manager/gr-cursor-manager.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import '../gr-tooltip-content/gr-tooltip-content.js'; +import '../../../styles/shared-styles.js'; +import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-dropdown_html.js'; + +const REL_NOOPENER = 'noopener'; +const REL_EXTERNAL = 'external'; + +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrDropdown extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-dropdown'; } + /** + * Fired when a non-link dropdown item with the given ID is tapped. + * + * @event tap-item-<id> + */ /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * Fired when a non-link dropdown item is tapped. + * + * @event tap-item */ - class GrDropdown extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-dropdown'; } - /** - * Fired when a non-link dropdown item with the given ID is tapped. - * - * @event tap-item-<id> - */ - /** - * Fired when a non-link dropdown item is tapped. - * - * @event tap-item - */ + static get properties() { + return { + items: { + type: Array, + observer: '_resetCursorStops', + }, + downArrow: Boolean, + topContent: Object, + horizontalAlign: { + type: String, + value: 'left', + }, - static get properties() { - return { - items: { - type: Array, - observer: '_resetCursorStops', - }, - downArrow: Boolean, - topContent: Object, - horizontalAlign: { - type: String, - value: 'left', - }, + /** + * Style the dropdown trigger as a link (rather than a button). + */ + link: { + type: Boolean, + value: false, + }, - /** - * Style the dropdown trigger as a link (rather than a button). - */ - link: { - type: Boolean, - value: false, - }, + verticalOffset: { + type: Number, + value: 40, + }, - verticalOffset: { - type: Number, - value: 40, - }, + /** + * List the IDs of dropdown buttons to be disabled. (Note this only + * diisables bittons and not link entries.) + */ + disabledIds: { + type: Array, + value() { return []; }, + }, - /** - * List the IDs of dropdown buttons to be disabled. (Note this only - * diisables bittons and not link entries.) - */ - disabledIds: { - type: Array, - value() { return []; }, - }, + /** + * The elements of the list. + */ + _listElements: { + type: Array, + value() { return []; }, + }, + }; + } - /** - * The elements of the list. - */ - _listElements: { - type: Array, - value() { return []; }, - }, - }; - } + get keyBindings() { + return { + 'down': '_handleDown', + 'enter space': '_handleEnter', + 'tab': '_handleTab', + 'up': '_handleUp', + }; + } - get keyBindings() { - return { - 'down': '_handleDown', - 'enter space': '_handleEnter', - 'tab': '_handleTab', - 'up': '_handleUp', - }; - } - - /** - * Handle the up key. - * - * @param {!Event} e - */ - _handleUp(e) { - if (this.$.dropdown.opened) { - e.preventDefault(); - e.stopPropagation(); - this.$.cursor.previous(); - } else { - this._open(); - } - } - - /** - * Handle the down key. - * - * @param {!Event} e - */ - _handleDown(e) { - if (this.$.dropdown.opened) { - e.preventDefault(); - e.stopPropagation(); - this.$.cursor.next(); - } else { - this._open(); - } - } - - /** - * Handle the tab key. - * - * @param {!Event} e - */ - _handleTab(e) { - if (this.$.dropdown.opened) { - // Tab in a native select is a no-op. Emulate this. - e.preventDefault(); - e.stopPropagation(); - } - } - - /** - * Handle the enter key. - * - * @param {!Event} e - */ - _handleEnter(e) { + /** + * Handle the up key. + * + * @param {!Event} e + */ + _handleUp(e) { + if (this.$.dropdown.opened) { e.preventDefault(); e.stopPropagation(); - if (this.$.dropdown.opened) { - // TODO(milutin): This solution is not particularly robust in general. - // Since gr-tooltip-content click on shadow dom is not propagated down, - // we have to target `a` inside it. - const el = this.$.cursor.target.querySelector(':not([hidden]) a'); - if (el) { el.click(); } - } else { - this._open(); - } - } - - /** - * Handle a click on the iron-dropdown element. - * - * @param {!Event} e - */ - _handleDropdownClick(e) { - this._close(); - } - - /** - * Hanlde a click on the button to open the dropdown. - * - * @param {!Event} e - */ - _dropdownTriggerTapHandler(e) { - e.preventDefault(); - e.stopPropagation(); - if (this.$.dropdown.opened) { - this._close(); - } else { - this._open(); - } - } - - /** - * Open the dropdown and initialize the cursor. - */ - _open() { - this.$.dropdown.open(); - this._resetCursorStops(); - this.$.cursor.setCursorAtIndex(0); - this.$.cursor.target.focus(); - } - - _close() { - // async is needed so that that the click event is fired before the - // dropdown closes (This was a bug for touch devices). - this.async(() => { - this.$.dropdown.close(); - }, 1); - } - - /** - * Get the class for a top-content item based on the given boolean. - * - * @param {boolean} bold Whether the item is bold. - * @return {string} The class for the top-content item. - */ - _getClassIfBold(bold) { - return bold ? 'bold-text' : ''; - } - - /** - * Build a URL for the given host and path. The base URL will be only added, - * if it is not already included in the path. - * - * @param {!string} host - * @param {!string} path - * @return {!string} The scheme-relative URL. - */ - _computeURLHelper(host, path) { - const base = path.startsWith(this.getBaseUrl()) ? - '' : this.getBaseUrl(); - return '//' + host + base + path; - } - - /** - * Build a scheme-relative URL for the current host. Will include the base - * URL if one is present. Note: the URL will be scheme-relative but absolute - * with regard to the host. - * - * @param {!string} path The path for the URL. - * @return {!string} The scheme-relative URL. - */ - _computeRelativeURL(path) { - const host = window.location.host; - return this._computeURLHelper(host, path); - } - - /** - * Compute the URL for a link object. - * - * @param {!Object} link The object describing the link. - * @return {!string} The URL. - */ - _computeLinkURL(link) { - if (typeof link.url === 'undefined') { - return ''; - } - if (link.target || !link.url.startsWith('/')) { - return link.url; - } - return this._computeRelativeURL(link.url); - } - - /** - * Compute the value for the rel attribute of an anchor for the given link - * object. If the link has a target value, then the rel must be "noopener" - * for security reasons. - * - * @param {!Object} link The object describing the link. - * @return {?string} The rel value for the link. - */ - _computeLinkRel(link) { - // Note: noopener takes precedence over external. - if (link.target) { return REL_NOOPENER; } - if (link.external) { return REL_EXTERNAL; } - return null; - } - - /** - * Handle a click on an item of the dropdown. - * - * @param {!Event} e - */ - _handleItemTap(e) { - const id = e.target.getAttribute('data-id'); - const item = this.items.find(item => item.id === id); - if (id && !this.disabledIds.includes(id)) { - if (item) { - this.dispatchEvent(new CustomEvent('tap-item', {detail: item})); - } - this.dispatchEvent(new CustomEvent('tap-item-' + id)); - } - } - - /** - * If a dropdown item is shown as a button, get the class for the button. - * - * @param {string} id - * @param {!Object} disabledIdsRecord The change record for the disabled IDs - * list. - * @return {!string} The class for the item button. - */ - _computeDisabledClass(id, disabledIdsRecord) { - return disabledIdsRecord.base.includes(id) ? 'disabled' : ''; - } - - /** - * Recompute the stops for the dropdown item cursor. - */ - _resetCursorStops() { - if (this.items && this.items.length > 0 && this.$.dropdown.opened) { - Polymer.dom.flush(); - this._listElements = Array.from( - Polymer.dom(this.root).querySelectorAll('li')); - } - } - - _computeHasTooltip(tooltip) { - return !!tooltip; - } - - _computeIsDownload(link) { - return !!link.download; + this.$.cursor.previous(); + } else { + this._open(); } } - customElements.define(GrDropdown.is, GrDropdown); -})(); + /** + * Handle the down key. + * + * @param {!Event} e + */ + _handleDown(e) { + if (this.$.dropdown.opened) { + e.preventDefault(); + e.stopPropagation(); + this.$.cursor.next(); + } else { + this._open(); + } + } + + /** + * Handle the tab key. + * + * @param {!Event} e + */ + _handleTab(e) { + if (this.$.dropdown.opened) { + // Tab in a native select is a no-op. Emulate this. + e.preventDefault(); + e.stopPropagation(); + } + } + + /** + * Handle the enter key. + * + * @param {!Event} e + */ + _handleEnter(e) { + e.preventDefault(); + e.stopPropagation(); + if (this.$.dropdown.opened) { + // TODO(milutin): This solution is not particularly robust in general. + // Since gr-tooltip-content click on shadow dom is not propagated down, + // we have to target `a` inside it. + const el = this.$.cursor.target.querySelector(':not([hidden]) a'); + if (el) { el.click(); } + } else { + this._open(); + } + } + + /** + * Handle a click on the iron-dropdown element. + * + * @param {!Event} e + */ + _handleDropdownClick(e) { + this._close(); + } + + /** + * Hanlde a click on the button to open the dropdown. + * + * @param {!Event} e + */ + _dropdownTriggerTapHandler(e) { + e.preventDefault(); + e.stopPropagation(); + if (this.$.dropdown.opened) { + this._close(); + } else { + this._open(); + } + } + + /** + * Open the dropdown and initialize the cursor. + */ + _open() { + this.$.dropdown.open(); + this._resetCursorStops(); + this.$.cursor.setCursorAtIndex(0); + this.$.cursor.target.focus(); + } + + _close() { + // async is needed so that that the click event is fired before the + // dropdown closes (This was a bug for touch devices). + this.async(() => { + this.$.dropdown.close(); + }, 1); + } + + /** + * Get the class for a top-content item based on the given boolean. + * + * @param {boolean} bold Whether the item is bold. + * @return {string} The class for the top-content item. + */ + _getClassIfBold(bold) { + return bold ? 'bold-text' : ''; + } + + /** + * Build a URL for the given host and path. The base URL will be only added, + * if it is not already included in the path. + * + * @param {!string} host + * @param {!string} path + * @return {!string} The scheme-relative URL. + */ + _computeURLHelper(host, path) { + const base = path.startsWith(this.getBaseUrl()) ? + '' : this.getBaseUrl(); + return '//' + host + base + path; + } + + /** + * Build a scheme-relative URL for the current host. Will include the base + * URL if one is present. Note: the URL will be scheme-relative but absolute + * with regard to the host. + * + * @param {!string} path The path for the URL. + * @return {!string} The scheme-relative URL. + */ + _computeRelativeURL(path) { + const host = window.location.host; + return this._computeURLHelper(host, path); + } + + /** + * Compute the URL for a link object. + * + * @param {!Object} link The object describing the link. + * @return {!string} The URL. + */ + _computeLinkURL(link) { + if (typeof link.url === 'undefined') { + return ''; + } + if (link.target || !link.url.startsWith('/')) { + return link.url; + } + return this._computeRelativeURL(link.url); + } + + /** + * Compute the value for the rel attribute of an anchor for the given link + * object. If the link has a target value, then the rel must be "noopener" + * for security reasons. + * + * @param {!Object} link The object describing the link. + * @return {?string} The rel value for the link. + */ + _computeLinkRel(link) { + // Note: noopener takes precedence over external. + if (link.target) { return REL_NOOPENER; } + if (link.external) { return REL_EXTERNAL; } + return null; + } + + /** + * Handle a click on an item of the dropdown. + * + * @param {!Event} e + */ + _handleItemTap(e) { + const id = e.target.getAttribute('data-id'); + const item = this.items.find(item => item.id === id); + if (id && !this.disabledIds.includes(id)) { + if (item) { + this.dispatchEvent(new CustomEvent('tap-item', {detail: item})); + } + this.dispatchEvent(new CustomEvent('tap-item-' + id)); + } + } + + /** + * If a dropdown item is shown as a button, get the class for the button. + * + * @param {string} id + * @param {!Object} disabledIdsRecord The change record for the disabled IDs + * list. + * @return {!string} The class for the item button. + */ + _computeDisabledClass(id, disabledIdsRecord) { + return disabledIdsRecord.base.includes(id) ? 'disabled' : ''; + } + + /** + * Recompute the stops for the dropdown item cursor. + */ + _resetCursorStops() { + if (this.items && this.items.length > 0 && this.$.dropdown.opened) { + flush(); + this._listElements = Array.from( + dom(this.root).querySelectorAll('li')); + } + } + + _computeHasTooltip(tooltip) { + return !!tooltip; + } + + _computeIsDownload(link) { + return !!link.download; + } +} + +customElements.define(GrDropdown.is, GrDropdown);
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js index 5d28390..99028af 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_html.js
@@ -1,32 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> -<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-dropdown"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: inline-block; @@ -97,72 +87,32 @@ font-weight: var(--font-weight-bold); } </style> - <gr-button - link="[[link]]" - class="dropdown-trigger" id="trigger" - down-arrow="[[downArrow]]" - on-click="_dropdownTriggerTapHandler"> + <gr-button link="[[link]]" class="dropdown-trigger" id="trigger" down-arrow="[[downArrow]]" on-click="_dropdownTriggerTapHandler"> <slot></slot> </gr-button> - <iron-dropdown id="dropdown" - vertical-align="top" - vertical-offset="[[verticalOffset]]" - allow-outside-scroll="true" - horizontal-align="[[horizontalAlign]]" - on-click="_handleDropdownClick"> + <iron-dropdown id="dropdown" vertical-align="top" vertical-offset="[[verticalOffset]]" allow-outside-scroll="true" horizontal-align="[[horizontalAlign]]" on-click="_handleDropdownClick"> <div class="dropdown-content" slot="dropdown-content"> <ul> <template is="dom-if" if="[[topContent]]"> <div class="topContent"> - <template - is="dom-repeat" - items="[[topContent]]" - as="item" - initial-count="75"> - <div - class$="[[_getClassIfBold(item.bold)]] top-item" - tabindex="-1"> + <template is="dom-repeat" items="[[topContent]]" as="item" initial-count="75"> + <div class\$="[[_getClassIfBold(item.bold)]] top-item" tabindex="-1"> [[item.text]] </div> </template> </div> </template> - <template - is="dom-repeat" - items="[[items]]" - as="link" - initial-count="75"> + <template is="dom-repeat" items="[[items]]" as="link" initial-count="75"> <li tabindex="-1"> - <gr-tooltip-content - has-tooltip="[[_computeHasTooltip(link.tooltip)]]" - title$="[[link.tooltip]]"> - <span - class$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]" - data-id$="[[link.id]]" - on-click="_handleItemTap" - hidden$="[[link.url]]" - tabindex="-1">[[link.name]]</span> - <a - class="itemAction" - href$="[[_computeLinkURL(link)]]" - download$="[[_computeIsDownload(link)]]" - rel$="[[_computeLinkRel(link)]]" - target$="[[link.target]]" - hidden$="[[!link.url]]" - tabindex="-1">[[link.name]]</a> + <gr-tooltip-content has-tooltip="[[_computeHasTooltip(link.tooltip)]]" title\$="[[link.tooltip]]"> + <span class\$="itemAction [[_computeDisabledClass(link.id, disabledIds.*)]]" data-id\$="[[link.id]]" on-click="_handleItemTap" hidden\$="[[link.url]]" tabindex="-1">[[link.name]]</span> + <a class="itemAction" href\$="[[_computeLinkURL(link)]]" download\$="[[_computeIsDownload(link)]]" rel\$="[[_computeLinkRel(link)]]" target\$="[[link.target]]" hidden\$="[[!link.url]]" tabindex="-1">[[link.name]]</a> </gr-tooltip-content> </li> </template> </ul> </div> </iron-dropdown> - <gr-cursor-manager - id="cursor" - cursor-target-class="selected" - scroll-behavior="never" - focus-on-move - stops="[[_listElements]]"></gr-cursor-manager> + <gr-cursor-manager id="cursor" cursor-target-class="selected" scroll-behavior="never" focus-on-move="" stops="[[_listElements]]"></gr-cursor-manager> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-dropdown.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html index 2d7f090..dcbab4d 100644 --- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html +++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-dropdown</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-dropdown.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-dropdown.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dropdown.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,176 +40,179 @@ </template> </test-fixture> -<script> - suite('gr-dropdown tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-dropdown.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-dropdown tests', () => { + let element; + let sandbox; + setup(() => { + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + }); + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeIsDownload', () => { + assert.isTrue(element._computeIsDownload({download: true})); + assert.isFalse(element._computeIsDownload({download: false})); + }); + + test('tap on trigger opens menu, then closes', () => { + sandbox.stub(element, '_open', () => { element.$.dropdown.open(); }); + sandbox.stub(element, '_close', () => { element.$.dropdown.close(); }); + assert.isFalse(element.$.dropdown.opened); + MockInteractions.tap(element.$.trigger); + assert.isTrue(element.$.dropdown.opened); + MockInteractions.tap(element.$.trigger); + assert.isFalse(element.$.dropdown.opened); + }); + + test('_computeURLHelper', () => { + const path = '/test'; + const host = 'http://www.testsite.com'; + const computedPath = element._computeURLHelper(host, path); + assert.equal(computedPath, '//http://www.testsite.com/test'); + }); + + test('link URLs', () => { + assert.equal( + element._computeLinkURL({url: 'http://example.com/test'}), + 'http://example.com/test'); + assert.equal( + element._computeLinkURL({url: 'https://example.com/test'}), + 'https://example.com/test'); + assert.equal( + element._computeLinkURL({url: '/test'}), + '//' + window.location.host + '/test'); + assert.equal( + element._computeLinkURL({url: '/test', target: '_blank'}), + '/test'); + }); + + test('link rel', () => { + let link = {url: '/test'}; + assert.isNull(element._computeLinkRel(link)); + + link = {url: '/test', target: '_blank'}; + assert.equal(element._computeLinkRel(link), 'noopener'); + + link = {url: '/test', external: true}; + assert.equal(element._computeLinkRel(link), 'external'); + + link = {url: '/test', target: '_blank', external: true}; + assert.equal(element._computeLinkRel(link), 'noopener'); + }); + + test('_getClassIfBold', () => { + let bold = true; + assert.equal(element._getClassIfBold(bold), 'bold-text'); + + bold = false; + assert.equal(element._getClassIfBold(bold), ''); + }); + + test('Top text exists and is bolded correctly', () => { + element.topContent = [{text: 'User', bold: true}, {text: 'email'}]; + flushAsynchronousOperations(); + const topItems = dom(element.root).querySelectorAll('.top-item'); + assert.equal(topItems.length, 2); + assert.isTrue(topItems[0].classList.contains('bold-text')); + assert.isFalse(topItems[1].classList.contains('bold-text')); + }); + + test('non link items', () => { + const item0 = {name: 'item one', id: 'foo'}; + element.items = [item0, {name: 'item two', id: 'bar'}]; + const fooTapped = sandbox.stub(); + const tapped = sandbox.stub(); + element.addEventListener('tap-item-foo', fooTapped); + element.addEventListener('tap-item', tapped); + flushAsynchronousOperations(); + MockInteractions.tap(element.shadowRoot + .querySelector('.itemAction')); + assert.isTrue(fooTapped.called); + assert.isTrue(tapped.called); + assert.deepEqual(tapped.lastCall.args[0].detail, item0); + }); + + test('disabled non link item', () => { + element.items = [{name: 'item one', id: 'foo'}]; + element.disabledIds = ['foo']; + + const stub = sandbox.stub(); + const tapped = sandbox.stub(); + element.addEventListener('tap-item-foo', stub); + element.addEventListener('tap-item', tapped); + flushAsynchronousOperations(); + MockInteractions.tap(element.shadowRoot + .querySelector('.itemAction')); + assert.isFalse(stub.called); + assert.isFalse(tapped.called); + }); + + test('properly sets tooltips', () => { + element.items = [ + {name: 'item one', id: 'foo', tooltip: 'hello'}, + {name: 'item two', id: 'bar'}, + ]; + element.disabledIds = []; + flushAsynchronousOperations(); + const tooltipContents = dom(element.root) + .querySelectorAll('iron-dropdown li gr-tooltip-content'); + assert.equal(tooltipContents.length, 2); + assert.isTrue(tooltipContents[0].hasTooltip); + assert.equal(tooltipContents[0].getAttribute('title'), 'hello'); + assert.isFalse(tooltipContents[1].hasTooltip); + }); + + suite('keyboard navigation', () => { setup(() => { - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - }); - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('_computeIsDownload', () => { - assert.isTrue(element._computeIsDownload({download: true})); - assert.isFalse(element._computeIsDownload({download: false})); - }); - - test('tap on trigger opens menu, then closes', () => { - sandbox.stub(element, '_open', () => { element.$.dropdown.open(); }); - sandbox.stub(element, '_close', () => { element.$.dropdown.close(); }); - assert.isFalse(element.$.dropdown.opened); - MockInteractions.tap(element.$.trigger); - assert.isTrue(element.$.dropdown.opened); - MockInteractions.tap(element.$.trigger); - assert.isFalse(element.$.dropdown.opened); - }); - - test('_computeURLHelper', () => { - const path = '/test'; - const host = 'http://www.testsite.com'; - const computedPath = element._computeURLHelper(host, path); - assert.equal(computedPath, '//http://www.testsite.com/test'); - }); - - test('link URLs', () => { - assert.equal( - element._computeLinkURL({url: 'http://example.com/test'}), - 'http://example.com/test'); - assert.equal( - element._computeLinkURL({url: 'https://example.com/test'}), - 'https://example.com/test'); - assert.equal( - element._computeLinkURL({url: '/test'}), - '//' + window.location.host + '/test'); - assert.equal( - element._computeLinkURL({url: '/test', target: '_blank'}), - '/test'); - }); - - test('link rel', () => { - let link = {url: '/test'}; - assert.isNull(element._computeLinkRel(link)); - - link = {url: '/test', target: '_blank'}; - assert.equal(element._computeLinkRel(link), 'noopener'); - - link = {url: '/test', external: true}; - assert.equal(element._computeLinkRel(link), 'external'); - - link = {url: '/test', target: '_blank', external: true}; - assert.equal(element._computeLinkRel(link), 'noopener'); - }); - - test('_getClassIfBold', () => { - let bold = true; - assert.equal(element._getClassIfBold(bold), 'bold-text'); - - bold = false; - assert.equal(element._getClassIfBold(bold), ''); - }); - - test('Top text exists and is bolded correctly', () => { - element.topContent = [{text: 'User', bold: true}, {text: 'email'}]; - flushAsynchronousOperations(); - const topItems = Polymer.dom(element.root).querySelectorAll('.top-item'); - assert.equal(topItems.length, 2); - assert.isTrue(topItems[0].classList.contains('bold-text')); - assert.isFalse(topItems[1].classList.contains('bold-text')); - }); - - test('non link items', () => { - const item0 = {name: 'item one', id: 'foo'}; - element.items = [item0, {name: 'item two', id: 'bar'}]; - const fooTapped = sandbox.stub(); - const tapped = sandbox.stub(); - element.addEventListener('tap-item-foo', fooTapped); - element.addEventListener('tap-item', tapped); - flushAsynchronousOperations(); - MockInteractions.tap(element.shadowRoot - .querySelector('.itemAction')); - assert.isTrue(fooTapped.called); - assert.isTrue(tapped.called); - assert.deepEqual(tapped.lastCall.args[0].detail, item0); - }); - - test('disabled non link item', () => { - element.items = [{name: 'item one', id: 'foo'}]; - element.disabledIds = ['foo']; - - const stub = sandbox.stub(); - const tapped = sandbox.stub(); - element.addEventListener('tap-item-foo', stub); - element.addEventListener('tap-item', tapped); - flushAsynchronousOperations(); - MockInteractions.tap(element.shadowRoot - .querySelector('.itemAction')); - assert.isFalse(stub.called); - assert.isFalse(tapped.called); - }); - - test('properly sets tooltips', () => { element.items = [ - {name: 'item one', id: 'foo', tooltip: 'hello'}, + {name: 'item one', id: 'foo'}, {name: 'item two', id: 'bar'}, ]; - element.disabledIds = []; flushAsynchronousOperations(); - const tooltipContents = Polymer.dom(element.root) - .querySelectorAll('iron-dropdown li gr-tooltip-content'); - assert.equal(tooltipContents.length, 2); - assert.isTrue(tooltipContents[0].hasTooltip); - assert.equal(tooltipContents[0].getAttribute('title'), 'hello'); - assert.isFalse(tooltipContents[1].hasTooltip); }); - suite('keyboard navigation', () => { - setup(() => { - element.items = [ - {name: 'item one', id: 'foo'}, - {name: 'item two', id: 'bar'}, - ]; - flushAsynchronousOperations(); - }); + test('down', () => { + const stub = sandbox.stub(element.$.cursor, 'next'); + assert.isFalse(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isTrue(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 40); + assert.isTrue(stub.called); + }); - test('down', () => { - const stub = sandbox.stub(element.$.cursor, 'next'); - assert.isFalse(element.$.dropdown.opened); - MockInteractions.pressAndReleaseKeyOn(element, 40); - assert.isTrue(element.$.dropdown.opened); - MockInteractions.pressAndReleaseKeyOn(element, 40); - assert.isTrue(stub.called); - }); + test('up', () => { + const stub = sandbox.stub(element.$.cursor, 'previous'); + assert.isFalse(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isTrue(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 38); + assert.isTrue(stub.called); + }); - test('up', () => { - const stub = sandbox.stub(element.$.cursor, 'previous'); - assert.isFalse(element.$.dropdown.opened); - MockInteractions.pressAndReleaseKeyOn(element, 38); - assert.isTrue(element.$.dropdown.opened); - MockInteractions.pressAndReleaseKeyOn(element, 38); - assert.isTrue(stub.called); - }); + test('enter/space', () => { + // Because enter and space are handled by the same fn, we need only to + // test one. + assert.isFalse(element.$.dropdown.opened); + MockInteractions.pressAndReleaseKeyOn(element, 32); // Space + assert.isTrue(element.$.dropdown.opened); - test('enter/space', () => { - // Because enter and space are handled by the same fn, we need only to - // test one. - assert.isFalse(element.$.dropdown.opened); - MockInteractions.pressAndReleaseKeyOn(element, 32); // Space - assert.isTrue(element.$.dropdown.opened); - - const el = element.$.cursor.target.querySelector(':not([hidden]) a'); - const stub = sandbox.stub(el, 'click'); - MockInteractions.pressAndReleaseKeyOn(element, 32); // Space - assert.isTrue(stub.called); - }); + const el = element.$.cursor.target.querySelector(':not([hidden]) a'); + const stub = sandbox.stub(el, 'click'); + MockInteractions.pressAndReleaseKeyOn(element, 32); // Space + assert.isTrue(stub.called); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js index 4510d3f..a417e3f 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content.js
@@ -14,152 +14,163 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const RESTORED_MESSAGE = 'Content restored from a previous edit.'; - const STORAGE_DEBOUNCE_INTERVAL_MS = 400; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-storage/gr-storage.js'; +import '../gr-button/gr-button.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-editable-content_html.js'; + +const RESTORED_MESSAGE = 'Content restored from a previous edit.'; +const STORAGE_DEBOUNCE_INTERVAL_MS = 400; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrEditableContent extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-editable-content'; } + /** + * Fired when the save button is pressed. + * + * @event editable-content-save + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when the cancel button is pressed. + * + * @event editable-content-cancel */ - class GrEditableContent extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-editable-content'; } - /** - * Fired when the save button is pressed. - * - * @event editable-content-save - */ - /** - * Fired when the cancel button is pressed. - * - * @event editable-content-cancel - */ + /** + * Fired when content is restored from storage. + * + * @event show-alert + */ - /** - * Fired when content is restored from storage. - * - * @event show-alert - */ - - static get properties() { - return { - content: { - notify: true, - type: String, - }, - disabled: { - reflectToAttribute: true, - type: Boolean, - value: false, - }, - editing: { - observer: '_editingChanged', - type: Boolean, - value: false, - }, - removeZeroWidthSpace: Boolean, - // If no storage key is provided, content is not stored. - storageKey: String, - _saveDisabled: { - computed: '_computeSaveDisabled(disabled, content, _newContent)', - type: Boolean, - value: true, - }, - _newContent: { - type: String, - observer: '_newContentChanged', - }, - }; - } - - focusTextarea() { - this.shadowRoot.querySelector('iron-autogrow-textarea').textarea.focus(); - } - - _newContentChanged(newContent, oldContent) { - if (!this.storageKey) { return; } - - this.debounce('store', () => { - if (newContent.length) { - this.$.storage.setEditableContentItem(this.storageKey, newContent); - } else { - // This does not really happen, because we don't clear newContent - // after saving (see below). So this only occurs when the user clears - // all the content in the editable textarea. But <gr-storage> cleans - // up itself after one day, so we are not so concerned about leaving - // some garbage behind. - this.$.storage.eraseEditableContentItem(this.storageKey); - } - }, STORAGE_DEBOUNCE_INTERVAL_MS); - } - - _editingChanged(editing) { - // This method is for initializing _newContent when you start editing. - // Restoring content from local storage is not perfect and has - // some issues: - // - // 1. When you start editing in multiple tabs, then we are vulnerable to - // race conditions between the tabs. - // 2. The stored content is keyed by revision, so when you upload a new - // patchset and click "reload" and then click "cancel" on the content- - // editable, then you won't be able to recover the content anymore. - // - // Because of these issues we believe that it is better to only recover - // content from local storage when you enter editing mode for the first - // time. Otherwise it is better to just keep the last editing state from - // the same session. - if (!editing || this._newContent) { - return; - } - - let content; - if (this.storageKey) { - const storedContent = - this.$.storage.getEditableContentItem(this.storageKey); - if (storedContent && storedContent.message) { - content = storedContent.message; - this.dispatchEvent(new CustomEvent('show-alert', { - detail: {message: RESTORED_MESSAGE}, - bubbles: true, - composed: true, - })); - } - } - if (!content) { - content = this.content || ''; - } - - // TODO(wyatta) switch linkify sequence, see issue 5526. - this._newContent = this.removeZeroWidthSpace ? - content.replace(/^R=\u200B/gm, 'R=') : - content; - } - - _computeSaveDisabled(disabled, content, newContent) { - return disabled || !newContent || content === newContent; - } - - _handleSave(e) { - e.preventDefault(); - this.fire('editable-content-save', {content: this._newContent}); - // It would be nice, if we would set this._newContent = undefined here, - // but we can only do that when we are sure that the save operation has - // succeeded. - } - - _handleCancel(e) { - e.preventDefault(); - this.editing = false; - this.fire('editable-content-cancel'); - } + static get properties() { + return { + content: { + notify: true, + type: String, + }, + disabled: { + reflectToAttribute: true, + type: Boolean, + value: false, + }, + editing: { + observer: '_editingChanged', + type: Boolean, + value: false, + }, + removeZeroWidthSpace: Boolean, + // If no storage key is provided, content is not stored. + storageKey: String, + _saveDisabled: { + computed: '_computeSaveDisabled(disabled, content, _newContent)', + type: Boolean, + value: true, + }, + _newContent: { + type: String, + observer: '_newContentChanged', + }, + }; } - customElements.define(GrEditableContent.is, GrEditableContent); -})(); + focusTextarea() { + this.shadowRoot.querySelector('iron-autogrow-textarea').textarea.focus(); + } + + _newContentChanged(newContent, oldContent) { + if (!this.storageKey) { return; } + + this.debounce('store', () => { + if (newContent.length) { + this.$.storage.setEditableContentItem(this.storageKey, newContent); + } else { + // This does not really happen, because we don't clear newContent + // after saving (see below). So this only occurs when the user clears + // all the content in the editable textarea. But <gr-storage> cleans + // up itself after one day, so we are not so concerned about leaving + // some garbage behind. + this.$.storage.eraseEditableContentItem(this.storageKey); + } + }, STORAGE_DEBOUNCE_INTERVAL_MS); + } + + _editingChanged(editing) { + // This method is for initializing _newContent when you start editing. + // Restoring content from local storage is not perfect and has + // some issues: + // + // 1. When you start editing in multiple tabs, then we are vulnerable to + // race conditions between the tabs. + // 2. The stored content is keyed by revision, so when you upload a new + // patchset and click "reload" and then click "cancel" on the content- + // editable, then you won't be able to recover the content anymore. + // + // Because of these issues we believe that it is better to only recover + // content from local storage when you enter editing mode for the first + // time. Otherwise it is better to just keep the last editing state from + // the same session. + if (!editing || this._newContent) { + return; + } + + let content; + if (this.storageKey) { + const storedContent = + this.$.storage.getEditableContentItem(this.storageKey); + if (storedContent && storedContent.message) { + content = storedContent.message; + this.dispatchEvent(new CustomEvent('show-alert', { + detail: {message: RESTORED_MESSAGE}, + bubbles: true, + composed: true, + })); + } + } + if (!content) { + content = this.content || ''; + } + + // TODO(wyatta) switch linkify sequence, see issue 5526. + this._newContent = this.removeZeroWidthSpace ? + content.replace(/^R=\u200B/gm, 'R=') : + content; + } + + _computeSaveDisabled(disabled, content, newContent) { + return disabled || !newContent || content === newContent; + } + + _handleSave(e) { + e.preventDefault(); + this.fire('editable-content-save', {content: this._newContent}); + // It would be nice, if we would set this._newContent = undefined here, + // but we can only do that when we are sure that the save operation has + // succeeded. + } + + _handleCancel(e) { + e.preventDefault(); + this.editing = false; + this.fire('editable-content-cancel'); + } +} + +customElements.define(GrEditableContent.is, GrEditableContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js index 627f948..e0e5047 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-storage/gr-storage.html"> -<link rel="import" href="../gr-button/gr-button.html"> - -<dom-module id="gr-editable-content"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -60,24 +53,15 @@ justify-content: space-between; } </style> - <div class="viewer" hidden$="[[editing]]"> + <div class="viewer" hidden\$="[[editing]]"> <slot></slot> </div> - <div class="editor" hidden$="[[!editing]]"> - <iron-autogrow-textarea - autocomplete="on" - bind-value="{{_newContent}}" - disabled="[[disabled]]"></iron-autogrow-textarea> + <div class="editor" hidden\$="[[!editing]]"> + <iron-autogrow-textarea autocomplete="on" bind-value="{{_newContent}}" disabled="[[disabled]]"></iron-autogrow-textarea> <div class="editButtons"> - <gr-button primary - on-click="_handleSave" - disabled="[[_saveDisabled]]">Save</gr-button> - <gr-button - on-click="_handleCancel" - disabled="[[disabled]]">Cancel</gr-button> + <gr-button primary="" on-click="_handleSave" disabled="[[_saveDisabled]]">Save</gr-button> + <gr-button on-click="_handleCancel" disabled="[[disabled]]">Cancel</gr-button> </div> </div> <gr-storage id="storage"></gr-storage> - </template> - <script src="gr-editable-content.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html index d34ff78..8f3b4d3 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-content/gr-editable-content_test.html
@@ -19,12 +19,12 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editable-content</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> <!-- Can't use absolute path below for mock-interaction.js. Web component tester(wct) has a built-in http server and it serves "/components" directory (which is actually /node_modules directory). Also, wct patches some files to load modules from /components. @@ -33,9 +33,14 @@ --> <script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script> -<link rel="import" href="gr-editable-content.html"> +<script type="module" src="./gr-editable-content.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-editable-content.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -43,128 +48,130 @@ </template> </test-fixture> -<script> - suite('gr-editable-content tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-editable-content.js'; +suite('gr-editable-content tests', () => { + let element; + let sandbox; + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { sandbox.restore(); }); + + test('save event', done => { + element.content = ''; + element._newContent = 'foo'; + element.addEventListener('editable-content-save', e => { + assert.equal(e.detail.content, 'foo'); + done(); + }); + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button[primary]')); + }); + + test('cancel event', done => { + element.addEventListener('editable-content-cancel', () => { + done(); + }); + MockInteractions.tap(element.shadowRoot + .querySelector('gr-button:not([primary])')); + }); + + test('enabling editing keeps old content', () => { + element.content = 'current content'; + element._newContent = 'old content'; + element.editing = true; + assert.equal(element._newContent, 'old content'); + }); + + test('disabling editing does not update edit field contents', () => { + element.content = 'current content'; + element.editing = true; + element._newContent = 'stale content'; + element.editing = false; + assert.equal(element._newContent, 'stale content'); + }); + + test('zero width spaces are removed properly', () => { + element.removeZeroWidthSpace = true; + element.content = 'R=\u200Btest@google.com'; + element.editing = true; + assert.equal(element._newContent, 'R=test@google.com'); + }); + + suite('editing', () => { setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { sandbox.restore(); }); - - test('save event', done => { - element.content = ''; - element._newContent = 'foo'; - element.addEventListener('editable-content-save', e => { - assert.equal(e.detail.content, 'foo'); - done(); - }); - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button[primary]')); - }); - - test('cancel event', done => { - element.addEventListener('editable-content-cancel', () => { - done(); - }); - MockInteractions.tap(element.shadowRoot - .querySelector('gr-button:not([primary])')); - }); - - test('enabling editing keeps old content', () => { - element.content = 'current content'; - element._newContent = 'old content'; - element.editing = true; - assert.equal(element._newContent, 'old content'); - }); - - test('disabling editing does not update edit field contents', () => { element.content = 'current content'; element.editing = true; - element._newContent = 'stale content'; - element.editing = false; - assert.equal(element._newContent, 'stale content'); }); - test('zero width spaces are removed properly', () => { - element.removeZeroWidthSpace = true; - element.content = 'R=\u200Btest@google.com'; - element.editing = true; - assert.equal(element._newContent, 'R=test@google.com'); + test('save button is disabled initially', () => { + assert.isTrue(element.shadowRoot + .querySelector('gr-button[primary]').disabled); }); - suite('editing', () => { - setup(() => { - element.content = 'current content'; - element.editing = true; - }); - - test('save button is disabled initially', () => { - assert.isTrue(element.shadowRoot - .querySelector('gr-button[primary]').disabled); - }); - - test('save button is enabled when content changes', () => { - element._newContent = 'new content'; - assert.isFalse(element.shadowRoot - .querySelector('gr-button[primary]').disabled); - }); - }); - - suite('storageKey and related behavior', () => { - let dispatchSpy; - setup(() => { - element.content = 'current content'; - element.storageKey = 'test'; - dispatchSpy = sandbox.spy(element, 'dispatchEvent'); - }); - - test('editing toggled to true, has stored data', () => { - sandbox.stub(element.$.storage, 'getEditableContentItem') - .returns({message: 'stored content'}); - element.editing = true; - - assert.equal(element._newContent, 'stored content'); - assert.isTrue(dispatchSpy.called); - assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert'); - }); - - test('editing toggled to true, has no stored data', () => { - sandbox.stub(element.$.storage, 'getEditableContentItem') - .returns({}); - element.editing = true; - - assert.equal(element._newContent, 'current content'); - assert.isFalse(dispatchSpy.called); - }); - - test('edits are cached', () => { - const storeStub = - sandbox.stub(element.$.storage, 'setEditableContentItem'); - const eraseStub = - sandbox.stub(element.$.storage, 'eraseEditableContentItem'); - element.editing = true; - - element._newContent = 'new content'; - flushAsynchronousOperations(); - element.flushDebouncer('store'); - - assert.isTrue(storeStub.called); - assert.deepEqual( - [element.storageKey, element._newContent], - storeStub.lastCall.args); - - element._newContent = ''; - flushAsynchronousOperations(); - element.flushDebouncer('store'); - - assert.isTrue(eraseStub.called); - assert.deepEqual([element.storageKey], eraseStub.lastCall.args); - }); + test('save button is enabled when content changes', () => { + element._newContent = 'new content'; + assert.isFalse(element.shadowRoot + .querySelector('gr-button[primary]').disabled); }); }); + + suite('storageKey and related behavior', () => { + let dispatchSpy; + setup(() => { + element.content = 'current content'; + element.storageKey = 'test'; + dispatchSpy = sandbox.spy(element, 'dispatchEvent'); + }); + + test('editing toggled to true, has stored data', () => { + sandbox.stub(element.$.storage, 'getEditableContentItem') + .returns({message: 'stored content'}); + element.editing = true; + + assert.equal(element._newContent, 'stored content'); + assert.isTrue(dispatchSpy.called); + assert.equal(dispatchSpy.lastCall.args[0].type, 'show-alert'); + }); + + test('editing toggled to true, has no stored data', () => { + sandbox.stub(element.$.storage, 'getEditableContentItem') + .returns({}); + element.editing = true; + + assert.equal(element._newContent, 'current content'); + assert.isFalse(dispatchSpy.called); + }); + + test('edits are cached', () => { + const storeStub = + sandbox.stub(element.$.storage, 'setEditableContentItem'); + const eraseStub = + sandbox.stub(element.$.storage, 'eraseEditableContentItem'); + element.editing = true; + + element._newContent = 'new content'; + flushAsynchronousOperations(); + element.flushDebouncer('store'); + + assert.isTrue(storeStub.called); + assert.deepEqual( + [element.storageKey, element._newContent], + storeStub.lastCall.args); + + element._newContent = ''; + flushAsynchronousOperations(); + element.flushDebouncer('store'); + + assert.isTrue(eraseStub.called); + assert.deepEqual([element.storageKey], eraseStub.lastCall.args); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js index ef5bb8c..3de5a64 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label.js
@@ -14,193 +14,207 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const AWAIT_MAX_ITERS = 10; - const AWAIT_STEP = 5; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import {IronOverlayBehaviorImpl} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js'; +import '@polymer/iron-dropdown/iron-dropdown.js'; +import '@polymer/paper-input/paper-input.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-button/gr-button.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-editable-label_html.js'; + +const AWAIT_MAX_ITERS = 10; +const AWAIT_STEP = 5; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrEditableLabel extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-editable-label'; } + /** + * Fired when the value is changed. + * + * @event changed + */ + + static get properties() { + return { + labelText: String, + editing: { + type: Boolean, + value: false, + }, + value: { + type: String, + notify: true, + value: '', + observer: '_updateTitle', + }, + placeholder: { + type: String, + value: '', + }, + readOnly: { + type: Boolean, + value: false, + }, + uppercase: { + type: Boolean, + reflectToAttribute: true, + value: false, + }, + maxLength: Number, + _inputText: String, + // This is used to push the iron-input element up on the page, so + // the input is placed in approximately the same position as the + // trigger. + _verticalOffset: { + type: Number, + readOnly: true, + value: -30, + }, + }; + } + + /** @override */ + ready() { + super.ready(); + this._ensureAttribute('tabindex', '0'); + } + + get keyBindings() { + return { + enter: '_handleEnter', + esc: '_handleEsc', + }; + } + + _usePlaceholder(value, placeholder) { + return (!value || !value.length) && placeholder; + } + + _computeLabel(value, placeholder) { + if (this._usePlaceholder(value, placeholder)) { + return placeholder; + } + return value; + } + + _showDropdown() { + if (this.readOnly || this.editing) { return; } + return this._open().then(() => { + this._nativeInput.focus(); + if (!this.$.input.value) { return; } + this._nativeInput.setSelectionRange(0, this.$.input.value.length); + }); + } + + open() { + return this._open().then(() => { + this._nativeInput.focus(); + }); + } + + _open(...args) { + this.$.dropdown.open(); + this._inputText = this.value; + this.editing = true; + + return new Promise(resolve => { + IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args); + this._awaitOpen(resolve); + }); + } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually + * opening. Eventually replace with a direct way to listen to the overlay. */ - class GrEditableLabel extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-editable-label'; } - /** - * Fired when the value is changed. - * - * @event changed - */ + _awaitOpen(fn) { + let iters = 0; + const step = () => { + this.async(() => { + if (this.$.dropdown.style.display !== 'none') { + fn.call(this); + } else if (iters++ < AWAIT_MAX_ITERS) { + step.call(this); + } + }, AWAIT_STEP); + }; + step.call(this); + } - static get properties() { - return { - labelText: String, - editing: { - type: Boolean, - value: false, - }, - value: { - type: String, - notify: true, - value: '', - observer: '_updateTitle', - }, - placeholder: { - type: String, - value: '', - }, - readOnly: { - type: Boolean, - value: false, - }, - uppercase: { - type: Boolean, - reflectToAttribute: true, - value: false, - }, - maxLength: Number, - _inputText: String, - // This is used to push the iron-input element up on the page, so - // the input is placed in approximately the same position as the - // trigger. - _verticalOffset: { - type: Number, - readOnly: true, - value: -30, - }, - }; - } + _id() { + return this.getAttribute('id') || 'global'; + } - /** @override */ - ready() { - super.ready(); - this._ensureAttribute('tabindex', '0'); - } + _save() { + if (!this.editing) { return; } + this.$.dropdown.close(); + this.value = this._inputText; + this.editing = false; + this.fire('changed', this.value); + } - get keyBindings() { - return { - enter: '_handleEnter', - esc: '_handleEsc', - }; - } + _cancel() { + if (!this.editing) { return; } + this.$.dropdown.close(); + this.editing = false; + this._inputText = this.value; + } - _usePlaceholder(value, placeholder) { - return (!value || !value.length) && placeholder; - } + get _nativeInput() { + // In Polymer 2, the namespace of nativeInput + // changed from input to nativeInput + return this.$.input.$.nativeInput || this.$.input.$.input; + } - _computeLabel(value, placeholder) { - if (this._usePlaceholder(value, placeholder)) { - return placeholder; - } - return value; - } - - _showDropdown() { - if (this.readOnly || this.editing) { return; } - return this._open().then(() => { - this._nativeInput.focus(); - if (!this.$.input.value) { return; } - this._nativeInput.setSelectionRange(0, this.$.input.value.length); - }); - } - - open() { - return this._open().then(() => { - this._nativeInput.focus(); - }); - } - - _open(...args) { - this.$.dropdown.open(); - this._inputText = this.value; - this.editing = true; - - return new Promise(resolve => { - Polymer.IronOverlayBehaviorImpl.open.apply(this.$.dropdown, args); - this._awaitOpen(resolve); - }); - } - - /** - * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually - * opening. Eventually replace with a direct way to listen to the overlay. - */ - _awaitOpen(fn) { - let iters = 0; - const step = () => { - this.async(() => { - if (this.$.dropdown.style.display !== 'none') { - fn.call(this); - } else if (iters++ < AWAIT_MAX_ITERS) { - step.call(this); - } - }, AWAIT_STEP); - }; - step.call(this); - } - - _id() { - return this.getAttribute('id') || 'global'; - } - - _save() { - if (!this.editing) { return; } - this.$.dropdown.close(); - this.value = this._inputText; - this.editing = false; - this.fire('changed', this.value); - } - - _cancel() { - if (!this.editing) { return; } - this.$.dropdown.close(); - this.editing = false; - this._inputText = this.value; - } - - get _nativeInput() { - // In Polymer 2, the namespace of nativeInput - // changed from input to nativeInput - return this.$.input.$.nativeInput || this.$.input.$.input; - } - - _handleEnter(e) { - e = this.getKeyboardEvent(e); - const target = Polymer.dom(e).rootTarget; - if (target === this._nativeInput) { - e.preventDefault(); - this._save(); - } - } - - _handleEsc(e) { - e = this.getKeyboardEvent(e); - const target = Polymer.dom(e).rootTarget; - if (target === this._nativeInput) { - e.preventDefault(); - this._cancel(); - } - } - - _computeLabelClass(readOnly, value, placeholder) { - const classes = []; - if (!readOnly) { classes.push('editable'); } - if (this._usePlaceholder(value, placeholder)) { - classes.push('placeholder'); - } - return classes.join(' '); - } - - _updateTitle(value) { - this.setAttribute('title', this._computeLabel(value, this.placeholder)); + _handleEnter(e) { + e = this.getKeyboardEvent(e); + const target = dom(e).rootTarget; + if (target === this._nativeInput) { + e.preventDefault(); + this._save(); } } - customElements.define(GrEditableLabel.is, GrEditableLabel); -})(); + _handleEsc(e) { + e = this.getKeyboardEvent(e); + const target = dom(e).rootTarget; + if (target === this._nativeInput) { + e.preventDefault(); + this._cancel(); + } + } + + _computeLabelClass(readOnly, value, placeholder) { + const classes = []; + if (!readOnly) { classes.push('editable'); } + if (this._usePlaceholder(value, placeholder)) { + classes.push('placeholder'); + } + return classes.join(' '); + } + + _updateTitle(value) { + this.setAttribute('title', this._computeLabel(value, this.placeholder)); + } +} + +customElements.define(GrEditableLabel.is, GrEditableLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js index 8d0d1c37..9bc31a3 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html"> -<link rel="import" href="/bower_components/iron-dropdown/iron-dropdown.html"> -<link rel="import" href="/bower_components/paper-input/paper-input.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-button/gr-button.html"> - -<dom-module id="gr-editable-label"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { align-items: center; @@ -78,30 +69,16 @@ --paper-input-container-focus-color: var(--link-color); } </style> - <label - class$="[[_computeLabelClass(readOnly, value, placeholder)]]" - title$="[[_computeLabel(value, placeholder)]]" - on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label> - <iron-dropdown id="dropdown" - vertical-align="auto" - horizontal-align="auto" - vertical-offset="[[_verticalOffset]]" - allow-outside-scroll="true" - on-iron-overlay-canceled="_cancel"> + <label class\$="[[_computeLabelClass(readOnly, value, placeholder)]]" title\$="[[_computeLabel(value, placeholder)]]" on-click="_showDropdown">[[_computeLabel(value, placeholder)]]</label> + <iron-dropdown id="dropdown" vertical-align="auto" horizontal-align="auto" vertical-offset="[[_verticalOffset]]" allow-outside-scroll="true" on-iron-overlay-canceled="_cancel"> <div class="dropdown-content" slot="dropdown-content"> <div class="inputContainer"> - <paper-input - id="input" - label="[[labelText]]" - maxlength="[[maxLength]]" - value="{{_inputText}}"></paper-input> + <paper-input id="input" label="[[labelText]]" maxlength="[[maxLength]]" value="{{_inputText}}"></paper-input> <div class="buttons"> - <gr-button link id="cancelBtn" on-click="_cancel">cancel</gr-button> - <gr-button link id="saveBtn" on-click="_save">save</gr-button> + <gr-button link="" id="cancelBtn" on-click="_cancel">cancel</gr-button> + <gr-button link="" id="saveBtn" on-click="_save">save</gr-button> </div> </div> </div> </iron-dropdown> - </template> - <script src="gr-editable-label.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html index ccf5e3b..29e6619 100644 --- a/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html +++ b/polygerrit-ui/app/elements/shared/gr-editable-label/gr-editable-label_test.html
@@ -19,12 +19,12 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editable-label</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> <!-- Can't use absolute path below for mock-interaction.js. Web component tester(wct) has a built-in http server and it serves "/components" directory (which is actually /node_modules directory). Also, wct patches some files to load modules from /components. @@ -33,9 +33,14 @@ --> <script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script> -<link rel="import" href="gr-editable-label.html"> +<script type="module" src="./gr-editable-label.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-editable-label.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -60,197 +65,200 @@ </template> </test-fixture> -<script> - suite('gr-editable-label tests', async () => { - await readyToTest(); - let element; - let elementNoPlaceholder; - let input; - let label; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-editable-label.js'; +import {flush as flush$0} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-editable-label tests', () => { + let element; + let elementNoPlaceholder; + let input; + let label; + let sandbox; - setup(done => { - element = fixture('basic'); - elementNoPlaceholder = fixture('no-placeholder'); + setup(done => { + element = fixture('basic'); + elementNoPlaceholder = fixture('no-placeholder'); - label = element.shadowRoot - .querySelector('label'); - sandbox = sinon.sandbox.create(); - flush(() => { - // In Polymer 2 inputElement isn't nativeInput anymore - input = element.$.input.$.nativeInput || element.$.input.inputElement; - done(); - }); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('element render', () => { - // The dropdown is closed and the label is visible: - assert.isFalse(element.$.dropdown.opened); - assert.isTrue(label.classList.contains('editable')); - assert.equal(label.textContent, 'value text'); - const focusSpy = sandbox.spy(input, 'focus'); - const showSpy = sandbox.spy(element, '_showDropdown'); - - MockInteractions.tap(label); - - return showSpy.lastCall.returnValue.then(() => { - // The dropdown is open (which covers up the label): - assert.isTrue(element.$.dropdown.opened); - assert.isTrue(focusSpy.called); - assert.equal(input.value, 'value text'); - }); - }); - - test('title with placeholder', done => { - assert.equal(element.title, 'value text'); - element.value = ''; - - element.async(() => { - assert.equal(element.title, 'label text'); - done(); - }); - }); - - test('title without placeholder', done => { - assert.equal(elementNoPlaceholder.title, ''); - element.value = 'value text'; - - element.async(() => { - assert.equal(element.title, 'value text'); - done(); - }); - }); - - test('edit value', done => { - const editedStub = sandbox.stub(); - element.addEventListener('changed', editedStub); - assert.isFalse(element.editing); - - MockInteractions.tap(label); - - Polymer.dom.flush(); - - assert.isTrue(element.editing); - element._inputText = 'new text'; - - assert.isFalse(editedStub.called); - - element.async(() => { - assert.isTrue(editedStub.called); - assert.equal(input.value, 'new text'); - assert.isFalse(element.editing); - done(); - }); - - // Press enter: - MockInteractions.keyDownOn(input, 13); - }); - - test('save button', done => { - const editedStub = sandbox.stub(); - element.addEventListener('changed', editedStub); - assert.isFalse(element.editing); - - MockInteractions.tap(label); - - Polymer.dom.flush(); - - assert.isTrue(element.editing); - element._inputText = 'new text'; - - assert.isFalse(editedStub.called); - - element.async(() => { - assert.isTrue(editedStub.called); - assert.equal(input.value, 'new text'); - assert.isFalse(element.editing); - done(); - }); - - // Press enter: - MockInteractions.tap(element.$.saveBtn, 13); - }); - - test('edit and then escape key', done => { - const editedStub = sandbox.stub(); - element.addEventListener('changed', editedStub); - assert.isFalse(element.editing); - - MockInteractions.tap(label); - - Polymer.dom.flush(); - - assert.isTrue(element.editing); - element._inputText = 'new text'; - - assert.isFalse(editedStub.called); - - element.async(() => { - assert.isFalse(editedStub.called); - // Text changes sould be discarded. - assert.equal(input.value, 'value text'); - assert.isFalse(element.editing); - done(); - }); - - // Press escape: - MockInteractions.keyDownOn(input, 27); - }); - - test('cancel button', done => { - const editedStub = sandbox.stub(); - element.addEventListener('changed', editedStub); - assert.isFalse(element.editing); - - MockInteractions.tap(label); - - Polymer.dom.flush(); - - assert.isTrue(element.editing); - element._inputText = 'new text'; - - assert.isFalse(editedStub.called); - - element.async(() => { - assert.isFalse(editedStub.called); - // Text changes sould be discarded. - assert.equal(input.value, 'value text'); - assert.isFalse(element.editing); - done(); - }); - - // Press escape: - MockInteractions.tap(element.$.cancelBtn); - }); - - suite('gr-editable-label read-only tests', () => { - let element; - let label; - - setup(() => { - element = fixture('read-only'); - label = element.shadowRoot - .querySelector('label'); - }); - - test('disallows edit when read-only', () => { - // The dropdown is closed. - assert.isFalse(element.$.dropdown.opened); - MockInteractions.tap(label); - - Polymer.dom.flush(); - - // The dropdown is still closed. - assert.isFalse(element.$.dropdown.opened); - }); - - test('label is not marked as editable', () => { - assert.isFalse(label.classList.contains('editable')); - }); + label = element.shadowRoot + .querySelector('label'); + sandbox = sinon.sandbox.create(); + flush(() => { + // In Polymer 2 inputElement isn't nativeInput anymore + input = element.$.input.$.nativeInput || element.$.input.inputElement; + done(); }); }); + + teardown(() => { + sandbox.restore(); + }); + + test('element render', () => { + // The dropdown is closed and the label is visible: + assert.isFalse(element.$.dropdown.opened); + assert.isTrue(label.classList.contains('editable')); + assert.equal(label.textContent, 'value text'); + const focusSpy = sandbox.spy(input, 'focus'); + const showSpy = sandbox.spy(element, '_showDropdown'); + + MockInteractions.tap(label); + + return showSpy.lastCall.returnValue.then(() => { + // The dropdown is open (which covers up the label): + assert.isTrue(element.$.dropdown.opened); + assert.isTrue(focusSpy.called); + assert.equal(input.value, 'value text'); + }); + }); + + test('title with placeholder', done => { + assert.equal(element.title, 'value text'); + element.value = ''; + + element.async(() => { + assert.equal(element.title, 'label text'); + done(); + }); + }); + + test('title without placeholder', done => { + assert.equal(elementNoPlaceholder.title, ''); + element.value = 'value text'; + + element.async(() => { + assert.equal(element.title, 'value text'); + done(); + }); + }); + + test('edit value', done => { + const editedStub = sandbox.stub(); + element.addEventListener('changed', editedStub); + assert.isFalse(element.editing); + + MockInteractions.tap(label); + + flush$0(); + + assert.isTrue(element.editing); + element._inputText = 'new text'; + + assert.isFalse(editedStub.called); + + element.async(() => { + assert.isTrue(editedStub.called); + assert.equal(input.value, 'new text'); + assert.isFalse(element.editing); + done(); + }); + + // Press enter: + MockInteractions.keyDownOn(input, 13); + }); + + test('save button', done => { + const editedStub = sandbox.stub(); + element.addEventListener('changed', editedStub); + assert.isFalse(element.editing); + + MockInteractions.tap(label); + + flush$0(); + + assert.isTrue(element.editing); + element._inputText = 'new text'; + + assert.isFalse(editedStub.called); + + element.async(() => { + assert.isTrue(editedStub.called); + assert.equal(input.value, 'new text'); + assert.isFalse(element.editing); + done(); + }); + + // Press enter: + MockInteractions.tap(element.$.saveBtn, 13); + }); + + test('edit and then escape key', done => { + const editedStub = sandbox.stub(); + element.addEventListener('changed', editedStub); + assert.isFalse(element.editing); + + MockInteractions.tap(label); + + flush$0(); + + assert.isTrue(element.editing); + element._inputText = 'new text'; + + assert.isFalse(editedStub.called); + + element.async(() => { + assert.isFalse(editedStub.called); + // Text changes sould be discarded. + assert.equal(input.value, 'value text'); + assert.isFalse(element.editing); + done(); + }); + + // Press escape: + MockInteractions.keyDownOn(input, 27); + }); + + test('cancel button', done => { + const editedStub = sandbox.stub(); + element.addEventListener('changed', editedStub); + assert.isFalse(element.editing); + + MockInteractions.tap(label); + + flush$0(); + + assert.isTrue(element.editing); + element._inputText = 'new text'; + + assert.isFalse(editedStub.called); + + element.async(() => { + assert.isFalse(editedStub.called); + // Text changes sould be discarded. + assert.equal(input.value, 'value text'); + assert.isFalse(element.editing); + done(); + }); + + // Press escape: + MockInteractions.tap(element.$.cancelBtn); + }); + + suite('gr-editable-label read-only tests', () => { + let element; + let label; + + setup(() => { + element = fixture('read-only'); + label = element.shadowRoot + .querySelector('label'); + }); + + test('disallows edit when read-only', () => { + // The dropdown is closed. + assert.isFalse(element.$.dropdown.opened); + MockInteractions.tap(label); + + flush$0(); + + // The dropdown is still closed. + assert.isFalse(element.$.dropdown.opened); + }); + + test('label is not marked as editable', () => { + assert.isFalse(label.classList.contains('editable')); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html index 305702a..a58df2e 100644 --- a/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-event-interface/gr-event-interface_test.html
@@ -19,13 +19,18 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="../../../bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../gr-js-api-interface/gr-js-api-interface.html"> +<script src="../../../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../gr-js-api-interface/gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-js-api-interface/gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -33,118 +38,120 @@ </template> </test-fixture> -<script> - suite('gr-event-interface tests', async () => { - await readyToTest(); - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../gr-js-api-interface/gr-js-api-interface.js'; +suite('gr-event-interface tests', () => { + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('test on Gerrit', () => { setup(() => { - sandbox = sinon.sandbox.create(); + fixture('basic'); + Gerrit.removeAllListeners(); }); - teardown(() => { - sandbox.restore(); + test('communicate between plugin and Gerrit', done => { + const eventName = 'test-plugin-event'; + let p; + Gerrit.on(eventName, e => { + assert.equal(e.value, 'test'); + assert.equal(e.plugin, p); + done(); + }); + Gerrit.install(plugin => { + p = plugin; + Gerrit.emit(eventName, {value: 'test', plugin}); + }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); }); - suite('test on Gerrit', () => { - setup(() => { - fixture('basic'); - Gerrit.removeAllListeners(); + test('listen on events from core', done => { + const eventName = 'test-plugin-event'; + Gerrit.on(eventName, e => { + assert.equal(e.value, 'test'); + done(); }); - test('communicate between plugin and Gerrit', done => { - const eventName = 'test-plugin-event'; - let p; + Gerrit.emit(eventName, {value: 'test'}); + }); + + test('communicate across plugins', done => { + const eventName = 'test-plugin-event'; + Gerrit.install(plugin => { Gerrit.on(eventName, e => { - assert.equal(e.value, 'test'); - assert.equal(e.plugin, p); + assert.equal(e.plugin.getPluginName(), 'testB'); done(); }); - Gerrit.install(plugin => { - p = plugin; - Gerrit.emit(eventName, {value: 'test', plugin}); - }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - }); + }, '0.1', + 'http://test.com/plugins/testA/static/testA.js'); - test('listen on events from core', done => { - const eventName = 'test-plugin-event'; - Gerrit.on(eventName, e => { - assert.equal(e.value, 'test'); - done(); - }); - - Gerrit.emit(eventName, {value: 'test'}); - }); - - test('communicate across plugins', done => { - const eventName = 'test-plugin-event'; - Gerrit.install(plugin => { - Gerrit.on(eventName, e => { - assert.equal(e.plugin.getPluginName(), 'testB'); - done(); - }); - }, '0.1', - 'http://test.com/plugins/testA/static/testA.js'); - - Gerrit.install(plugin => { - Gerrit.emit(eventName, {plugin}); - }, '0.1', - 'http://test.com/plugins/testB/static/testB.js'); - }); - }); - - suite('test on interfaces', () => { - let testObj; - - class TestClass extends EventEmitter { - } - - setup(() => { - testObj = new TestClass(); - }); - - test('on', () => { - const cbStub = sinon.stub(); - testObj.on('test', cbStub); - testObj.emit('test'); - testObj.emit('test'); - assert.isTrue(cbStub.calledTwice); - }); - - test('once', () => { - const cbStub = sinon.stub(); - testObj.once('test', cbStub); - testObj.emit('test'); - testObj.emit('test'); - assert.isTrue(cbStub.calledOnce); - }); - - test('unsubscribe', () => { - const cbStub = sinon.stub(); - const unsubscribe = testObj.on('test', cbStub); - testObj.emit('test'); - unsubscribe(); - testObj.emit('test'); - assert.isTrue(cbStub.calledOnce); - }); - - test('off', () => { - const cbStub = sinon.stub(); - testObj.on('test', cbStub); - testObj.emit('test'); - testObj.off('test', cbStub); - testObj.emit('test'); - assert.isTrue(cbStub.calledOnce); - }); - - test('removeAllListeners', () => { - const cbStub = sinon.stub(); - testObj.on('test', cbStub); - testObj.removeAllListeners('test'); - testObj.emit('test'); - assert.isTrue(cbStub.notCalled); - }); + Gerrit.install(plugin => { + Gerrit.emit(eventName, {plugin}); + }, '0.1', + 'http://test.com/plugins/testB/static/testB.js'); }); }); + + suite('test on interfaces', () => { + let testObj; + + class TestClass extends EventEmitter { + } + + setup(() => { + testObj = new TestClass(); + }); + + test('on', () => { + const cbStub = sinon.stub(); + testObj.on('test', cbStub); + testObj.emit('test'); + testObj.emit('test'); + assert.isTrue(cbStub.calledTwice); + }); + + test('once', () => { + const cbStub = sinon.stub(); + testObj.once('test', cbStub); + testObj.emit('test'); + testObj.emit('test'); + assert.isTrue(cbStub.calledOnce); + }); + + test('unsubscribe', () => { + const cbStub = sinon.stub(); + const unsubscribe = testObj.on('test', cbStub); + testObj.emit('test'); + unsubscribe(); + testObj.emit('test'); + assert.isTrue(cbStub.calledOnce); + }); + + test('off', () => { + const cbStub = sinon.stub(); + testObj.on('test', cbStub); + testObj.emit('test'); + testObj.off('test', cbStub); + testObj.emit('test'); + assert.isTrue(cbStub.calledOnce); + }); + + test('removeAllListeners', () => { + const cbStub = sinon.stub(); + testObj.on('test', cbStub); + testObj.removeAllListeners('test'); + testObj.emit('test'); + assert.isTrue(cbStub.notCalled); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js index d4995cc..0d19f00 100644 --- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel.js
@@ -14,225 +14,231 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrFixedPanel extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-fixed-panel'; } +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-fixed-panel_html.js'; - static get properties() { - return { - floatingDisabled: { - type: Boolean, - value: false, - }, - readyForMeasure: { - type: Boolean, - observer: '_readyForMeasureObserver', - }, - keepOnScroll: { - type: Boolean, - value: false, - }, - _isMeasured: { - type: Boolean, - value: false, - }, +/** @extends Polymer.Element */ +class GrFixedPanel extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** - * Initial offset from the top of the document, in pixels. - */ - _topInitial: Number, + static get is() { return 'gr-fixed-panel'; } - /** - * Current offset from the top of the window, in pixels. - */ - _topLast: Number, + static get properties() { + return { + floatingDisabled: { + type: Boolean, + value: false, + }, + readyForMeasure: { + type: Boolean, + observer: '_readyForMeasureObserver', + }, + keepOnScroll: { + type: Boolean, + value: false, + }, + _isMeasured: { + type: Boolean, + value: false, + }, - _headerHeight: Number, - _headerFloating: { - type: Boolean, - value: false, - }, - _observer: { - type: Object, - value: null, - }, - /** - * If place before any other content defines how much - * of the content below it is covered by this panel - */ - floatingHeight: { - type: Number, - value: 0, - notify: true, - }, + /** + * Initial offset from the top of the document, in pixels. + */ + _topInitial: Number, - _webComponentsReady: Boolean, - }; + /** + * Current offset from the top of the window, in pixels. + */ + _topLast: Number, + + _headerHeight: Number, + _headerFloating: { + type: Boolean, + value: false, + }, + _observer: { + type: Object, + value: null, + }, + /** + * If place before any other content defines how much + * of the content below it is covered by this panel + */ + floatingHeight: { + type: Number, + value: 0, + notify: true, + }, + + _webComponentsReady: Boolean, + }; + } + + static get observers() { + return [ + '_updateFloatingHeight(floatingDisabled, _isMeasured, _headerHeight)', + ]; + } + + _updateFloatingHeight(floatingDisabled, isMeasured, headerHeight) { + if ([ + floatingDisabled, + isMeasured, + headerHeight, + ].some(arg => arg === undefined)) { + return; } + this.floatingHeight = + (!floatingDisabled && isMeasured) ? headerHeight : 0; + } - static get observers() { - return [ - '_updateFloatingHeight(floatingDisabled, _isMeasured, _headerHeight)', - ]; + /** @override */ + attached() { + super.attached(); + if (this.floatingDisabled) { + return; } - - _updateFloatingHeight(floatingDisabled, isMeasured, headerHeight) { - if ([ - floatingDisabled, - isMeasured, - headerHeight, - ].some(arg => arg === undefined)) { - return; - } - this.floatingHeight = - (!floatingDisabled && isMeasured) ? headerHeight : 0; + // Enable content measure unless blocked by param. + if (this.readyForMeasure !== false) { + this.readyForMeasure = true; } + this.listen(window, 'resize', 'update'); + this.listen(window, 'scroll', '_updateOnScroll'); + this._observer = new MutationObserver(this.update.bind(this)); + this._observer.observe(this.$.header, {childList: true, subtree: true}); + } - /** @override */ - attached() { - super.attached(); - if (this.floatingDisabled) { - return; - } - // Enable content measure unless blocked by param. - if (this.readyForMeasure !== false) { - this.readyForMeasure = true; - } - this.listen(window, 'resize', 'update'); - this.listen(window, 'scroll', '_updateOnScroll'); - this._observer = new MutationObserver(this.update.bind(this)); - this._observer.observe(this.$.header, {childList: true, subtree: true}); - } - - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'scroll', '_updateOnScroll'); - this.unlisten(window, 'resize', 'update'); - if (this._observer) { - this._observer.disconnect(); - } - } - - _readyForMeasureObserver(readyForMeasure) { - if (readyForMeasure) { - this.update(); - } - } - - _computeHeaderClass(headerFloating, topLast) { - const fixedAtTop = this.keepOnScroll && topLast === 0; - return [ - headerFloating ? 'floating' : '', - fixedAtTop ? 'fixedAtTop' : '', - ].join(' '); - } - - unfloat() { - if (this.floatingDisabled) { - return; - } - this.$.header.style.top = ''; - this._headerFloating = false; - this.updateStyles({'--header-height': ''}); - } - - update() { - this.debounce('update', () => { - this._updateDebounced(); - }, 100); - } - - _updateOnScroll() { - this.debounce('update', () => { - this._updateDebounced(); - }); - } - - _updateDebounced() { - if (this.floatingDisabled) { - return; - } - this._isMeasured = false; - this._maybeFloatHeader(); - this._reposition(); - } - - _getElementTop() { - return this.getBoundingClientRect().top; - } - - _reposition() { - if (!this._headerFloating) { - return; - } - const header = this.$.header; - // Since the outer element is relative positioned, can use its top - // to determine how to position the inner header element. - const elemTop = this._getElementTop(); - let newTop; - if (this.keepOnScroll && elemTop < 0) { - // Should stick to the top. - newTop = 0; - } else { - // Keep in line with the outer element. - newTop = elemTop; - } - // Initialize top style if it doesn't exist yet. - if (!header.style.top && this._topLast === newTop) { - header.style.top = newTop; - } - if (this._topLast !== newTop) { - if (newTop === undefined) { - header.style.top = ''; - } else { - header.style.top = newTop + 'px'; - } - this._topLast = newTop; - } - } - - _measure() { - if (this._isMeasured) { - return; // Already measured. - } - const rect = this.$.header.getBoundingClientRect(); - if (rect.height === 0 && rect.width === 0) { - return; // Not ready for measurement yet. - } - const top = document.body.scrollTop + rect.top; - this._topLast = top; - this._headerHeight = rect.height; - this._topInitial = - this.getBoundingClientRect().top + document.body.scrollTop; - this._isMeasured = true; - } - - _isFloatingNeeded() { - return this.keepOnScroll || - document.body.scrollWidth > document.body.clientWidth; - } - - _maybeFloatHeader() { - if (!this._isFloatingNeeded()) { - return; - } - this._measure(); - if (this._isMeasured) { - this._floatHeader(); - } - } - - _floatHeader() { - this.updateStyles({'--header-height': this._headerHeight + 'px'}); - this._headerFloating = true; + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'scroll', '_updateOnScroll'); + this.unlisten(window, 'resize', 'update'); + if (this._observer) { + this._observer.disconnect(); } } - customElements.define(GrFixedPanel.is, GrFixedPanel); -})(); + _readyForMeasureObserver(readyForMeasure) { + if (readyForMeasure) { + this.update(); + } + } + + _computeHeaderClass(headerFloating, topLast) { + const fixedAtTop = this.keepOnScroll && topLast === 0; + return [ + headerFloating ? 'floating' : '', + fixedAtTop ? 'fixedAtTop' : '', + ].join(' '); + } + + unfloat() { + if (this.floatingDisabled) { + return; + } + this.$.header.style.top = ''; + this._headerFloating = false; + this.updateStyles({'--header-height': ''}); + } + + update() { + this.debounce('update', () => { + this._updateDebounced(); + }, 100); + } + + _updateOnScroll() { + this.debounce('update', () => { + this._updateDebounced(); + }); + } + + _updateDebounced() { + if (this.floatingDisabled) { + return; + } + this._isMeasured = false; + this._maybeFloatHeader(); + this._reposition(); + } + + _getElementTop() { + return this.getBoundingClientRect().top; + } + + _reposition() { + if (!this._headerFloating) { + return; + } + const header = this.$.header; + // Since the outer element is relative positioned, can use its top + // to determine how to position the inner header element. + const elemTop = this._getElementTop(); + let newTop; + if (this.keepOnScroll && elemTop < 0) { + // Should stick to the top. + newTop = 0; + } else { + // Keep in line with the outer element. + newTop = elemTop; + } + // Initialize top style if it doesn't exist yet. + if (!header.style.top && this._topLast === newTop) { + header.style.top = newTop; + } + if (this._topLast !== newTop) { + if (newTop === undefined) { + header.style.top = ''; + } else { + header.style.top = newTop + 'px'; + } + this._topLast = newTop; + } + } + + _measure() { + if (this._isMeasured) { + return; // Already measured. + } + const rect = this.$.header.getBoundingClientRect(); + if (rect.height === 0 && rect.width === 0) { + return; // Not ready for measurement yet. + } + const top = document.body.scrollTop + rect.top; + this._topLast = top; + this._headerHeight = rect.height; + this._topInitial = + this.getBoundingClientRect().top + document.body.scrollTop; + this._isMeasured = true; + } + + _isFloatingNeeded() { + return this.keepOnScroll || + document.body.scrollWidth > document.body.clientWidth; + } + + _maybeFloatHeader() { + if (!this._isFloatingNeeded()) { + return; + } + this._measure(); + if (this._isMeasured) { + this._floatHeader(); + } + } + + _floatHeader() { + this.updateStyles({'--header-height': this._headerHeight + 'px'}); + this._headerFloating = true; + } +} + +customElements.define(GrFixedPanel.is, GrFixedPanel);
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js index 14285b4..69ae735 100644 --- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-fixed-panel"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { box-sizing: border-box; @@ -44,9 +41,7 @@ box-shadow: var(--elevation-level-2); } </style> - <header id="header" class$="[[_computeHeaderClass(_headerFloating, _topLast)]]"> + <header id="header" class\$="[[_computeHeaderClass(_headerFloating, _topLast)]]"> <slot></slot> </header> - </template> - <script src="gr-fixed-panel.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html index 7ae0265..e2f77c5 100644 --- a/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html +++ b/polygerrit-ui/app/elements/shared/gr-fixed-panel/gr-fixed-panel_test.html
@@ -19,13 +19,13 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-fixed-panel</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-fixed-panel.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-fixed-panel.js"></script> <style> /* Prevent horizontal scrolling on page. New version of web-component-tester creates body with margins */ @@ -35,7 +35,12 @@ } </style> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-fixed-panel.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -45,83 +50,85 @@ </template> </test-fixture> -<script> - suite('gr-fixed-panel', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-fixed-panel.js'; +suite('gr-fixed-panel', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.readyForMeasure = true; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('can be disabled with floatingDisabled', () => { + element.floatingDisabled = true; + sandbox.stub(element, '_reposition'); + window.dispatchEvent(new CustomEvent('resize')); + element.flushDebouncer('update'); + assert.isFalse(element._reposition.called); + }); + + test('header is the height of the content', () => { + assert.equal(element.getBoundingClientRect().height, 100); + }); + + test('scroll triggers _reposition', () => { + sandbox.stub(element, '_reposition'); + window.dispatchEvent(new CustomEvent('scroll')); + element.flushDebouncer('update'); + assert.isTrue(element._reposition.called); + }); + + suite('_reposition', () => { + const getHeaderTop = function() { + return element.$.header.style.top; + }; + + const emulateScrollY = function(distance) { + element._getElementTop.returns(element._headerTopInitial - distance); + element._updateDebounced(); + element.flushDebouncer('scroll'); + }; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.readyForMeasure = true; + element._headerTopInitial = 10; + sandbox.stub(element, '_getElementTop') + .returns(element._headerTopInitial); }); - teardown(() => { - sandbox.restore(); + test('scrolls header along with document', () => { + emulateScrollY(20); + // No top property is set when !_headerFloating. + assert.equal(getHeaderTop(), ''); }); - test('can be disabled with floatingDisabled', () => { - element.floatingDisabled = true; - sandbox.stub(element, '_reposition'); - window.dispatchEvent(new CustomEvent('resize')); - element.flushDebouncer('update'); - assert.isFalse(element._reposition.called); + test('does not stick to the top by default', () => { + emulateScrollY(150); + // No top property is set when !_headerFloating. + assert.equal(getHeaderTop(), ''); }); - test('header is the height of the content', () => { - assert.equal(element.getBoundingClientRect().height, 100); + test('sticks to the top if enabled', () => { + element.keepOnScroll = true; + emulateScrollY(120); + assert.equal(getHeaderTop(), '0px'); }); - test('scroll triggers _reposition', () => { - sandbox.stub(element, '_reposition'); - window.dispatchEvent(new CustomEvent('scroll')); - element.flushDebouncer('update'); - assert.isTrue(element._reposition.called); - }); - - suite('_reposition', () => { - const getHeaderTop = function() { - return element.$.header.style.top; - }; - - const emulateScrollY = function(distance) { - element._getElementTop.returns(element._headerTopInitial - distance); - element._updateDebounced(); - element.flushDebouncer('scroll'); - }; - - setup(() => { - element._headerTopInitial = 10; - sandbox.stub(element, '_getElementTop') - .returns(element._headerTopInitial); - }); - - test('scrolls header along with document', () => { - emulateScrollY(20); - // No top property is set when !_headerFloating. - assert.equal(getHeaderTop(), ''); - }); - - test('does not stick to the top by default', () => { - emulateScrollY(150); - // No top property is set when !_headerFloating. - assert.equal(getHeaderTop(), ''); - }); - - test('sticks to the top if enabled', () => { - element.keepOnScroll = true; - emulateScrollY(120); - assert.equal(getHeaderTop(), '0px'); - }); - - test('drops a shadow when fixed to the top', () => { - element.keepOnScroll = true; - emulateScrollY(5); - assert.isFalse(element.$.header.classList.contains('fixedAtTop')); - emulateScrollY(120); - assert.isTrue(element.$.header.classList.contains('fixedAtTop')); - }); + test('drops a shadow when fixed to the top', () => { + element.keepOnScroll = true; + emulateScrollY(5); + assert.isFalse(element.$.header.classList.contains('fixedAtTop')); + emulateScrollY(120); + assert.isTrue(element.$.header.classList.contains('fixedAtTop')); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js index e62fcc8..139e09c 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.js
@@ -14,288 +14,296 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // eslint-disable-next-line no-unused-vars - const QUOTE_MARKER_PATTERN = /\n\s?>\s/g; - const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/; +import '../gr-linked-text/gr-linked-text.js'; +import '../../../styles/shared-styles.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-formatted-text_html.js'; - /** @extends Polymer.Element */ - class GrFormattedText extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-formatted-text'; } +// eslint-disable-next-line no-unused-vars +const QUOTE_MARKER_PATTERN = /\n\s?>\s/g; +const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/; - static get properties() { - return { - content: { - type: String, - observer: '_contentChanged', - }, - config: Object, - noTrailingMargin: { - type: Boolean, - value: false, - }, - }; - } +/** @extends Polymer.Element */ +class GrFormattedText extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - static get observers() { - return [ - '_contentOrConfigChanged(content, config)', - ]; - } + static get is() { return 'gr-formatted-text'; } - /** @override */ - ready() { - super.ready(); - if (this.noTrailingMargin) { - this.classList.add('noTrailingMargin'); - } - } + static get properties() { + return { + content: { + type: String, + observer: '_contentChanged', + }, + config: Object, + noTrailingMargin: { + type: Boolean, + value: false, + }, + }; + } - _contentChanged(content) { - // In the case where the config may not be set (perhaps due to the - // request for it still being in flight), set the content anyway to - // prevent waiting on the config to display the text. - if (this.config) { return; } - this._contentOrConfigChanged(content); - } + static get observers() { + return [ + '_contentOrConfigChanged(content, config)', + ]; + } - /** - * Given a source string, update the DOM inside #container. - */ - _contentOrConfigChanged(content) { - const container = Polymer.dom(this.$.container); - - // Remove existing content. - while (container.firstChild) { - container.removeChild(container.firstChild); - } - - // Add new content. - for (const node of this._computeNodes(this._computeBlocks(content))) { - container.appendChild(node); - } - } - - /** - * Given a source string, parse into an array of block objects. Each block - * has a `type` property which takes any of the follwoing values. - * * 'paragraph' - * * 'quote' (Block quote.) - * * 'pre' (Pre-formatted text.) - * * 'list' (Unordered list.) - * * 'code' (code blocks.) - * - * For blocks of type 'paragraph', 'pre' and 'code' there is a `text` - * property that maps to a string of the block's content. - * - * For blocks of type 'list', there is an `items` property that maps to a - * list of strings representing the list items. - * - * For blocks of type 'quote', there is a `blocks` property that maps to a - * list of blocks contained in the quote. - * - * NOTE: Strings appearing in all block objects are NOT escaped. - * - * @param {string} content - * @return {!Array<!Object>} - */ - _computeBlocks(content) { - if (!content) { return []; } - - const result = []; - const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n'); - - for (let i = 0; i < lines.length; i++) { - if (!lines[i].length) { - continue; - } - - if (this._isCodeMarkLine(lines[i])) { - // handle multi-line code - let nextI = i+1; - while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) { - nextI++; - } - - if (this._isCodeMarkLine(lines[nextI])) { - result.push({ - type: 'code', - text: lines.slice(i+1, nextI).join('\n'), - }); - i = nextI; - continue; - } - - // otherwise treat it as regular line and continue - // check for other cases - } - - if (this._isSingleLineCode(lines[i])) { - // no guard check as _isSingleLineCode tested on the pattern - const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2]; - result.push({type: 'code', text: codeContent}); - } else if (this._isList(lines[i])) { - let nextI = i + 1; - while (this._isList(lines[nextI])) { - nextI++; - } - result.push(this._makeList(lines.slice(i, nextI))); - i = nextI - 1; - } else if (this._isQuote(lines[i])) { - let nextI = i + 1; - while (this._isQuote(lines[nextI])) { - nextI++; - } - const blockLines = lines.slice(i, nextI) - .map(l => l.replace(/^[ ]?>[ ]?/, '')); - result.push({ - type: 'quote', - blocks: this._computeBlocks(blockLines.join('\n')), - }); - i = nextI - 1; - } else if (this._isPreFormat(lines[i])) { - let nextI = i + 1; - // include pre or all regular lines but stop at next new line - while (this._isPreFormat(lines[nextI]) - || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) { - nextI++; - } - result.push({ - type: 'pre', - text: lines.slice(i, nextI).join('\n'), - }); - i = nextI - 1; - } else { - let nextI = i + 1; - while (this._isRegularLine(lines[nextI])) { - nextI++; - } - result.push({ - type: 'paragraph', - text: lines.slice(i, nextI).join('\n'), - }); - i = nextI - 1; - } - } - - return result; - } - - /** - * Take a block of comment text that contains a list, generate appropriate - * block objects and append them to the output list. - * - * * Item one. - * * Item two. - * * item three. - * - * TODO(taoalpha): maybe we should also support nested list - * - * @param {!Array<string>} lines The block containing the list. - */ - _makeList(lines) { - const block = {type: 'list', items: []}; - let line; - - for (let i = 0; i < lines.length; i++) { - line = lines[i]; - line = line.substring(1).trim(); - block.items.push(line); - } - return block; - } - - _isRegularLine(line) { - // line can not be recognized by existing patterns - if (line === undefined) return false; - return !this._isQuote(line) && !this._isCodeMarkLine(line) - && !this._isSingleLineCode(line) && !this._isList(line) && - !this._isPreFormat(line); - } - - _isQuote(line) { - return line && (line.startsWith('> ') || line.startsWith(' > ')); - } - - _isCodeMarkLine(line) { - return line && line.trim() === '```'; - } - - _isSingleLineCode(line) { - return line && CODE_MARKER_PATTERN.test(line); - } - - _isPreFormat(line) { - return line && /^[ \t]/.test(line); - } - - _isList(line) { - return line && /^[-*] /.test(line); - } - - /** - * @param {string} content - * @param {boolean=} opt_isPre - */ - _makeLinkedText(content, opt_isPre) { - const text = document.createElement('gr-linked-text'); - text.config = this.config; - text.content = content; - text.pre = true; - if (opt_isPre) { - text.classList.add('pre'); - } - return text; - } - - /** - * Map an array of block objects to an array of DOM nodes. - * - * @param {!Array<!Object>} blocks - * @return {!Array<!HTMLElement>} - */ - _computeNodes(blocks) { - return blocks.map(block => { - if (block.type === 'paragraph') { - const p = document.createElement('p'); - p.appendChild(this._makeLinkedText(block.text)); - return p; - } - - if (block.type === 'quote') { - const bq = document.createElement('blockquote'); - for (const node of this._computeNodes(block.blocks)) { - bq.appendChild(node); - } - return bq; - } - - if (block.type === 'code') { - const code = document.createElement('code'); - code.textContent = block.text; - return code; - } - - if (block.type === 'pre') { - return this._makeLinkedText(block.text, true); - } - - if (block.type === 'list') { - const ul = document.createElement('ul'); - for (const item of block.items) { - const li = document.createElement('li'); - li.appendChild(this._makeLinkedText(item)); - ul.appendChild(li); - } - return ul; - } - }); + /** @override */ + ready() { + super.ready(); + if (this.noTrailingMargin) { + this.classList.add('noTrailingMargin'); } } - customElements.define(GrFormattedText.is, GrFormattedText); -})(); + _contentChanged(content) { + // In the case where the config may not be set (perhaps due to the + // request for it still being in flight), set the content anyway to + // prevent waiting on the config to display the text. + if (this.config) { return; } + this._contentOrConfigChanged(content); + } + + /** + * Given a source string, update the DOM inside #container. + */ + _contentOrConfigChanged(content) { + const container = dom(this.$.container); + + // Remove existing content. + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + // Add new content. + for (const node of this._computeNodes(this._computeBlocks(content))) { + container.appendChild(node); + } + } + + /** + * Given a source string, parse into an array of block objects. Each block + * has a `type` property which takes any of the follwoing values. + * * 'paragraph' + * * 'quote' (Block quote.) + * * 'pre' (Pre-formatted text.) + * * 'list' (Unordered list.) + * * 'code' (code blocks.) + * + * For blocks of type 'paragraph', 'pre' and 'code' there is a `text` + * property that maps to a string of the block's content. + * + * For blocks of type 'list', there is an `items` property that maps to a + * list of strings representing the list items. + * + * For blocks of type 'quote', there is a `blocks` property that maps to a + * list of blocks contained in the quote. + * + * NOTE: Strings appearing in all block objects are NOT escaped. + * + * @param {string} content + * @return {!Array<!Object>} + */ + _computeBlocks(content) { + if (!content) { return []; } + + const result = []; + const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n'); + + for (let i = 0; i < lines.length; i++) { + if (!lines[i].length) { + continue; + } + + if (this._isCodeMarkLine(lines[i])) { + // handle multi-line code + let nextI = i+1; + while (!this._isCodeMarkLine(lines[nextI]) && nextI < lines.length) { + nextI++; + } + + if (this._isCodeMarkLine(lines[nextI])) { + result.push({ + type: 'code', + text: lines.slice(i+1, nextI).join('\n'), + }); + i = nextI; + continue; + } + + // otherwise treat it as regular line and continue + // check for other cases + } + + if (this._isSingleLineCode(lines[i])) { + // no guard check as _isSingleLineCode tested on the pattern + const codeContent = lines[i].match(CODE_MARKER_PATTERN)[2]; + result.push({type: 'code', text: codeContent}); + } else if (this._isList(lines[i])) { + let nextI = i + 1; + while (this._isList(lines[nextI])) { + nextI++; + } + result.push(this._makeList(lines.slice(i, nextI))); + i = nextI - 1; + } else if (this._isQuote(lines[i])) { + let nextI = i + 1; + while (this._isQuote(lines[nextI])) { + nextI++; + } + const blockLines = lines.slice(i, nextI) + .map(l => l.replace(/^[ ]?>[ ]?/, '')); + result.push({ + type: 'quote', + blocks: this._computeBlocks(blockLines.join('\n')), + }); + i = nextI - 1; + } else if (this._isPreFormat(lines[i])) { + let nextI = i + 1; + // include pre or all regular lines but stop at next new line + while (this._isPreFormat(lines[nextI]) + || (this._isRegularLine(lines[nextI]) && lines[nextI].length)) { + nextI++; + } + result.push({ + type: 'pre', + text: lines.slice(i, nextI).join('\n'), + }); + i = nextI - 1; + } else { + let nextI = i + 1; + while (this._isRegularLine(lines[nextI])) { + nextI++; + } + result.push({ + type: 'paragraph', + text: lines.slice(i, nextI).join('\n'), + }); + i = nextI - 1; + } + } + + return result; + } + + /** + * Take a block of comment text that contains a list, generate appropriate + * block objects and append them to the output list. + * + * * Item one. + * * Item two. + * * item three. + * + * TODO(taoalpha): maybe we should also support nested list + * + * @param {!Array<string>} lines The block containing the list. + */ + _makeList(lines) { + const block = {type: 'list', items: []}; + let line; + + for (let i = 0; i < lines.length; i++) { + line = lines[i]; + line = line.substring(1).trim(); + block.items.push(line); + } + return block; + } + + _isRegularLine(line) { + // line can not be recognized by existing patterns + if (line === undefined) return false; + return !this._isQuote(line) && !this._isCodeMarkLine(line) + && !this._isSingleLineCode(line) && !this._isList(line) && + !this._isPreFormat(line); + } + + _isQuote(line) { + return line && (line.startsWith('> ') || line.startsWith(' > ')); + } + + _isCodeMarkLine(line) { + return line && line.trim() === '```'; + } + + _isSingleLineCode(line) { + return line && CODE_MARKER_PATTERN.test(line); + } + + _isPreFormat(line) { + return line && /^[ \t]/.test(line); + } + + _isList(line) { + return line && /^[-*] /.test(line); + } + + /** + * @param {string} content + * @param {boolean=} opt_isPre + */ + _makeLinkedText(content, opt_isPre) { + const text = document.createElement('gr-linked-text'); + text.config = this.config; + text.content = content; + text.pre = true; + if (opt_isPre) { + text.classList.add('pre'); + } + return text; + } + + /** + * Map an array of block objects to an array of DOM nodes. + * + * @param {!Array<!Object>} blocks + * @return {!Array<!HTMLElement>} + */ + _computeNodes(blocks) { + return blocks.map(block => { + if (block.type === 'paragraph') { + const p = document.createElement('p'); + p.appendChild(this._makeLinkedText(block.text)); + return p; + } + + if (block.type === 'quote') { + const bq = document.createElement('blockquote'); + for (const node of this._computeNodes(block.blocks)) { + bq.appendChild(node); + } + return bq; + } + + if (block.type === 'code') { + const code = document.createElement('code'); + code.textContent = block.text; + return code; + } + + if (block.type === 'pre') { + return this._makeLinkedText(block.text, true); + } + + if (block.type === 'list') { + const ul = document.createElement('ul'); + for (const item of block.items) { + const li = document.createElement('li'); + li.appendChild(this._makeLinkedText(item)); + ul.appendChild(li); + } + return ul; + } + }); + } +} + +customElements.define(GrFormattedText.is, GrFormattedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js index 0c254ee..a30b65a 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../gr-linked-text/gr-linked-text.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-formatted-text"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -66,6 +63,4 @@ </style> <div id="container"></div> - </template> - <script src="gr-formatted-text.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html index a6d8524..56bd44ab 100644 --- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-editable-label</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-formatted-text.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-formatted-text.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-formatted-text.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,395 +40,397 @@ </template> </test-fixture> -<script> - suite('gr-formatted-text tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-formatted-text.js'; +suite('gr-formatted-text tests', () => { + let element; + let sandbox; - function assertBlock(result, index, type, text) { - assert.equal(result[index].type, type); - assert.equal(result[index].text, text); - } + function assertBlock(result, index, type, text) { + assert.equal(result[index].type, type); + assert.equal(result[index].text, text); + } - function assertListBlock(result, resultIndex, itemIndex, text) { - assert.equal(result[resultIndex].type, 'list'); - assert.equal(result[resultIndex].items[itemIndex], text); - } + function assertListBlock(result, resultIndex, itemIndex, text) { + assert.equal(result[resultIndex].type, 'list'); + assert.equal(result[resultIndex].items[itemIndex], text); + } - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('parse null undefined and empty', () => { - assert.lengthOf(element._computeBlocks(null), 0); - assert.lengthOf(element._computeBlocks(undefined), 0); - assert.lengthOf(element._computeBlocks(''), 0); - }); - - test('parse simple', () => { - const comment = 'Para1'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertBlock(result, 0, 'paragraph', comment); - }); - - test('parse multiline para', () => { - const comment = 'Para 1\nStill para 1'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertBlock(result, 0, 'paragraph', comment); - }); - - test('parse para break without special blocks', () => { - const comment = 'Para 1\n\nPara 2\n\nPara 3'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertBlock(result, 0, 'paragraph', comment); - }); - - test('parse quote', () => { - const comment = '> Quote text'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assert.equal(result[0].type, 'quote'); - assert.lengthOf(result[0].blocks, 1); - assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); - }); - - test('parse quote lead space', () => { - const comment = ' > Quote text'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assert.equal(result[0].type, 'quote'); - assert.lengthOf(result[0].blocks, 1); - assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); - }); - - test('parse multiline quote', () => { - const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assert.equal(result[0].type, 'quote'); - assert.lengthOf(result[0].blocks, 1); - assertBlock(result[0].blocks, 0, 'paragraph', - 'Quote line 1\nQuote line 2\nQuote line 3'); - }); - - test('parse pre', () => { - const comment = ' Four space indent.'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertBlock(result, 0, 'pre', comment); - }); - - test('parse one space pre', () => { - const comment = ' One space indent.\n Another line.'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertBlock(result, 0, 'pre', comment); - }); - - test('parse tab pre', () => { - const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertBlock(result, 0, 'pre', comment); - }); - - test('parse star list', () => { - const comment = '* Item 1\n* Item 2\n* Item 3'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertListBlock(result, 0, 0, 'Item 1'); - assertListBlock(result, 0, 1, 'Item 2'); - assertListBlock(result, 0, 2, 'Item 3'); - }); - - test('parse dash list', () => { - const comment = '- Item 1\n- Item 2\n- Item 3'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertListBlock(result, 0, 0, 'Item 1'); - assertListBlock(result, 0, 1, 'Item 2'); - assertListBlock(result, 0, 2, 'Item 3'); - }); - - test('parse mixed list', () => { - const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assertListBlock(result, 0, 0, 'Item 1'); - assertListBlock(result, 0, 1, 'Item 2'); - assertListBlock(result, 0, 2, 'Item 3'); - assertListBlock(result, 0, 3, 'Item 4'); - }); - - test('parse mixed block types', () => { - const comment = 'Paragraph\nacross\na\nfew\nlines.' + - '\n\n' + - '> Quote\n> across\n> not many lines.' + - '\n\n' + - 'Another paragraph' + - '\n\n' + - '* Series\n* of\n* list\n* items' + - '\n\n' + - 'Yet another paragraph' + - '\n\n' + - '\tPreformatted text.' + - '\n\n' + - 'Parting words.'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 7); - assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n'); - - assert.equal(result[1].type, 'quote'); - assert.lengthOf(result[1].blocks, 1); - assertBlock(result[1].blocks, 0, 'paragraph', - 'Quote\nacross\nnot many lines.'); - - assertBlock(result, 2, 'paragraph', 'Another paragraph\n'); - assertListBlock(result, 3, 0, 'Series'); - assertListBlock(result, 3, 1, 'of'); - assertListBlock(result, 3, 2, 'list'); - assertListBlock(result, 3, 3, 'items'); - assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n'); - assertBlock(result, 5, 'pre', '\tPreformatted text.'); - assertBlock(result, 6, 'paragraph', 'Parting words.'); - }); - - test('bullet list 1', () => { - const comment = 'A\n\n* line 1\n* 2nd line'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertBlock(result, 0, 'paragraph', 'A\n'); - assertListBlock(result, 1, 0, 'line 1'); - assertListBlock(result, 1, 1, '2nd line'); - }); - - test('bullet list 2', () => { - const comment = 'A\n* line 1\n* 2nd line\n\nB'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 3); - assertBlock(result, 0, 'paragraph', 'A'); - assertListBlock(result, 1, 0, 'line 1'); - assertListBlock(result, 1, 1, '2nd line'); - assertBlock(result, 2, 'paragraph', 'B'); - }); - - test('bullet list 3', () => { - const comment = '* line 1\n* 2nd line\n\nB'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertListBlock(result, 0, 0, 'line 1'); - assertListBlock(result, 0, 1, '2nd line'); - assertBlock(result, 1, 'paragraph', 'B'); - }); - - test('bullet list 4', () => { - const comment = 'To see this bug, you have to:\n' + - '* Be on IMAP or EAS (not on POP)\n' + - '* Be very unlucky\n'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:'); - assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); - assertListBlock(result, 1, 1, 'Be very unlucky'); - }); - - test('bullet list 5', () => { - const comment = 'To see this bug,\n' + - 'you have to:\n' + - '* Be on IMAP or EAS (not on POP)\n' + - '* Be very unlucky\n'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:'); - assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); - assertListBlock(result, 1, 1, 'Be very unlucky'); - }); - - test('dash list 1', () => { - const comment = 'A\n- line 1\n- 2nd line'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertBlock(result, 0, 'paragraph', 'A'); - assertListBlock(result, 1, 0, 'line 1'); - assertListBlock(result, 1, 1, '2nd line'); - }); - - test('dash list 2', () => { - const comment = 'A\n- line 1\n- 2nd line\n\nB'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 3); - assertBlock(result, 0, 'paragraph', 'A'); - assertListBlock(result, 1, 0, 'line 1'); - assertListBlock(result, 1, 1, '2nd line'); - assertBlock(result, 2, 'paragraph', 'B'); - }); - - test('dash list 3', () => { - const comment = '- line 1\n- 2nd line\n\nB'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertListBlock(result, 0, 0, 'line 1'); - assertListBlock(result, 0, 1, '2nd line'); - assertBlock(result, 1, 'paragraph', 'B'); - }); - - test('nested list will NOT be recognized', () => { - // will be rendered as two separate lists - const comment = '- line 1\n - line with indentation\n- line 2'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 3); - assertListBlock(result, 0, 0, 'line 1'); - assert.equal(result[1].type, 'pre'); - assertListBlock(result, 2, 0, 'line 2'); - }); - - test('pre format 1', () => { - const comment = 'A\n This is pre\n formatted'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertBlock(result, 0, 'paragraph', 'A'); - assertBlock(result, 1, 'pre', ' This is pre\n formatted'); - }); - - test('pre format 2', () => { - const comment = 'A\n This is pre\n formatted\n\nbut this is not'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 3); - assertBlock(result, 0, 'paragraph', 'A'); - assertBlock(result, 1, 'pre', ' This is pre\n formatted'); - assertBlock(result, 2, 'paragraph', 'but this is not'); - }); - - test('pre format 3', () => { - const comment = 'A\n Q\n <R>\n S\n\nB'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 3); - assertBlock(result, 0, 'paragraph', 'A'); - assertBlock(result, 1, 'pre', ' Q\n <R>\n S'); - assertBlock(result, 2, 'paragraph', 'B'); - }); - - test('pre format 4', () => { - const comment = ' Q\n <R>\n S\n\nB'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assertBlock(result, 0, 'pre', ' Q\n <R>\n S'); - assertBlock(result, 1, 'paragraph', 'B'); - }); - - test('quote 1', () => { - const comment = '> I\'m happy\n > with quotes!\n\nSee above.'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assert.equal(result[0].type, 'quote'); - assert.lengthOf(result[0].blocks, 1); - assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!'); - assertBlock(result, 1, 'paragraph', 'See above.'); - }); - - test('quote 2', () => { - const comment = 'See this said:\n > a quoted\n > string block\n\nOK?'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 3); - assertBlock(result, 0, 'paragraph', 'See this said:'); - assert.equal(result[1].type, 'quote'); - assert.lengthOf(result[1].blocks, 1); - assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block'); - assertBlock(result, 2, 'paragraph', 'OK?'); - }); - - test('nested quotes', () => { - const comment = ' > > prior\n > \n > next\n'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assert.equal(result[0].type, 'quote'); - assert.lengthOf(result[0].blocks, 2); - assert.equal(result[0].blocks[0].type, 'quote'); - assert.lengthOf(result[0].blocks[0].blocks, 1); - assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior'); - assertBlock(result[0].blocks, 1, 'paragraph', 'next'); - }); - - test('code 1', () => { - const comment = '```\n// test code\n```'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assert.equal(result[0].type, 'code'); - assert.equal(result[0].text, '// test code'); - }); - - test('code 2', () => { - const comment = 'test code\n```// test code```'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assert.equal(result[0].type, 'paragraph'); - assert.equal(result[0].text, 'test code'); - assert.equal(result[1].type, 'code'); - assert.equal(result[1].text, '// test code'); - }); - - test('code 3', () => { - const comment = 'test code\n```// test code```'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assert.equal(result[0].type, 'paragraph'); - assert.equal(result[0].text, 'test code'); - assert.equal(result[1].type, 'code'); - assert.equal(result[1].text, '// test code'); - }); - - test('not a code', () => { - const comment = 'test code\n```// test code'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 1); - assert.equal(result[0].type, 'paragraph'); - assert.equal(result[0].text, 'test code\n```// test code'); - }); - - test('not a code 2', () => { - const comment = 'test code\n```\n// test code'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 2); - assert.equal(result[0].type, 'paragraph'); - assert.equal(result[0].text, 'test code'); - assert.equal(result[1].type, 'paragraph'); - assert.equal(result[1].text, '```\n// test code'); - }); - - test('mix all 1', () => { - const comment = ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' + - '```// test code```\n\n> reference is here'; - const result = element._computeBlocks(comment); - assert.lengthOf(result, 5); - assert.equal(result[0].type, 'pre'); - assert.equal(result[1].type, 'list'); - assert.equal(result[2].type, 'paragraph'); - assert.equal(result[3].type, 'code'); - assert.equal(result[4].type, 'quote'); - }); - - test('_computeNodes called without config', () => { - const computeNodesSpy = sandbox.spy(element, '_computeNodes'); - element.content = 'some text'; - assert.isTrue(computeNodesSpy.called); - }); - - test('_contentOrConfigChanged called with config', () => { - const contentStub = sandbox.stub(element, '_contentChanged'); - const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); - element.content = 'some text'; - element.config = {}; - assert.isTrue(contentStub.called); - assert.isTrue(contentConfigStub.called); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('parse null undefined and empty', () => { + assert.lengthOf(element._computeBlocks(null), 0); + assert.lengthOf(element._computeBlocks(undefined), 0); + assert.lengthOf(element._computeBlocks(''), 0); + }); + + test('parse simple', () => { + const comment = 'Para1'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'paragraph', comment); + }); + + test('parse multiline para', () => { + const comment = 'Para 1\nStill para 1'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'paragraph', comment); + }); + + test('parse para break without special blocks', () => { + const comment = 'Para 1\n\nPara 2\n\nPara 3'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'paragraph', comment); + }); + + test('parse quote', () => { + const comment = '> Quote text'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); + }); + + test('parse quote lead space', () => { + const comment = ' > Quote text'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', 'Quote text'); + }); + + test('parse multiline quote', () => { + const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', + 'Quote line 1\nQuote line 2\nQuote line 3'); + }); + + test('parse pre', () => { + const comment = ' Four space indent.'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse one space pre', () => { + const comment = ' One space indent.\n Another line.'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse tab pre', () => { + const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertBlock(result, 0, 'pre', comment); + }); + + test('parse star list', () => { + const comment = '* Item 1\n* Item 2\n* Item 3'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertListBlock(result, 0, 0, 'Item 1'); + assertListBlock(result, 0, 1, 'Item 2'); + assertListBlock(result, 0, 2, 'Item 3'); + }); + + test('parse dash list', () => { + const comment = '- Item 1\n- Item 2\n- Item 3'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertListBlock(result, 0, 0, 'Item 1'); + assertListBlock(result, 0, 1, 'Item 2'); + assertListBlock(result, 0, 2, 'Item 3'); + }); + + test('parse mixed list', () => { + const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assertListBlock(result, 0, 0, 'Item 1'); + assertListBlock(result, 0, 1, 'Item 2'); + assertListBlock(result, 0, 2, 'Item 3'); + assertListBlock(result, 0, 3, 'Item 4'); + }); + + test('parse mixed block types', () => { + const comment = 'Paragraph\nacross\na\nfew\nlines.' + + '\n\n' + + '> Quote\n> across\n> not many lines.' + + '\n\n' + + 'Another paragraph' + + '\n\n' + + '* Series\n* of\n* list\n* items' + + '\n\n' + + 'Yet another paragraph' + + '\n\n' + + '\tPreformatted text.' + + '\n\n' + + 'Parting words.'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 7); + assertBlock(result, 0, 'paragraph', 'Paragraph\nacross\na\nfew\nlines.\n'); + + assert.equal(result[1].type, 'quote'); + assert.lengthOf(result[1].blocks, 1); + assertBlock(result[1].blocks, 0, 'paragraph', + 'Quote\nacross\nnot many lines.'); + + assertBlock(result, 2, 'paragraph', 'Another paragraph\n'); + assertListBlock(result, 3, 0, 'Series'); + assertListBlock(result, 3, 1, 'of'); + assertListBlock(result, 3, 2, 'list'); + assertListBlock(result, 3, 3, 'items'); + assertBlock(result, 4, 'paragraph', 'Yet another paragraph\n'); + assertBlock(result, 5, 'pre', '\tPreformatted text.'); + assertBlock(result, 6, 'paragraph', 'Parting words.'); + }); + + test('bullet list 1', () => { + const comment = 'A\n\n* line 1\n* 2nd line'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'A\n'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + }); + + test('bullet list 2', () => { + const comment = 'A\n* line 1\n* 2nd line\n\nB'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + assertBlock(result, 2, 'paragraph', 'B'); + }); + + test('bullet list 3', () => { + const comment = '* line 1\n* 2nd line\n\nB'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertListBlock(result, 0, 0, 'line 1'); + assertListBlock(result, 0, 1, '2nd line'); + assertBlock(result, 1, 'paragraph', 'B'); + }); + + test('bullet list 4', () => { + const comment = 'To see this bug, you have to:\n' + + '* Be on IMAP or EAS (not on POP)\n' + + '* Be very unlucky\n'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'To see this bug, you have to:'); + assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); + assertListBlock(result, 1, 1, 'Be very unlucky'); + }); + + test('bullet list 5', () => { + const comment = 'To see this bug,\n' + + 'you have to:\n' + + '* Be on IMAP or EAS (not on POP)\n' + + '* Be very unlucky\n'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'To see this bug,\nyou have to:'); + assertListBlock(result, 1, 0, 'Be on IMAP or EAS (not on POP)'); + assertListBlock(result, 1, 1, 'Be very unlucky'); + }); + + test('dash list 1', () => { + const comment = 'A\n- line 1\n- 2nd line'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + }); + + test('dash list 2', () => { + const comment = 'A\n- line 1\n- 2nd line\n\nB'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertListBlock(result, 1, 0, 'line 1'); + assertListBlock(result, 1, 1, '2nd line'); + assertBlock(result, 2, 'paragraph', 'B'); + }); + + test('dash list 3', () => { + const comment = '- line 1\n- 2nd line\n\nB'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertListBlock(result, 0, 0, 'line 1'); + assertListBlock(result, 0, 1, '2nd line'); + assertBlock(result, 1, 'paragraph', 'B'); + }); + + test('nested list will NOT be recognized', () => { + // will be rendered as two separate lists + const comment = '- line 1\n - line with indentation\n- line 2'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertListBlock(result, 0, 0, 'line 1'); + assert.equal(result[1].type, 'pre'); + assertListBlock(result, 2, 0, 'line 2'); + }); + + test('pre format 1', () => { + const comment = 'A\n This is pre\n formatted'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'paragraph', 'A'); + assertBlock(result, 1, 'pre', ' This is pre\n formatted'); + }); + + test('pre format 2', () => { + const comment = 'A\n This is pre\n formatted\n\nbut this is not'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertBlock(result, 1, 'pre', ' This is pre\n formatted'); + assertBlock(result, 2, 'paragraph', 'but this is not'); + }); + + test('pre format 3', () => { + const comment = 'A\n Q\n <R>\n S\n\nB'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'A'); + assertBlock(result, 1, 'pre', ' Q\n <R>\n S'); + assertBlock(result, 2, 'paragraph', 'B'); + }); + + test('pre format 4', () => { + const comment = ' Q\n <R>\n S\n\nB'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assertBlock(result, 0, 'pre', ' Q\n <R>\n S'); + assertBlock(result, 1, 'paragraph', 'B'); + }); + + test('quote 1', () => { + const comment = '> I\'m happy\n > with quotes!\n\nSee above.'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 1); + assertBlock(result[0].blocks, 0, 'paragraph', 'I\'m happy\nwith quotes!'); + assertBlock(result, 1, 'paragraph', 'See above.'); + }); + + test('quote 2', () => { + const comment = 'See this said:\n > a quoted\n > string block\n\nOK?'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 3); + assertBlock(result, 0, 'paragraph', 'See this said:'); + assert.equal(result[1].type, 'quote'); + assert.lengthOf(result[1].blocks, 1); + assertBlock(result[1].blocks, 0, 'paragraph', 'a quoted\nstring block'); + assertBlock(result, 2, 'paragraph', 'OK?'); + }); + + test('nested quotes', () => { + const comment = ' > > prior\n > \n > next\n'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'quote'); + assert.lengthOf(result[0].blocks, 2); + assert.equal(result[0].blocks[0].type, 'quote'); + assert.lengthOf(result[0].blocks[0].blocks, 1); + assertBlock(result[0].blocks[0].blocks, 0, 'paragraph', 'prior'); + assertBlock(result[0].blocks, 1, 'paragraph', 'next'); + }); + + test('code 1', () => { + const comment = '```\n// test code\n```'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'code'); + assert.equal(result[0].text, '// test code'); + }); + + test('code 2', () => { + const comment = 'test code\n```// test code```'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assert.equal(result[0].type, 'paragraph'); + assert.equal(result[0].text, 'test code'); + assert.equal(result[1].type, 'code'); + assert.equal(result[1].text, '// test code'); + }); + + test('code 3', () => { + const comment = 'test code\n```// test code```'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assert.equal(result[0].type, 'paragraph'); + assert.equal(result[0].text, 'test code'); + assert.equal(result[1].type, 'code'); + assert.equal(result[1].text, '// test code'); + }); + + test('not a code', () => { + const comment = 'test code\n```// test code'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 1); + assert.equal(result[0].type, 'paragraph'); + assert.equal(result[0].text, 'test code\n```// test code'); + }); + + test('not a code 2', () => { + const comment = 'test code\n```\n// test code'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 2); + assert.equal(result[0].type, 'paragraph'); + assert.equal(result[0].text, 'test code'); + assert.equal(result[1].type, 'paragraph'); + assert.equal(result[1].text, '```\n// test code'); + }); + + test('mix all 1', () => { + const comment = ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' + + '```// test code```\n\n> reference is here'; + const result = element._computeBlocks(comment); + assert.lengthOf(result, 5); + assert.equal(result[0].type, 'pre'); + assert.equal(result[1].type, 'list'); + assert.equal(result[2].type, 'paragraph'); + assert.equal(result[3].type, 'code'); + assert.equal(result[4].type, 'quote'); + }); + + test('_computeNodes called without config', () => { + const computeNodesSpy = sandbox.spy(element, '_computeNodes'); + element.content = 'some text'; + assert.isTrue(computeNodesSpy.called); + }); + + test('_contentOrConfigChanged called with config', () => { + const contentStub = sandbox.stub(element, '_contentChanged'); + const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); + element.content = 'some text'; + element.config = {}; + assert.isTrue(contentStub.called); + assert.isTrue(contentConfigStub.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js index ce34d3a..ce2303f 100644 --- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js +++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard.js
@@ -14,322 +14,331 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; - const HOVER_CLASS = 'hovered'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../styles/shared-styles.js'; +import '../../../scripts/rootElement.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-hovercard_html.js'; + +const HOVER_CLASS = 'hovered'; + +/** + * When the hovercard is positioned diagonally (bottom-left, bottom-right, + * top-left, or top-right), we add additional (invisible) padding so that the + * area that a user can hover over to access the hovercard is larger. + */ +const DIAGONAL_OVERFLOW = 15; + +/** @extends Polymer.Element */ +class GrHovercard extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-hovercard'; } + + static get properties() { + return { + /** + * @type {?} + */ + _target: Object, + + /** + * Determines whether or not the hovercard is visible. + * + * @type {boolean} + */ + _isShowing: { + type: Boolean, + value: false, + }, + /** + * The `id` of the element that the hovercard is anchored to. + * + * @type {string} + */ + for: { + type: String, + observer: '_forChanged', + }, + + /** + * The spacing between the top of the hovercard and the element it is + * anchored to. + * + * @type {number} + */ + offset: { + type: Number, + value: 14, + }, + + /** + * Positions the hovercard to the top, right, bottom, left, bottom-left, + * bottom-right, top-left, or top-right of its content. + * + * @type {string} + */ + position: { + type: String, + value: 'bottom', + }, + + container: Object, + /** + * ID for the container element. + * + * @type {string} + */ + containerId: { + type: String, + value: 'gr-hovercard-container', + }, + }; + } + + /** @override */ + attached() { + super.attached(); + if (!this._target) { this._target = this.target; } + this.listen(this._target, 'mouseenter', 'show'); + this.listen(this._target, 'focus', 'show'); + this.listen(this._target, 'mouseleave', 'hide'); + this.listen(this._target, 'blur', 'hide'); + this.listen(this._target, 'click', 'hide'); + } + + /** @override */ + created() { + super.created(); + this.addEventListener('mouseleave', + e => this.hide(e)); + } + + /** @override */ + ready() { + super.ready(); + // First, check to see if the container has already been created. + this.container = Gerrit.getRootElement() + .querySelector('#' + this.containerId); + + if (this.container) { return; } + + // If it does not exist, create and initialize the hovercard container. + this.container = document.createElement('div'); + this.container.setAttribute('id', this.containerId); + Gerrit.getRootElement().appendChild(this.container); + } + + removeListeners() { + this.unlisten(this._target, 'mouseenter', 'show'); + this.unlisten(this._target, 'focus', 'show'); + this.unlisten(this._target, 'mouseleave', 'hide'); + this.unlisten(this._target, 'blur', 'hide'); + this.unlisten(this._target, 'click', 'hide'); + } /** - * When the hovercard is positioned diagonally (bottom-left, bottom-right, - * top-left, or top-right), we add additional (invisible) padding so that the - * area that a user can hover over to access the hovercard is larger. + * Returns the target element that the hovercard is anchored to (the `id` of + * the `for` property). + * + * @type {HTMLElement} */ - const DIAGONAL_OVERFLOW = 15; + get target() { + const parentNode = dom(this).parentNode; + // If the parentNode is a document fragment, then we need to use the host. + const ownerRoot = dom(this).getOwnerRoot(); + let target; + if (this.for) { + target = dom(ownerRoot).querySelector('#' + this.for); + } else { + target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? + ownerRoot.host : + parentNode; + } + return target; + } - /** @extends Polymer.Element */ - class GrHovercard extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-hovercard'; } - - static get properties() { - return { - /** - * @type {?} - */ - _target: Object, - - /** - * Determines whether or not the hovercard is visible. - * - * @type {boolean} - */ - _isShowing: { - type: Boolean, - value: false, - }, - /** - * The `id` of the element that the hovercard is anchored to. - * - * @type {string} - */ - for: { - type: String, - observer: '_forChanged', - }, - - /** - * The spacing between the top of the hovercard and the element it is - * anchored to. - * - * @type {number} - */ - offset: { - type: Number, - value: 14, - }, - - /** - * Positions the hovercard to the top, right, bottom, left, bottom-left, - * bottom-right, top-left, or top-right of its content. - * - * @type {string} - */ - position: { - type: String, - value: 'bottom', - }, - - container: Object, - /** - * ID for the container element. - * - * @type {string} - */ - containerId: { - type: String, - value: 'gr-hovercard-container', - }, - }; + /** + * Hides/closes the hovercard. This occurs when the user triggers the + * `mouseleave` event on the hovercard's `target` element (as long as the + * user is not hovering over the hovercard). + * + * @param {Event} e DOM Event (e.g. `mouseleave` event) + */ + hide(e) { + const targetRect = this._target.getBoundingClientRect(); + const x = e.clientX; + const y = e.clientY; + if (x > targetRect.left && x < targetRect.right && y > targetRect.top && + y < targetRect.bottom) { + // Sometimes the hovercard itself obscures the mouse pointer, and + // that generates a mouseleave event. We don't want to hide the hovercard + // in that situation. + return; } - /** @override */ - attached() { - super.attached(); - if (!this._target) { this._target = this.target; } - this.listen(this._target, 'mouseenter', 'show'); - this.listen(this._target, 'focus', 'show'); - this.listen(this._target, 'mouseleave', 'hide'); - this.listen(this._target, 'blur', 'hide'); - this.listen(this._target, 'click', 'hide'); + // If the hovercard is already hidden or the user is now hovering over the + // hovercard or the user is returning from the hovercard but now hovering + // over the target (to stop an annoying flicker effect), just return. + if (!this._isShowing || e.toElement === this || + (e.fromElement === this && e.toElement === this._target)) { + return; } - /** @override */ - created() { - super.created(); - this.addEventListener('mouseleave', - e => this.hide(e)); - } + // Mark that the hovercard is not visible and do not allow focusing + this._isShowing = false; - /** @override */ - ready() { - super.ready(); - // First, check to see if the container has already been created. - this.container = Gerrit.getRootElement() - .querySelector('#' + this.containerId); + // Clear styles in preparation for the next time we need to show the card + this.classList.remove(HOVER_CLASS); - if (this.container) { return; } + // Reset and remove the hovercard from the DOM + this.style.cssText = ''; + this.$.hovercard.setAttribute('tabindex', -1); - // If it does not exist, create and initialize the hovercard container. - this.container = document.createElement('div'); - this.container.setAttribute('id', this.containerId); - Gerrit.getRootElement().appendChild(this.container); - } - - removeListeners() { - this.unlisten(this._target, 'mouseenter', 'show'); - this.unlisten(this._target, 'focus', 'show'); - this.unlisten(this._target, 'mouseleave', 'hide'); - this.unlisten(this._target, 'blur', 'hide'); - this.unlisten(this._target, 'click', 'hide'); - } - - /** - * Returns the target element that the hovercard is anchored to (the `id` of - * the `for` property). - * - * @type {HTMLElement} - */ - get target() { - const parentNode = Polymer.dom(this).parentNode; - // If the parentNode is a document fragment, then we need to use the host. - const ownerRoot = Polymer.dom(this).getOwnerRoot(); - let target; - if (this.for) { - target = Polymer.dom(ownerRoot).querySelector('#' + this.for); - } else { - target = parentNode.nodeType == Node.DOCUMENT_FRAGMENT_NODE ? - ownerRoot.host : - parentNode; - } - return target; - } - - /** - * Hides/closes the hovercard. This occurs when the user triggers the - * `mouseleave` event on the hovercard's `target` element (as long as the - * user is not hovering over the hovercard). - * - * @param {Event} e DOM Event (e.g. `mouseleave` event) - */ - hide(e) { - const targetRect = this._target.getBoundingClientRect(); - const x = e.clientX; - const y = e.clientY; - if (x > targetRect.left && x < targetRect.right && y > targetRect.top && - y < targetRect.bottom) { - // Sometimes the hovercard itself obscures the mouse pointer, and - // that generates a mouseleave event. We don't want to hide the hovercard - // in that situation. - return; - } - - // If the hovercard is already hidden or the user is now hovering over the - // hovercard or the user is returning from the hovercard but now hovering - // over the target (to stop an annoying flicker effect), just return. - if (!this._isShowing || e.toElement === this || - (e.fromElement === this && e.toElement === this._target)) { - return; - } - - // Mark that the hovercard is not visible and do not allow focusing - this._isShowing = false; - - // Clear styles in preparation for the next time we need to show the card - this.classList.remove(HOVER_CLASS); - - // Reset and remove the hovercard from the DOM - this.style.cssText = ''; - this.$.hovercard.setAttribute('tabindex', -1); - - // Remove the hovercard from the container, given that it is still a child - // of the container. - if (this.container.contains(this)) { - this.container.removeChild(this); - } - } - - /** - * Shows/opens the hovercard. This occurs when the user triggers the - * `mousenter` event on the hovercard's `target` element. - * - * @param {Event} e DOM Event (e.g., `mouseenter` event) - */ - show(e) { - if (this._isShowing) { - return; - } - - // Mark that the hovercard is now visible - this._isShowing = true; - this.setAttribute('tabindex', 0); - - // Add it to the DOM and calculate its position - this.container.appendChild(this); - this.updatePosition(); - - // Trigger the transition - this.classList.add(HOVER_CLASS); - } - - /** - * Updates the hovercard's position based on the `position` attribute - * and the current position of the `target` element. - * - * The hovercard is supposed to stay open if the user hovers over it. - * To keep it open when the user moves away from the target, the bounding - * rects of the target and hovercard must touch or overlap. - * - * NOTE: You do not need to directly call this method unless you need to - * update the position of the tooltip while it is already visible (the - * target element has moved and the tooltip is still open). - */ - updatePosition() { - if (!this._target) { return; } - - // Calculate the necessary measurements and positions - const parentRect = document.documentElement.getBoundingClientRect(); - const targetRect = this._target.getBoundingClientRect(); - const thisRect = this.getBoundingClientRect(); - - const targetLeft = targetRect.left - parentRect.left; - const targetTop = targetRect.top - parentRect.top; - - let hovercardLeft; - let hovercardTop; - const diagonalPadding = this.offset + DIAGONAL_OVERFLOW; - let cssText = ''; - - // Find the top and left position values based on the position attribute - // of the hovercard. - switch (this.position) { - case 'top': - hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2; - hovercardTop = targetTop - thisRect.height - this.offset; - cssText += `padding-bottom:${this.offset - }px; margin-bottom:-${this.offset}px;`; - break; - case 'bottom': - hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2; - hovercardTop = targetTop + targetRect.height + this.offset; - cssText += - `padding-top:${this.offset}px; margin-top:-${this.offset}px;`; - break; - case 'left': - hovercardLeft = targetLeft - thisRect.width - this.offset; - hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2; - cssText += - `padding-right:${this.offset}px; margin-right:-${this.offset}px;`; - break; - case 'right': - hovercardLeft = targetRect.right + this.offset; - hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2; - cssText += - `padding-left:${this.offset}px; margin-left:-${this.offset}px;`; - break; - case 'bottom-right': - hovercardLeft = targetRect.left + targetRect.width + this.offset; - hovercardTop = targetRect.top + targetRect.height + this.offset; - cssText += `padding-top:${diagonalPadding}px;`; - cssText += `padding-left:${diagonalPadding}px;`; - cssText += `margin-left:-${diagonalPadding}px;`; - cssText += `margin-top:-${diagonalPadding}px;`; - break; - case 'bottom-left': - hovercardLeft = targetRect.left - thisRect.width - this.offset; - hovercardTop = targetRect.top + targetRect.height + this.offset; - cssText += `padding-top:${diagonalPadding}px;`; - cssText += `padding-right:${diagonalPadding}px;`; - cssText += `margin-right:-${diagonalPadding}px;`; - cssText += `margin-top:-${diagonalPadding}px;`; - break; - case 'top-left': - hovercardLeft = targetRect.left - thisRect.width - this.offset; - hovercardTop = targetRect.top - thisRect.height - this.offset; - cssText += `padding-bottom:${diagonalPadding}px;`; - cssText += `padding-right:${diagonalPadding}px;`; - cssText += `margin-bottom:-${diagonalPadding}px;`; - cssText += `margin-right:-${diagonalPadding}px;`; - break; - case 'top-right': - hovercardLeft = targetRect.left + targetRect.width + this.offset; - hovercardTop = targetRect.top - thisRect.height - this.offset; - cssText += `padding-bottom:${diagonalPadding}px;`; - cssText += `padding-left:${diagonalPadding}px;`; - cssText += `margin-bottom:-${diagonalPadding}px;`; - cssText += `margin-left:-${diagonalPadding}px;`; - break; - } - - // Prevent hovercard from appearing outside the viewport. - // TODO(kaspern): fix hovercard appearing outside viewport on bottom and - // right. - if (hovercardLeft < 0) { hovercardLeft = 0; } - if (hovercardTop < 0) { hovercardTop = 0; } - // Set the hovercard's position - cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`; - this.style.cssText = cssText; - } - - /** - * Responds to a change in the `for` value and gets the updated `target` - * element for the hovercard. - * - * @private - */ - _forChanged() { - this._target = this.target; + // Remove the hovercard from the container, given that it is still a child + // of the container. + if (this.container.contains(this)) { + this.container.removeChild(this); } } - customElements.define(GrHovercard.is, GrHovercard); -})(); + /** + * Shows/opens the hovercard. This occurs when the user triggers the + * `mousenter` event on the hovercard's `target` element. + * + * @param {Event} e DOM Event (e.g., `mouseenter` event) + */ + show(e) { + if (this._isShowing) { + return; + } + + // Mark that the hovercard is now visible + this._isShowing = true; + this.setAttribute('tabindex', 0); + + // Add it to the DOM and calculate its position + this.container.appendChild(this); + this.updatePosition(); + + // Trigger the transition + this.classList.add(HOVER_CLASS); + } + + /** + * Updates the hovercard's position based on the `position` attribute + * and the current position of the `target` element. + * + * The hovercard is supposed to stay open if the user hovers over it. + * To keep it open when the user moves away from the target, the bounding + * rects of the target and hovercard must touch or overlap. + * + * NOTE: You do not need to directly call this method unless you need to + * update the position of the tooltip while it is already visible (the + * target element has moved and the tooltip is still open). + */ + updatePosition() { + if (!this._target) { return; } + + // Calculate the necessary measurements and positions + const parentRect = document.documentElement.getBoundingClientRect(); + const targetRect = this._target.getBoundingClientRect(); + const thisRect = this.getBoundingClientRect(); + + const targetLeft = targetRect.left - parentRect.left; + const targetTop = targetRect.top - parentRect.top; + + let hovercardLeft; + let hovercardTop; + const diagonalPadding = this.offset + DIAGONAL_OVERFLOW; + let cssText = ''; + + // Find the top and left position values based on the position attribute + // of the hovercard. + switch (this.position) { + case 'top': + hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2; + hovercardTop = targetTop - thisRect.height - this.offset; + cssText += `padding-bottom:${this.offset + }px; margin-bottom:-${this.offset}px;`; + break; + case 'bottom': + hovercardLeft = targetLeft + (targetRect.width - thisRect.width) / 2; + hovercardTop = targetTop + targetRect.height + this.offset; + cssText += + `padding-top:${this.offset}px; margin-top:-${this.offset}px;`; + break; + case 'left': + hovercardLeft = targetLeft - thisRect.width - this.offset; + hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2; + cssText += + `padding-right:${this.offset}px; margin-right:-${this.offset}px;`; + break; + case 'right': + hovercardLeft = targetRect.right + this.offset; + hovercardTop = targetTop + (targetRect.height - thisRect.height) / 2; + cssText += + `padding-left:${this.offset}px; margin-left:-${this.offset}px;`; + break; + case 'bottom-right': + hovercardLeft = targetRect.left + targetRect.width + this.offset; + hovercardTop = targetRect.top + targetRect.height + this.offset; + cssText += `padding-top:${diagonalPadding}px;`; + cssText += `padding-left:${diagonalPadding}px;`; + cssText += `margin-left:-${diagonalPadding}px;`; + cssText += `margin-top:-${diagonalPadding}px;`; + break; + case 'bottom-left': + hovercardLeft = targetRect.left - thisRect.width - this.offset; + hovercardTop = targetRect.top + targetRect.height + this.offset; + cssText += `padding-top:${diagonalPadding}px;`; + cssText += `padding-right:${diagonalPadding}px;`; + cssText += `margin-right:-${diagonalPadding}px;`; + cssText += `margin-top:-${diagonalPadding}px;`; + break; + case 'top-left': + hovercardLeft = targetRect.left - thisRect.width - this.offset; + hovercardTop = targetRect.top - thisRect.height - this.offset; + cssText += `padding-bottom:${diagonalPadding}px;`; + cssText += `padding-right:${diagonalPadding}px;`; + cssText += `margin-bottom:-${diagonalPadding}px;`; + cssText += `margin-right:-${diagonalPadding}px;`; + break; + case 'top-right': + hovercardLeft = targetRect.left + targetRect.width + this.offset; + hovercardTop = targetRect.top - thisRect.height - this.offset; + cssText += `padding-bottom:${diagonalPadding}px;`; + cssText += `padding-left:${diagonalPadding}px;`; + cssText += `margin-bottom:-${diagonalPadding}px;`; + cssText += `margin-left:-${diagonalPadding}px;`; + break; + } + + // Prevent hovercard from appearing outside the viewport. + // TODO(kaspern): fix hovercard appearing outside viewport on bottom and + // right. + if (hovercardLeft < 0) { hovercardLeft = 0; } + if (hovercardTop < 0) { hovercardTop = 0; } + // Set the hovercard's position + cssText += `left:${hovercardLeft}px; top:${hovercardTop}px;`; + this.style.cssText = cssText; + } + + /** + * Responds to a change in the `for` value and gets the updated `target` + * element for the hovercard. + * + * @private + */ + _forChanged() { + this._target = this.target; + } +} + +customElements.define(GrHovercard.is, GrHovercard);
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js index c666227..2969bdb 100644 --- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js +++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../styles/shared-styles.html"> -<script src="../../../scripts/rootElement.js"></script> - -<dom-module id="gr-hovercard"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { box-sizing: border-box; @@ -44,6 +39,4 @@ <div id="hovercard" role="tooltip" tabindex="-1"> <slot></slot> </div> - </template> - <script src="gr-hovercard.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html index e091fcf..ed087ca 100644 --- a/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html +++ b/polygerrit-ui/app/elements/shared/gr-hovercard/gr-hovercard_test.html
@@ -19,12 +19,12 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-hovercard</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> <!-- Can't use absolute path below for mock-interaction.js. Web component tester(wct) has a built-in http server and it serves "/components" directory (which is actually /node_modules directory). Also, wct patches some files to load modules from /components. @@ -33,9 +33,14 @@ --> <script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script> -<link rel="import" href="gr-hovercard.html"> +<script type="module" src="./gr-hovercard.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-hovercard.js'; +void(0); +</script> <button id="foo">Hello</button> <test-fixture id="basic"> @@ -44,87 +49,90 @@ </template> </test-fixture> -<script> - suite('gr-hovercard tests', async () => { - await readyToTest(); - let element; - let sandbox; - // For css animations - const TRANSITION_TIME = 500; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-hovercard.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-hovercard tests', () => { + let element; + let sandbox; + // For css animations + const TRANSITION_TIME = 500; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('updatePosition', () => { - // Test that the correct style properties have at least been set. - element.position = 'bottom'; - element.updatePosition(); - assert.typeOf(element.style.getPropertyValue('left'), 'string'); - assert.typeOf(element.style.getPropertyValue('top'), 'string'); - assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string'); - assert.typeOf(element.style.getPropertyValue('marginTop'), 'string'); - - const parentRect = document.documentElement.getBoundingClientRect(); - const targetRect = element._target.getBoundingClientRect(); - const thisRect = element.getBoundingClientRect(); - - const targetLeft = targetRect.left - parentRect.left; - const targetTop = targetRect.top - parentRect.top; - - const pixelCompare = pixel => - Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10); - - assert.equal( - pixelCompare(element.style.left), - pixelCompare( - (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px')); - assert.equal( - pixelCompare(element.style.top), - pixelCompare( - (targetTop + targetRect.height + element.offset) + 'px')); - }); - - test('hide', done => { - element.hide({}); - setTimeout(() => { - const style = getComputedStyle(element); - assert.isFalse(element._isShowing); - assert.isFalse(element.classList.contains('hovered')); - assert.equal(style.opacity, '0'); - assert.equal(style.visibility, 'hidden'); - assert.notEqual(element.container, Polymer.dom(element).parentNode); - done(); - }, TRANSITION_TIME); - }); - - test('show', done => { - element.show({}); - setTimeout(() => { - const style = getComputedStyle(element); - assert.isTrue(element._isShowing); - assert.isTrue(element.classList.contains('hovered')); - assert.equal(style.opacity, '1'); - assert.equal(style.visibility, 'visible'); - done(); - }, TRANSITION_TIME); - }); - - test('card shows on enter and hides on leave', done => { - const button = Polymer.dom(document).querySelector('button'); - assert.isFalse(element._isShowing); - button.addEventListener('mouseenter', event => { - assert.isTrue(element._isShowing); - button.dispatchEvent(new CustomEvent('mouseleave')); - }); - button.addEventListener('mouseleave', event => { - assert.isFalse(element._isShowing); - done(); - }); - button.dispatchEvent(new CustomEvent('mouseenter')); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { sandbox.restore(); }); + + test('updatePosition', () => { + // Test that the correct style properties have at least been set. + element.position = 'bottom'; + element.updatePosition(); + assert.typeOf(element.style.getPropertyValue('left'), 'string'); + assert.typeOf(element.style.getPropertyValue('top'), 'string'); + assert.typeOf(element.style.getPropertyValue('paddingTop'), 'string'); + assert.typeOf(element.style.getPropertyValue('marginTop'), 'string'); + + const parentRect = document.documentElement.getBoundingClientRect(); + const targetRect = element._target.getBoundingClientRect(); + const thisRect = element.getBoundingClientRect(); + + const targetLeft = targetRect.left - parentRect.left; + const targetTop = targetRect.top - parentRect.top; + + const pixelCompare = pixel => + Math.round(parseInt(pixel.substring(0, pixel.length - 1)), 10); + + assert.equal( + pixelCompare(element.style.left), + pixelCompare( + (targetLeft + (targetRect.width - thisRect.width) / 2) + 'px')); + assert.equal( + pixelCompare(element.style.top), + pixelCompare( + (targetTop + targetRect.height + element.offset) + 'px')); + }); + + test('hide', done => { + element.hide({}); + setTimeout(() => { + const style = getComputedStyle(element); + assert.isFalse(element._isShowing); + assert.isFalse(element.classList.contains('hovered')); + assert.equal(style.opacity, '0'); + assert.equal(style.visibility, 'hidden'); + assert.notEqual(element.container, dom(element).parentNode); + done(); + }, TRANSITION_TIME); + }); + + test('show', done => { + element.show({}); + setTimeout(() => { + const style = getComputedStyle(element); + assert.isTrue(element._isShowing); + assert.isTrue(element.classList.contains('hovered')); + assert.equal(style.opacity, '1'); + assert.equal(style.visibility, 'visible'); + done(); + }, TRANSITION_TIME); + }); + + test('card shows on enter and hides on leave', done => { + const button = dom(document).querySelector('button'); + assert.isFalse(element._isShowing); + button.addEventListener('mouseenter', event => { + assert.isTrue(element._isShowing); + button.dispatchEvent(new CustomEvent('mouseleave')); + }); + button.addEventListener('mouseleave', event => { + assert.isFalse(element._isShowing); + done(); + }); + button.dispatchEvent(new CustomEvent('mouseenter')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js index 2245b98..5d7da6c 100644 --- a/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js +++ b/polygerrit-ui/app/elements/shared/gr-icons/gr-icons.js
@@ -1,75 +1,76 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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 '@polymer/iron-icon/iron-icon.js'; +import '@polymer/iron-iconset-svg/iron-iconset-svg.js'; +const $_documentContainer = document.createElement('template'); -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. ---> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> -<link rel="import" href="/bower_components/iron-iconset-svg/iron-iconset-svg.html"> - -<iron-iconset-svg name="gr-icons" size="24"> +$_documentContainer.innerHTML = `<iron-iconset-svg name="gr-icons" size="24"> <svg> <defs> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/></g> + <g id="expand-less"><path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/></g> + <g id="expand-more"><path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path></g> <!-- This SVG is a copy from material.io https://material.io/icons/#unfold_more --> - <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"/></g> + <g id="unfold-more"><path d="M0 0h24v24H0z" fill="none"></path><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></g> + <g id="search"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/></g> + <g id="settings"><path d="M19.43 12.98c.04-.32.07-.64.07-.98s-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98s.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></g> + <g id="create"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></g> + <g id="star"><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></g> + <g id="star-border"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></g> + <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></g> + <g id="chevron-left"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></g> + <g id="chevron-right"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></g> + <g id="more-vert"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g> + <g id="deleteEdit"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html --> - <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"/></g> + <g id="publishEdit"><path d="M5 4v2h14V4H5zm0 10h4v6h6v-6h4l-7-7-7 7z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html --> - <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></g> + <g id="delete"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> <g id="help"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> <g id="info"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"></path></g> <!-- This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full--> - <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"/><path d="M0 0h24v24H0V0z" fill="none"/></g> + <g id="hourglass"><path d="M6 2v6h.01L6 8.01 10 12l-4 4 .01.01H6V22h12v-5.99h-.01L18 16l-4-4 4-3.99-.01-.01H18V2H6z"></path><path d="M0 0h24v24H0V0z" fill="none"></path></g> <!-- This SVG is a copy from material.io https://material.io/icons/#mode_comment--> - <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"/><path d="M0 0h24v24H0z" fill="none"/></g> + <g id="comment"><path d="M21.99 4c0-1.1-.89-2-1.99-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h14l4 4-.01-18z"></path><path d="M0 0h24v24H0z" fill="none"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> <g id="error"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> <g id="lightbulb-outline"><path d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6C7.8 12.16 7 10.63 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"></path></g> <!-- This is a custom PolyGerrit SVG --> - <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"/></g> + <g id="side-by-side"><path d="M17.1578947,10.8888889 L2.84210526,10.8888889 C2.37894737,10.8888889 2,11.2888889 2,11.7777778 L2,17.1111111 C2,17.6 2.37894737,18 2.84210526,18 L17.1578947,18 C17.6210526,18 18,17.6 18,17.1111111 L18,11.7777778 C18,11.2888889 17.6210526,10.8888889 17.1578947,10.8888889 Z M17.1578947,2 L2.84210526,2 C2.37894737,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.37894737,9.11111111 2.84210526,9.11111111 L17.1578947,9.11111111 C17.6210526,9.11111111 18,8.71111111 18,8.22222222 L18,2.88888889 C18,2.4 17.6210526,2 17.1578947,2 Z M16.1973628,2 L2.78874238,2 C2.35493407,2 2,2.4 2,2.88888889 L2,8.22222222 C2,8.71111111 2.35493407,9.11111111 2.78874238,9.11111111 L16.1973628,9.11111111 C16.6311711,9.11111111 16.9861052,8.71111111 16.9861052,8.22222222 L16.9861052,2.88888889 C16.9861052,2.4 16.6311711,2 16.1973628,2 Z" id="Shape" transform="scale(1.2) translate(10.000000, 10.000000) rotate(-90.000000) translate(-10.000000, -10.000000)"></path></g> <!-- This is a custom PolyGerrit SVG --> - <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"/></g> + <g id="unified"><path d="M4,2 L17,2 C18.1045695,2 19,2.8954305 19,4 L19,16 C19,17.1045695 18.1045695,18 17,18 L4,18 C2.8954305,18 2,17.1045695 2,16 L2,4 L2,4 C2,2.8954305 2.8954305,2 4,2 L4,2 Z M4,7 L4,9 L17,9 L17,7 L4,7 Z M4,11 L4,13 L17,13 L17,11 L4,11 Z" id="Combined-Shape" transform="scale(1.12, 1.2)"></path></g> <!-- This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js --> - <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></g> + <g id="content-copy"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path></g> <!-- This is a custom PolyGerrit SVG --> - <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g> + <g id="check"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g> <!-- This is a custom PolyGerrit SVG --> <g id="robot"><path d="M4.137453,5.61015591 L4.54835569,1.5340419 C4.5717665,1.30180904 4.76724872,1.12504213 5.00065859,1.12504213 C5.23327176,1.12504213 5.42730868,1.30282046 5.44761309,1.53454578 L5.76084628,5.10933916 C6.16304484,5.03749412 6.57714381,5 7,5 L17,5 C20.8659932,5 24,8.13400675 24,12 L24,15.1250421 C24,18.9910354 20.8659932,22.1250421 17,22.1250421 L7,22.1250421 C3.13400675,22.1250421 2.19029351e-15,18.9910354 0,15.1250421 L0,12 C-3.48556243e-16,9.15382228 1.69864167,6.70438358 4.137453,5.61015591 Z M5.77553049,6.12504213 C3.04904264,6.69038358 1,9.10590202 1,12 L1,15.1250421 C1,18.4387506 3.6862915,21.1250421 7,21.1250421 L17,21.1250421 C20.3137085,21.1250421 23,18.4387506 23,15.1250421 L23,12 C23,8.6862915 20.3137085,6 17,6 L7,6 C6.60617231,6 6.2212068,6.03794347 5.84855971,6.11037415 L5.84984496,6.12504213 L5.77553049,6.12504213 Z M6.93003717,6.95027711 L17.1232083,6.95027711 C19.8638332,6.95027711 22.0855486,9.17199258 22.0855486,11.9126175 C22.0855486,14.6532424 19.8638332,16.8749579 17.1232083,16.8749579 L6.93003717,16.8749579 C4.18941226,16.8749579 1.9676968,14.6532424 1.9676968,11.9126175 C1.9676968,9.17199258 4.18941226,6.95027711 6.93003717,6.95027711 Z M7.60124392,14.0779303 C9.03787127,14.0779303 10.2024878,12.9691885 10.2024878,11.6014862 C10.2024878,10.2337839 9.03787127,9.12504213 7.60124392,9.12504213 C6.16461657,9.12504213 5,10.2337839 5,11.6014862 C5,12.9691885 6.16461657,14.0779303 7.60124392,14.0779303 Z M16.617997,14.1098288 C18.0638768,14.1098288 19.2359939,12.9939463 19.2359939,11.6174355 C19.2359939,10.2409246 18.0638768,9.12504213 16.617997,9.12504213 C15.1721172,9.12504213 14,10.2409246 14,11.6174355 C14,12.9939463 15.1721172,14.1098288 16.617997,14.1098288 Z M9.79751216,18.1250421 L15,18.1250421 L15,19.1250421 C15,19.6773269 14.5522847,20.1250421 14,20.1250421 L10.7975122,20.1250421 C10.2452274,20.1250421 9.79751216,19.6773269 9.79751216,19.1250421 L9.79751216,18.1250421 Z"></path></g> <!-- This is a custom PolyGerrit SVG --> @@ -90,9 +91,54 @@ <!-- This is a custom PolyGerrit SVG --> <g id="submit"><path d="M22.23,5 L11.65,15.58 L7.47000001,11.41 L6.06000001,12.82 L11.65,18.41 L23.649,6.41 L22.23,5 Z M16.58,5 L10.239,11.34 L11.65,12.75 L17.989,6.41 L16.58,5 Z M0.400000006,12.82 L5.99000001,18.41 L7.40000001,17 L1.82000001,11.41 L0.400000006,12.82 Z"></path></g> <!-- This is a custom PolyGerrit SVG --> - <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></g> + <g id="review"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"></path></g> <!-- This is a custom PolyGerrit SVG --> - <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"/></g> + <g id="zeroState"><path d="M22 9V7h-2V5c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-2h2v-2h-2v-2h2v-2h-2V9h2zm-4 10H4V5h14v14zM6 13h5v4H6zm6-6h4v3h-4zM6 7h5v5H6zm6 4h4v6h-4z"></path></g> </defs> </svg> -</iron-iconset-svg> +</iron-iconset-svg>`; + +document.head.appendChild($_documentContainer.content); + +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from material.io https://material.io/icons/#unfold_more */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/editor-icons.html */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from material.io https://material.io/icons/#ic_hourglass_full*/ +/* This SVG is a copy from material.io https://material.io/icons/#mode_comment*/ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This SVG is a copy from iron-icons https://github.com/PolymerElements/iron-icons/blob/master/iron-icons.js */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* This is a custom PolyGerrit SVG */ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html index ea5740f..c044327 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-annotation-actions-context</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../diff/gr-diff-highlight/gr-annotation.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../diff/gr-diff-highlight/gr-annotation.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"/> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../diff/gr-diff-highlight/gr-annotation.js'; +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,68 +43,71 @@ </template> </test-fixture> -<script> - suite('gr-annotation-actions-context tests', async () => { - await readyToTest(); - let instance; - let sandbox; - let el; - let lineNumberEl; - let plugin; +<script type="module"> +import '../../diff/gr-diff-highlight/gr-annotation.js'; +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +suite('gr-annotation-actions-context tests', () => { + let instance; + let sandbox; + let el; + let lineNumberEl; + let plugin; - setup(() => { - sandbox = sinon.sandbox.create(); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); + setup(() => { + sandbox = sinon.sandbox.create(); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); - const str = 'lorem ipsum blah blah'; - const line = {text: str}; - el = document.createElement('div'); - el.textContent = str; - el.setAttribute('data-side', 'right'); - lineNumberEl = document.createElement('td'); - lineNumberEl.classList.add('right'); - document.body.appendChild(el); - instance = new GrAnnotationActionsContext( - el, lineNumberEl, line, 'dummy/path', '123', '1'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('test annotateRange', () => { - const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement'); - const start = 0; - const end = 100; - const cssStyleObject = plugin.styles().css('background-color: #000000'); - - // Assert annotateElement is not called when side is different. - instance.annotateRange(start, end, cssStyleObject, 'left'); - assert.equal(annotateElementSpy.callCount, 0); - - // Assert annotateElement is called once when side is the same. - instance.annotateRange(start, end, cssStyleObject, 'right'); - assert.equal(annotateElementSpy.callCount, 1); - const args = annotateElementSpy.getCalls()[0].args; - assert.equal(args[0], el); - assert.equal(args[1], start); - assert.equal(args[2], end); - assert.equal(args[3], cssStyleObject.getClassName(el)); - }); - - test('test annotateLineNumber', () => { - const cssStyleObject = plugin.styles().css('background-color: #000000'); - - const className = cssStyleObject.getClassName(lineNumberEl); - - // Assert that css class is *not* applied when side is different. - instance.annotateLineNumber(cssStyleObject, 'left'); - assert.isFalse(lineNumberEl.classList.contains(className)); - - // Assert that css class is applied when side is the same. - instance.annotateLineNumber(cssStyleObject, 'right'); - assert.isTrue(lineNumberEl.classList.contains(className)); - }); + const str = 'lorem ipsum blah blah'; + const line = {text: str}; + el = document.createElement('div'); + el.textContent = str; + el.setAttribute('data-side', 'right'); + lineNumberEl = document.createElement('td'); + lineNumberEl.classList.add('right'); + document.body.appendChild(el); + instance = new GrAnnotationActionsContext( + el, lineNumberEl, line, 'dummy/path', '123', '1'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('test annotateRange', () => { + const annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement'); + const start = 0; + const end = 100; + const cssStyleObject = plugin.styles().css('background-color: #000000'); + + // Assert annotateElement is not called when side is different. + instance.annotateRange(start, end, cssStyleObject, 'left'); + assert.equal(annotateElementSpy.callCount, 0); + + // Assert annotateElement is called once when side is the same. + instance.annotateRange(start, end, cssStyleObject, 'right'); + assert.equal(annotateElementSpy.callCount, 1); + const args = annotateElementSpy.getCalls()[0].args; + assert.equal(args[0], el); + assert.equal(args[1], start); + assert.equal(args[2], end); + assert.equal(args[3], cssStyleObject.getClassName(el)); + }); + + test('test annotateLineNumber', () => { + const cssStyleObject = plugin.styles().css('background-color: #000000'); + + const className = cssStyleObject.getClassName(lineNumberEl); + + // Assert that css class is *not* applied when side is different. + instance.annotateLineNumber(cssStyleObject, 'left'); + assert.isFalse(lineNumberEl.classList.contains(className)); + + // Assert that css class is applied when side is the same. + instance.annotateLineNumber(cssStyleObject, 'right'); + assert.isTrue(lineNumberEl.classList.contains(className)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html index b8c4f83..061f22c 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-js-api_test.html
@@ -19,13 +19,13 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-annotation-actions-js-api-js-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../change/gr-change-actions/gr-change-actions.js"></script> <test-fixture id="basic"> <template> @@ -38,153 +38,155 @@ </template> </test-fixture> -<script> - suite('gr-annotation-actions-js-api tests', async () => { - await readyToTest(); - let annotationActions; - let sandbox; - let plugin; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../change/gr-change-actions/gr-change-actions.js'; +suite('gr-annotation-actions-js-api tests', () => { + let annotationActions; + let sandbox; + let plugin; - setup(() => { - sandbox = sinon.sandbox.create(); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - annotationActions = plugin.annotationApi(); - }); - - teardown(() => { - annotationActions = null; - sandbox.restore(); - }); - - test('add/get layer', () => { - const str = 'lorem ipsum blah blah'; - const line = {text: str}; - const el = document.createElement('div'); - el.textContent = str; - const changeNum = 1234; - const patchNum = 2; - let testLayerFuncCalled = false; - - const testLayerFunc = context => { - testLayerFuncCalled = true; - assert.equal(context.line, line); - assert.equal(context.changeNum, changeNum); - assert.equal(context.patchNum, 2); - }; - annotationActions.addLayer(testLayerFunc); - - const annotationLayer = annotationActions.getLayer( - '/dummy/path', changeNum, patchNum); - - const lineNumberEl = document.createElement('td'); - annotationLayer.annotate(el, lineNumberEl, line); - assert.isTrue(testLayerFuncCalled); - }); - - test('add notifier', () => { - const path1 = '/dummy/path1'; - const path2 = '/dummy/path2'; - const annotationLayer1 = annotationActions.getLayer(path1, 1, 2); - const annotationLayer2 = annotationActions.getLayer(path2, 1, 2); - const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners'); - const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners'); - - let notify; - let notifyFuncCalled; - const notifyFunc = n => { - notifyFuncCalled = true; - notify = n; - }; - annotationActions.addNotifier(notifyFunc); - assert.isTrue(notifyFuncCalled); - - // Assert that no layers are invoked with a different path. - notify('/dummy/path3', 0, 10, 'right'); - assert.isFalse(layer1Spy.called); - assert.isFalse(layer2Spy.called); - - // Assert that only the 1st layer is invoked with path1. - notify(path1, 0, 10, 'right'); - assert.isTrue(layer1Spy.called); - assert.isFalse(layer2Spy.called); - - // Reset spies. - layer1Spy.reset(); - layer2Spy.reset(); - - // Assert that only the 2nd layer is invoked with path2. - notify(path2, 0, 20, 'left'); - assert.isFalse(layer1Spy.called); - assert.isTrue(layer2Spy.called); - }); - - test('toggle checkbox', () => { - const fakeEl = {content: fixture('basic')}; - const hookStub = {onAttached: sandbox.stub()}; - sandbox.stub(plugin, 'hook').returns(hookStub); - - let checkbox; - let onAttachedFuncCalled = false; - const onAttachedFunc = c => { - checkbox = c; - onAttachedFuncCalled = true; - }; - annotationActions.enableToggleCheckbox('test label', onAttachedFunc); - const emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl); - emulateAttached(); - - // Assert that onAttachedFunc is called and HTML elements have the - // expected state. - assert.isTrue(onAttachedFuncCalled); - assert.equal(checkbox.id, 'annotation-checkbox'); - assert.isTrue(checkbox.disabled); - assert.equal(document.getElementById('annotation-label').textContent, - 'test label'); - assert.isFalse(document.getElementById('annotation-span').hidden); - - // Assert that error is shown if we try to enable checkbox again. - onAttachedFuncCalled = false; - annotationActions.enableToggleCheckbox('test label2', onAttachedFunc); - const errorStub = sandbox.stub( - console, 'error', (msg, err) => undefined); - emulateAttached(); - assert.isTrue( - errorStub.calledWith( - 'annotation-span is already enabled. Cannot re-enable.')); - // Assert that onAttachedFunc is not called and the label has not changed. - assert.isFalse(onAttachedFuncCalled); - assert.equal(document.getElementById('annotation-label').textContent, - 'test label'); - }); - - test('layer notify listeners', () => { - const annotationLayer = annotationActions.getLayer( - '/dummy/path', 1, 2); - let listenerCalledTimes = 0; - const startRange = 10; - const endRange = 20; - const side = 'right'; - const listener = (st, end, s) => { - listenerCalledTimes++; - assert.equal(st, startRange); - assert.equal(end, endRange); - assert.equal(s, side); - }; - - // Notify with 0 listeners added. - annotationLayer.notifyListeners(startRange, endRange, side); - assert.equal(listenerCalledTimes, 0); - - // Add 1 listener. - annotationLayer.addListener(listener); - annotationLayer.notifyListeners(startRange, endRange, side); - assert.equal(listenerCalledTimes, 1); - - // Add 1 more listener. Total 2 listeners. - annotationLayer.addListener(listener); - annotationLayer.notifyListeners(startRange, endRange, side); - assert.equal(listenerCalledTimes, 3); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + annotationActions = plugin.annotationApi(); }); + + teardown(() => { + annotationActions = null; + sandbox.restore(); + }); + + test('add/get layer', () => { + const str = 'lorem ipsum blah blah'; + const line = {text: str}; + const el = document.createElement('div'); + el.textContent = str; + const changeNum = 1234; + const patchNum = 2; + let testLayerFuncCalled = false; + + const testLayerFunc = context => { + testLayerFuncCalled = true; + assert.equal(context.line, line); + assert.equal(context.changeNum, changeNum); + assert.equal(context.patchNum, 2); + }; + annotationActions.addLayer(testLayerFunc); + + const annotationLayer = annotationActions.getLayer( + '/dummy/path', changeNum, patchNum); + + const lineNumberEl = document.createElement('td'); + annotationLayer.annotate(el, lineNumberEl, line); + assert.isTrue(testLayerFuncCalled); + }); + + test('add notifier', () => { + const path1 = '/dummy/path1'; + const path2 = '/dummy/path2'; + const annotationLayer1 = annotationActions.getLayer(path1, 1, 2); + const annotationLayer2 = annotationActions.getLayer(path2, 1, 2); + const layer1Spy = sandbox.spy(annotationLayer1, 'notifyListeners'); + const layer2Spy = sandbox.spy(annotationLayer2, 'notifyListeners'); + + let notify; + let notifyFuncCalled; + const notifyFunc = n => { + notifyFuncCalled = true; + notify = n; + }; + annotationActions.addNotifier(notifyFunc); + assert.isTrue(notifyFuncCalled); + + // Assert that no layers are invoked with a different path. + notify('/dummy/path3', 0, 10, 'right'); + assert.isFalse(layer1Spy.called); + assert.isFalse(layer2Spy.called); + + // Assert that only the 1st layer is invoked with path1. + notify(path1, 0, 10, 'right'); + assert.isTrue(layer1Spy.called); + assert.isFalse(layer2Spy.called); + + // Reset spies. + layer1Spy.reset(); + layer2Spy.reset(); + + // Assert that only the 2nd layer is invoked with path2. + notify(path2, 0, 20, 'left'); + assert.isFalse(layer1Spy.called); + assert.isTrue(layer2Spy.called); + }); + + test('toggle checkbox', () => { + const fakeEl = {content: fixture('basic')}; + const hookStub = {onAttached: sandbox.stub()}; + sandbox.stub(plugin, 'hook').returns(hookStub); + + let checkbox; + let onAttachedFuncCalled = false; + const onAttachedFunc = c => { + checkbox = c; + onAttachedFuncCalled = true; + }; + annotationActions.enableToggleCheckbox('test label', onAttachedFunc); + const emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl); + emulateAttached(); + + // Assert that onAttachedFunc is called and HTML elements have the + // expected state. + assert.isTrue(onAttachedFuncCalled); + assert.equal(checkbox.id, 'annotation-checkbox'); + assert.isTrue(checkbox.disabled); + assert.equal(document.getElementById('annotation-label').textContent, + 'test label'); + assert.isFalse(document.getElementById('annotation-span').hidden); + + // Assert that error is shown if we try to enable checkbox again. + onAttachedFuncCalled = false; + annotationActions.enableToggleCheckbox('test label2', onAttachedFunc); + const errorStub = sandbox.stub( + console, 'error', (msg, err) => undefined); + emulateAttached(); + assert.isTrue( + errorStub.calledWith( + 'annotation-span is already enabled. Cannot re-enable.')); + // Assert that onAttachedFunc is not called and the label has not changed. + assert.isFalse(onAttachedFuncCalled); + assert.equal(document.getElementById('annotation-label').textContent, + 'test label'); + }); + + test('layer notify listeners', () => { + const annotationLayer = annotationActions.getLayer( + '/dummy/path', 1, 2); + let listenerCalledTimes = 0; + const startRange = 10; + const endRange = 20; + const side = 'right'; + const listener = (st, end, s) => { + listenerCalledTimes++; + assert.equal(st, startRange); + assert.equal(end, endRange); + assert.equal(s, side); + }; + + // Notify with 0 listeners added. + annotationLayer.notifyListeners(startRange, endRange, side); + assert.equal(listenerCalledTimes, 0); + + // Add 1 listener. + annotationLayer.addListener(listener); + annotationLayer.notifyListeners(startRange, endRange, side); + assert.equal(listenerCalledTimes, 1); + + // Add 1 more listener. Total 2 listeners. + annotationLayer.addListener(listener); + annotationLayer.notifyListeners(startRange, endRange, side); + assert.equal(listenerCalledTimes, 3); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html index d70a8d2..154d287 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils_test.html
@@ -19,70 +19,77 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> -<script> - const PRELOADED_PROTOCOL = 'preloaded:'; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +const PRELOADED_PROTOCOL = 'preloaded:'; - suite('gr-api-utils tests', async () => { - await readyToTest(); - suite('test getPluginNameFromUrl', () => { - const {getPluginNameFromUrl} = window._apiUtils; +suite('gr-api-utils tests', () => { + suite('test getPluginNameFromUrl', () => { + const {getPluginNameFromUrl} = window._apiUtils; - test('with empty string', () => { - assert.equal(getPluginNameFromUrl(''), null); - }); + test('with empty string', () => { + assert.equal(getPluginNameFromUrl(''), null); + }); - test('with invalid url', () => { - assert.equal(getPluginNameFromUrl('test'), null); - }); + test('with invalid url', () => { + assert.equal(getPluginNameFromUrl('test'), null); + }); - test('with random invalid url', () => { - assert.equal(getPluginNameFromUrl('http://example.com'), null); - assert.equal( - getPluginNameFromUrl('http://example.com/static/a.html'), - null - ); - }); + test('with random invalid url', () => { + assert.equal(getPluginNameFromUrl('http://example.com'), null); + assert.equal( + getPluginNameFromUrl('http://example.com/static/a.html'), + null + ); + }); - test('with valid urls', () => { - assert.equal( - getPluginNameFromUrl('http://example.com/plugins/a.html'), - 'a' - ); - assert.equal( - getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'), - 'a' - ); - }); + test('with valid urls', () => { + assert.equal( + getPluginNameFromUrl('http://example.com/plugins/a.html'), + 'a' + ); + assert.equal( + getPluginNameFromUrl('http://example.com/plugins/a/static/t.html'), + 'a' + ); + }); - test('with preloaded urls', () => { - assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a'); - }); + test('with preloaded urls', () => { + assert.equal(getPluginNameFromUrl(`${PRELOADED_PROTOCOL}a`), 'a'); + }); - test('with gerrit-theme override', () => { - assert.equal( - getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'), - 'gerrit-theme' - ); - }); + test('with gerrit-theme override', () => { + assert.equal( + getPluginNameFromUrl('http://example.com/static/gerrit-theme.html'), + 'gerrit-theme' + ); + }); - test('with ASSETS_PATH', () => { - window.ASSETS_PATH = 'http://cdn.com/2'; - assert.equal( - getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`), - 'a' - ); - window.ASSETS_PATH = undefined; - }); + test('with ASSETS_PATH', () => { + window.ASSETS_PATH = 'http://cdn.com/2'; + assert.equal( + getPluginNameFromUrl(`${window.ASSETS_PATH}/plugins/a.html`), + 'a' + ); + window.ASSETS_PATH = undefined; }); }); +}); </script> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html index 91e1a49..1425f71 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-actions-js-api_test.html
@@ -19,19 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-actions-js-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> <!-- This must refer to the element this interface is wrapping around. Otherwise breaking changes to gr-change-actions won’t be noticed. --> -<link rel="import" href="../../change/gr-change-actions/gr-change-actions.html"> +<script type="module" src="../../change/gr-change-actions/gr-change-actions.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../change/gr-change-actions/gr-change-actions.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -39,191 +44,194 @@ </template> </test-fixture> -<script> - suite('gr-js-api-interface tests', async () => { - await readyToTest(); - let element; - let changeActions; - let plugin; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../change/gr-change-actions/gr-change-actions.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-js-api-interface tests', () => { + let element; + let changeActions; + let plugin; - // Because deepEqual doesn’t behave in Safari. - function assertArraysEqual(actual, expected) { - assert.equal(actual.length, expected.length); - for (let i = 0; i < actual.length; i++) { - assert.equal(actual[i], expected[i]); - } + // Because deepEqual doesn’t behave in Safari. + function assertArraysEqual(actual, expected) { + assert.equal(actual.length, expected.length); + for (let i = 0; i < actual.length; i++) { + assert.equal(actual[i], expected[i]); } + } - suite('early init', () => { - setup(() => { - Gerrit._testOnly_resetPlugins(); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - // Mimic all plugins loaded. - Gerrit._loadPlugins([]); - changeActions = plugin.changeActions(); - element = fixture('basic'); + suite('early init', () => { + setup(() => { + Gerrit._testOnly_resetPlugins(); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + // Mimic all plugins loaded. + Gerrit._loadPlugins([]); + changeActions = plugin.changeActions(); + element = fixture('basic'); + }); + + teardown(() => { + changeActions = null; + Gerrit._testOnly_resetPlugins(); + }); + + test('does not throw', ()=> { + assert.doesNotThrow(() => { + changeActions.add('change', 'foo'); }); + }); + }); - teardown(() => { - changeActions = null; - Gerrit._testOnly_resetPlugins(); - }); + suite('normal init', () => { + setup(() => { + Gerrit._testOnly_resetPlugins(); + element = fixture('basic'); + sinon.stub(element, '_editStatusChanged'); + element.change = {}; + element._hasKnownChainState = false; + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + changeActions = plugin.changeActions(); + // Mimic all plugins loaded. + Gerrit._loadPlugins([]); + }); - test('does not throw', ()=> { - assert.doesNotThrow(() => { - changeActions.add('change', 'foo'); + teardown(() => { + changeActions = null; + Gerrit._testOnly_resetPlugins(); + }); + + test('property existence', () => { + const properties = [ + 'ActionType', + 'ChangeActions', + 'RevisionActions', + ]; + for (const p of properties) { + assertArraysEqual(changeActions[p], element[p]); + } + }); + + test('add/remove primary action keys', () => { + element.primaryActionKeys = []; + changeActions.addPrimaryActionKey('foo'); + assertArraysEqual(element.primaryActionKeys, ['foo']); + changeActions.addPrimaryActionKey('foo'); + assertArraysEqual(element.primaryActionKeys, ['foo']); + changeActions.addPrimaryActionKey('bar'); + assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']); + changeActions.removePrimaryActionKey('foo'); + assertArraysEqual(element.primaryActionKeys, ['bar']); + changeActions.removePrimaryActionKey('baz'); + assertArraysEqual(element.primaryActionKeys, ['bar']); + changeActions.removePrimaryActionKey('bar'); + assertArraysEqual(element.primaryActionKeys, []); + }); + + test('action buttons', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + const handler = sinon.spy(); + changeActions.addTapListener(key, handler); + flush(() => { + MockInteractions.tap(element.shadowRoot + .querySelector('[data-action-key="' + key + '"]')); + assert(handler.calledOnce); + changeActions.removeTapListener(key, handler); + MockInteractions.tap(element.shadowRoot + .querySelector('[data-action-key="' + key + '"]')); + assert(handler.calledOnce); + changeActions.remove(key); + flush(() => { + assert.isNull(element.shadowRoot + .querySelector('[data-action-key="' + key + '"]')); + done(); }); }); }); - suite('normal init', () => { - setup(() => { - Gerrit._testOnly_resetPlugins(); - element = fixture('basic'); - sinon.stub(element, '_editStatusChanged'); - element.change = {}; - element._hasKnownChainState = false; - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - changeActions = plugin.changeActions(); - // Mimic all plugins loaded. - Gerrit._loadPlugins([]); - }); - - teardown(() => { - changeActions = null; - Gerrit._testOnly_resetPlugins(); - }); - - test('property existence', () => { - const properties = [ - 'ActionType', - 'ChangeActions', - 'RevisionActions', - ]; - for (const p of properties) { - assertArraysEqual(changeActions[p], element[p]); - } - }); - - test('add/remove primary action keys', () => { - element.primaryActionKeys = []; - changeActions.addPrimaryActionKey('foo'); - assertArraysEqual(element.primaryActionKeys, ['foo']); - changeActions.addPrimaryActionKey('foo'); - assertArraysEqual(element.primaryActionKeys, ['foo']); - changeActions.addPrimaryActionKey('bar'); - assertArraysEqual(element.primaryActionKeys, ['foo', 'bar']); - changeActions.removePrimaryActionKey('foo'); - assertArraysEqual(element.primaryActionKeys, ['bar']); - changeActions.removePrimaryActionKey('baz'); - assertArraysEqual(element.primaryActionKeys, ['bar']); - changeActions.removePrimaryActionKey('bar'); - assertArraysEqual(element.primaryActionKeys, []); - }); - - test('action buttons', done => { - const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); - const handler = sinon.spy(); - changeActions.addTapListener(key, handler); + test('action button properties', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(() => { + const button = element.shadowRoot + .querySelector('[data-action-key="' + key + '"]'); + assert.isOk(button); + assert.equal(button.getAttribute('data-label'), 'Bork!'); + assert.isNotOk(button.disabled); + changeActions.setLabel(key, 'Yo'); + changeActions.setTitle(key, 'Yo hint'); + changeActions.setEnabled(key, false); + changeActions.setIcon(key, 'pupper'); flush(() => { - MockInteractions.tap(element.shadowRoot - .querySelector('[data-action-key="' + key + '"]')); - assert(handler.calledOnce); - changeActions.removeTapListener(key, handler); - MockInteractions.tap(element.shadowRoot - .querySelector('[data-action-key="' + key + '"]')); - assert(handler.calledOnce); - changeActions.remove(key); - flush(() => { - assert.isNull(element.shadowRoot - .querySelector('[data-action-key="' + key + '"]')); - done(); - }); + assert.equal(button.getAttribute('data-label'), 'Yo'); + assert.equal(button.getAttribute('title'), 'Yo hint'); + assert.isTrue(button.disabled); + assert.equal(dom(button).querySelector('iron-icon').icon, + 'gr-icons:pupper'); + done(); }); }); + }); - test('action button properties', done => { - const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + test('hide action buttons', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(() => { + const button = element.shadowRoot + .querySelector('[data-action-key="' + key + '"]'); + assert.isOk(button); + assert.isFalse(button.hasAttribute('hidden')); + changeActions.setActionHidden( + changeActions.ActionType.REVISION, key, true); flush(() => { const button = element.shadowRoot .querySelector('[data-action-key="' + key + '"]'); - assert.isOk(button); - assert.equal(button.getAttribute('data-label'), 'Bork!'); - assert.isNotOk(button.disabled); - changeActions.setLabel(key, 'Yo'); - changeActions.setTitle(key, 'Yo hint'); - changeActions.setEnabled(key, false); - changeActions.setIcon(key, 'pupper'); - flush(() => { - assert.equal(button.getAttribute('data-label'), 'Yo'); - assert.equal(button.getAttribute('title'), 'Yo hint'); - assert.isTrue(button.disabled); - assert.equal(Polymer.dom(button).querySelector('iron-icon').icon, - 'gr-icons:pupper'); - done(); - }); + assert.isNotOk(button); + done(); }); }); + }); - test('hide action buttons', done => { - const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + test('move action button to overflow', done => { + const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + flush(() => { + assert.isTrue(element.$.moreActions.hidden); + assert.isOk(element.shadowRoot + .querySelector('[data-action-key="' + key + '"]')); + changeActions.setActionOverflow( + changeActions.ActionType.REVISION, key, true); flush(() => { - const button = element.shadowRoot - .querySelector('[data-action-key="' + key + '"]'); - assert.isOk(button); - assert.isFalse(button.hasAttribute('hidden')); - changeActions.setActionHidden( - changeActions.ActionType.REVISION, key, true); - flush(() => { - const button = element.shadowRoot - .querySelector('[data-action-key="' + key + '"]'); - assert.isNotOk(button); - done(); - }); - }); - }); - - test('move action button to overflow', done => { - const key = changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); - flush(() => { - assert.isTrue(element.$.moreActions.hidden); - assert.isOk(element.shadowRoot + assert.isNotOk(element.shadowRoot .querySelector('[data-action-key="' + key + '"]')); - changeActions.setActionOverflow( - changeActions.ActionType.REVISION, key, true); - flush(() => { - assert.isNotOk(element.shadowRoot - .querySelector('[data-action-key="' + key + '"]')); - assert.isFalse(element.$.moreActions.hidden); - assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!'); - done(); - }); + assert.isFalse(element.$.moreActions.hidden); + assert.strictEqual(element.$.moreActions.items[0].name, 'Bork!'); + done(); }); }); + }); - test('change actions priority', done => { - const key1 = - changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); - const key2 = - changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?'); + test('change actions priority', done => { + const key1 = + changeActions.add(changeActions.ActionType.REVISION, 'Bork!'); + const key2 = + changeActions.add(changeActions.ActionType.CHANGE, 'Squanch?'); + flush(() => { + let buttons = + dom(element.root).querySelectorAll('[data-action-key]'); + assert.equal(buttons[0].getAttribute('data-action-key'), key1); + assert.equal(buttons[1].getAttribute('data-action-key'), key2); + changeActions.setActionPriority( + changeActions.ActionType.REVISION, key1, 10); flush(() => { - let buttons = - Polymer.dom(element.root).querySelectorAll('[data-action-key]'); - assert.equal(buttons[0].getAttribute('data-action-key'), key1); - assert.equal(buttons[1].getAttribute('data-action-key'), key2); - changeActions.setActionPriority( - changeActions.ActionType.REVISION, key1, 10); - flush(() => { - buttons = - Polymer.dom(element.root).querySelectorAll('[data-action-key]'); - assert.equal(buttons[0].getAttribute('data-action-key'), key2); - assert.equal(buttons[1].getAttribute('data-action-key'), key1); - done(); - }); + buttons = + dom(element.root).querySelectorAll('[data-action-key]'); + assert.equal(buttons[0].getAttribute('data-action-key'), key2); + assert.equal(buttons[1].getAttribute('data-action-key'), key1); + done(); }); }); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html index 3147746..3a1c51c 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -19,19 +19,24 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-change-reply-js-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> <!-- This must refer to the element this interface is wrapping around. Otherwise breaking changes to gr-reply-dialog won’t be noticed. --> -<link rel="import" href="../../change/gr-reply-dialog/gr-reply-dialog.html"> +<script type="module" src="../../change/gr-reply-dialog/gr-reply-dialog.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../change/gr-reply-dialog/gr-reply-dialog.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -39,86 +44,88 @@ </template> </test-fixture> -<script> - suite('gr-change-reply-js-api tests', async () => { - await readyToTest(); - let element; - let sandbox; - let changeReply; - let plugin; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../change/gr-reply-dialog/gr-reply-dialog.js'; +suite('gr-change-reply-js-api tests', () => { + let element; + let sandbox; + let changeReply; + let plugin; + setup(() => { + sandbox = sinon.sandbox.create(); + stub('gr-rest-api-interface', { + getConfig() { return Promise.resolve({}); }, + getAccount() { return Promise.resolve(null); }, + }); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('early init', () => { setup(() => { - sandbox = sinon.sandbox.create(); - stub('gr-rest-api-interface', { - getConfig() { return Promise.resolve({}); }, - getAccount() { return Promise.resolve(null); }, - }); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + changeReply = plugin.changeReply(); + element = fixture('basic'); }); teardown(() => { - sandbox.restore(); + changeReply = null; }); - suite('early init', () => { - setup(() => { - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - changeReply = plugin.changeReply(); - element = fixture('basic'); - }); + test('works', () => { + sandbox.stub(element, 'getLabelValue').returns('+123'); + assert.equal(changeReply.getLabelValue('My-Label'), '+123'); - teardown(() => { - changeReply = null; - }); + sandbox.stub(element, 'setLabelValue'); + changeReply.setLabelValue('My-Label', '+1337'); + assert.isTrue( + element.setLabelValue.calledWithExactly('My-Label', '+1337')); - test('works', () => { - sandbox.stub(element, 'getLabelValue').returns('+123'); - assert.equal(changeReply.getLabelValue('My-Label'), '+123'); + sandbox.stub(element, 'send'); + changeReply.send(false); + assert.isTrue(element.send.calledWithExactly(false)); - sandbox.stub(element, 'setLabelValue'); - changeReply.setLabelValue('My-Label', '+1337'); - assert.isTrue( - element.setLabelValue.calledWithExactly('My-Label', '+1337')); - - sandbox.stub(element, 'send'); - changeReply.send(false); - assert.isTrue(element.send.calledWithExactly(false)); - - sandbox.stub(element, 'setPluginMessage'); - changeReply.showMessage('foobar'); - assert.isTrue(element.setPluginMessage.calledWithExactly('foobar')); - }); - }); - - suite('normal init', () => { - setup(() => { - element = fixture('basic'); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - changeReply = plugin.changeReply(); - }); - - teardown(() => { - changeReply = null; - }); - - test('works', () => { - sandbox.stub(element, 'getLabelValue').returns('+123'); - assert.equal(changeReply.getLabelValue('My-Label'), '+123'); - - sandbox.stub(element, 'setLabelValue'); - changeReply.setLabelValue('My-Label', '+1337'); - assert.isTrue( - element.setLabelValue.calledWithExactly('My-Label', '+1337')); - - sandbox.stub(element, 'send'); - changeReply.send(false); - assert.isTrue(element.send.calledWithExactly(false)); - - sandbox.stub(element, 'setPluginMessage'); - changeReply.showMessage('foobar'); - assert.isTrue(element.setPluginMessage.calledWithExactly('foobar')); - }); + sandbox.stub(element, 'setPluginMessage'); + changeReply.showMessage('foobar'); + assert.isTrue(element.setPluginMessage.calledWithExactly('foobar')); }); }); + + suite('normal init', () => { + setup(() => { + element = fixture('basic'); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + changeReply = plugin.changeReply(); + }); + + teardown(() => { + changeReply = null; + }); + + test('works', () => { + sandbox.stub(element, 'getLabelValue').returns('+123'); + assert.equal(changeReply.getLabelValue('My-Label'), '+123'); + + sandbox.stub(element, 'setLabelValue'); + changeReply.setLabelValue('My-Label', '+1337'); + assert.isTrue( + element.setLabelValue.calledWithExactly('My-Label', '+1337')); + + sandbox.stub(element, 'send'); + changeReply.send(false); + assert.isTrue(element.send.calledWithExactly(false)); + + sandbox.stub(element, 'setPluginMessage'); + changeReply.showMessage('foobar'); + assert.isTrue(element.setPluginMessage.calledWithExactly('foobar')); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html index ee95a5e..57f0646 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,68 +40,70 @@ </template> </test-fixture> -<script> - suite('gr-gerrit tests', async () => { - await readyToTest(); - let element; - let sandbox; - let sendStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +suite('gr-gerrit tests', () => { + let element; + let sandbox; + let sendStub; - setup(() => { - window.clock = sinon.useFakeTimers(); - sandbox = sinon.sandbox.create(); - sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); - stub('gr-rest-api-interface', { - getAccount() { - return Promise.resolve({name: 'Judy Hopps'}); - }, - send(...args) { - return sendStub(...args); - }, - }); - element = fixture('basic'); + setup(() => { + window.clock = sinon.useFakeTimers(); + sandbox = sinon.sandbox.create(); + sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); + stub('gr-rest-api-interface', { + getAccount() { + return Promise.resolve({name: 'Judy Hopps'}); + }, + send(...args) { + return sendStub(...args); + }, + }); + element = fixture('basic'); + }); + + teardown(() => { + window.clock.restore(); + sandbox.restore(); + element._removeEventCallbacks(); + Gerrit._testOnly_resetPlugins(); + }); + + suite('proxy methods', () => { + test('Gerrit._isPluginEnabled proxy to pluginLoader', () => { + const stubFn = sandbox.stub(); + sandbox.stub( + Gerrit._pluginLoader, + 'isPluginEnabled', + (...args) => stubFn(...args) + ); + Gerrit._isPluginEnabled('test_plugin'); + assert.isTrue(stubFn.calledWith('test_plugin')); }); - teardown(() => { - window.clock.restore(); - sandbox.restore(); - element._removeEventCallbacks(); - Gerrit._testOnly_resetPlugins(); + test('Gerrit._isPluginLoaded proxy to pluginLoader', () => { + const stubFn = sandbox.stub(); + sandbox.stub( + Gerrit._pluginLoader, + 'isPluginLoaded', + (...args) => stubFn(...args) + ); + Gerrit._isPluginLoaded('test_plugin'); + assert.isTrue(stubFn.calledWith('test_plugin')); }); - suite('proxy methods', () => { - test('Gerrit._isPluginEnabled proxy to pluginLoader', () => { - const stubFn = sandbox.stub(); - sandbox.stub( - Gerrit._pluginLoader, - 'isPluginEnabled', - (...args) => stubFn(...args) - ); - Gerrit._isPluginEnabled('test_plugin'); - assert.isTrue(stubFn.calledWith('test_plugin')); - }); - - test('Gerrit._isPluginLoaded proxy to pluginLoader', () => { - const stubFn = sandbox.stub(); - sandbox.stub( - Gerrit._pluginLoader, - 'isPluginLoaded', - (...args) => stubFn(...args) - ); - Gerrit._isPluginLoaded('test_plugin'); - assert.isTrue(stubFn.calledWith('test_plugin')); - }); - - test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => { - const stubFn = sandbox.stub(); - sandbox.stub( - Gerrit._pluginLoader, - 'isPluginPreloaded', - (...args) => stubFn(...args) - ); - Gerrit._isPluginPreloaded('test_plugin'); - assert.isTrue(stubFn.calledWith('test_plugin')); - }); + test('Gerrit._isPluginPreloaded proxy to pluginLoader', () => { + const stubFn = sandbox.stub(); + sandbox.stub( + Gerrit._pluginLoader, + 'isPluginPreloaded', + (...args) => stubFn(...args) + ); + Gerrit._isPluginPreloaded('test_plugin'); + assert.isTrue(stubFn.calledWith('test_plugin')); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.html deleted file mode 100644 index 922fa57..0000000 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.html +++ /dev/null
@@ -1,24 +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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> - -<dom-module id="gr-js-api-interface"> - <script src="gr-js-api-interface-element.js"></script> -</dom-module> \ No newline at end of file
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js index 393dc77..2523d47 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * 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. @@ -14,306 +14,311 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - // Note: for new events, naming convention should be: `a-b` - const EventType = { - HISTORY: 'history', - LABEL_CHANGE: 'labelchange', - SHOW_CHANGE: 'showchange', - SUBMIT_CHANGE: 'submitchange', - SHOW_REVISION_ACTIONS: 'show-revision-actions', - COMMIT_MSG_EDIT: 'commitmsgedit', - COMMENT: 'comment', - REVERT: 'revert', - REVERT_SUBMISSION: 'revert_submission', - POST_REVERT: 'postrevert', - ANNOTATE_DIFF: 'annotatediff', - ADMIN_MENU_LINKS: 'admin-menu-links', - HIGHLIGHTJS_LOADED: 'highlightjs-loaded', - }; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; - const Element = { - CHANGE_ACTIONS: 'changeactions', - REPLY_DIALOG: 'replydialog', - }; +// Note: for new events, naming convention should be: `a-b` +const EventType = { + HISTORY: 'history', + LABEL_CHANGE: 'labelchange', + SHOW_CHANGE: 'showchange', + SUBMIT_CHANGE: 'submitchange', + SHOW_REVISION_ACTIONS: 'show-revision-actions', + COMMIT_MSG_EDIT: 'commitmsgedit', + COMMENT: 'comment', + REVERT: 'revert', + REVERT_SUBMISSION: 'revert_submission', + POST_REVERT: 'postrevert', + ANNOTATE_DIFF: 'annotatediff', + ADMIN_MENU_LINKS: 'admin-menu-links', + HIGHLIGHTJS_LOADED: 'highlightjs-loaded', +}; - /** - * @appliesMixin Gerrit.PatchSetMixin - * @extends Polymer.Element - */ - class GrJsApiInterface extends Polymer.mixinBehaviors( [ - Gerrit.PatchSetBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-js-api-interface'; } +const Element = { + CHANGE_ACTIONS: 'changeactions', + REPLY_DIALOG: 'replydialog', +}; - constructor() { - super(); - this.Element = Element; - this.EventType = EventType; - } +/** + * @appliesMixin Gerrit.PatchSetMixin + * @extends Polymer.Element + */ +class GrJsApiInterface extends mixinBehaviors( [ + Gerrit.PatchSetBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get is() { return 'gr-js-api-interface'; } - static get properties() { - return { - _elements: { - type: Object, - value: {}, // Shared across all instances. - }, - _eventCallbacks: { - type: Object, - value: {}, // Shared across all instances. - }, - }; - } + constructor() { + super(); + this.Element = Element; + this.EventType = EventType; + } - handleEvent(type, detail) { - Gerrit.awaitPluginsLoaded().then(() => { - switch (type) { - case EventType.HISTORY: - this._handleHistory(detail); - break; - case EventType.SHOW_CHANGE: - this._handleShowChange(detail); - break; - case EventType.COMMENT: - this._handleComment(detail); - break; - case EventType.LABEL_CHANGE: - this._handleLabelChange(detail); - break; - case EventType.SHOW_REVISION_ACTIONS: - this._handleShowRevisionActions(detail); - break; - case EventType.HIGHLIGHTJS_LOADED: - this._handleHighlightjsLoaded(detail); - break; - default: - console.warn('handleEvent called with unsupported event type:', - type); - break; - } - }); - } + static get properties() { + return { + _elements: { + type: Object, + value: {}, // Shared across all instances. + }, + _eventCallbacks: { + type: Object, + value: {}, // Shared across all instances. + }, + }; + } - addElement(key, el) { - this._elements[key] = el; - } - - getElement(key) { - return this._elements[key]; - } - - addEventCallback(eventName, callback) { - if (!this._eventCallbacks[eventName]) { - this._eventCallbacks[eventName] = []; - } - this._eventCallbacks[eventName].push(callback); - } - - canSubmitChange(change, revision) { - const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE); - const cancelSubmit = submitCallbacks.some(callback => { - try { - return callback(change, revision) === false; - } catch (err) { - console.error(err); - } - return false; - }); - - return !cancelSubmit; - } - - _removeEventCallbacks() { - for (const k in EventType) { - if (!EventType.hasOwnProperty(k)) { continue; } - this._eventCallbacks[EventType[k]] = []; - } - } - - _handleHistory(detail) { - for (const cb of this._getEventCallbacks(EventType.HISTORY)) { - try { - cb(detail.path); - } catch (err) { - console.error(err); - } - } - } - - _handleShowChange(detail) { - // Note (issue 8221) Shallow clone the change object and add a mergeable - // getter with deprecation warning. This makes the change detail appear as - // though SKIP_MERGEABLE was not set, so that plugins that expect it can - // still access. - // - // This clone and getter can be removed after plugins migrate to use - // info.mergeable. - // - // assign on getter with existing property will report error - // see Issue: 12286 - const change = Object.assign({}, detail.change, { - get mergeable() { - console.warn('Accessing change.mergeable from SHOW_CHANGE is ' + - 'deprecated! Use info.mergeable instead.'); - return detail.info && detail.info.mergeable; - }, - }); - const patchNum = detail.patchNum; - const info = detail.info; - - let revision; - for (const rev of Object.values(change.revisions || {})) { - if (this.patchNumEquals(rev._number, patchNum)) { - revision = rev; + handleEvent(type, detail) { + Gerrit.awaitPluginsLoaded().then(() => { + switch (type) { + case EventType.HISTORY: + this._handleHistory(detail); break; - } + case EventType.SHOW_CHANGE: + this._handleShowChange(detail); + break; + case EventType.COMMENT: + this._handleComment(detail); + break; + case EventType.LABEL_CHANGE: + this._handleLabelChange(detail); + break; + case EventType.SHOW_REVISION_ACTIONS: + this._handleShowRevisionActions(detail); + break; + case EventType.HIGHLIGHTJS_LOADED: + this._handleHighlightjsLoaded(detail); + break; + default: + console.warn('handleEvent called with unsupported event type:', + type); + break; } + }); + } - for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) { - try { - cb(change, revision, info); - } catch (err) { - console.error(err); - } + addElement(key, el) { + this._elements[key] = el; + } + + getElement(key) { + return this._elements[key]; + } + + addEventCallback(eventName, callback) { + if (!this._eventCallbacks[eventName]) { + this._eventCallbacks[eventName] = []; + } + this._eventCallbacks[eventName].push(callback); + } + + canSubmitChange(change, revision) { + const submitCallbacks = this._getEventCallbacks(EventType.SUBMIT_CHANGE); + const cancelSubmit = submitCallbacks.some(callback => { + try { + return callback(change, revision) === false; + } catch (err) { + console.error(err); } - } + return false; + }); - /** - * @param {!{change: !Object, revisionActions: !Object}} detail - */ - _handleShowRevisionActions(detail) { - const registeredCallbacks = this._getEventCallbacks( - EventType.SHOW_REVISION_ACTIONS - ); - for (const cb of registeredCallbacks) { - try { - cb(detail.revisionActions, detail.change); - } catch (err) { - console.error(err); - } - } - } + return !cancelSubmit; + } - handleCommitMessage(change, msg) { - for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) { - try { - cb(change, msg); - } catch (err) { - console.error(err); - } - } - } - - _handleComment(detail) { - for (const cb of this._getEventCallbacks(EventType.COMMENT)) { - try { - cb(detail.node); - } catch (err) { - console.error(err); - } - } - } - - _handleLabelChange(detail) { - for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) { - try { - cb(detail.change); - } catch (err) { - console.error(err); - } - } - } - - _handleHighlightjsLoaded(detail) { - for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) { - try { - cb(detail.hljs); - } catch (err) { - console.error(err); - } - } - } - - modifyRevertMsg(change, revertMsg, origMsg) { - for (const cb of this._getEventCallbacks(EventType.REVERT)) { - try { - revertMsg = cb(change, revertMsg, origMsg); - } catch (err) { - console.error(err); - } - } - return revertMsg; - } - - modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) { - for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) { - try { - revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg); - } catch (err) { - console.error(err); - } - } - return revertSubmissionMsg; - } - - getDiffLayers(path, changeNum, patchNum) { - const layers = []; - for (const annotationApi of - this._getEventCallbacks(EventType.ANNOTATE_DIFF)) { - try { - const layer = annotationApi.getLayer(path, changeNum, patchNum); - layers.push(layer); - } catch (err) { - console.error(err); - } - } - return layers; - } - - /** - * Retrieves coverage data possibly provided by a plugin. - * - * Will wait for plugins to be loaded. If multiple plugins offer a coverage - * provider, the first one is returned. If no plugin offers a coverage provider, - * will resolve to null. - * - * @return {!Promise<?GrAnnotationActionsInterface>} - */ - getCoverageAnnotationApi() { - return Gerrit.awaitPluginsLoaded() - .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF) - .find(api => api.getCoverageProvider())); - } - - getAdminMenuLinks() { - const links = []; - for (const adminApi of - this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) { - links.push(...adminApi.getMenuLinks()); - } - return links; - } - - getLabelValuesPostRevert(change) { - let labels = {}; - for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) { - try { - labels = cb(change); - } catch (err) { - console.error(err); - } - } - return labels; - } - - _getEventCallbacks(type) { - return this._eventCallbacks[type] || []; + _removeEventCallbacks() { + for (const k in EventType) { + if (!EventType.hasOwnProperty(k)) { continue; } + this._eventCallbacks[EventType[k]] = []; } } - customElements.define(GrJsApiInterface.is, GrJsApiInterface); -})(); + _handleHistory(detail) { + for (const cb of this._getEventCallbacks(EventType.HISTORY)) { + try { + cb(detail.path); + } catch (err) { + console.error(err); + } + } + } + + _handleShowChange(detail) { + // Note (issue 8221) Shallow clone the change object and add a mergeable + // getter with deprecation warning. This makes the change detail appear as + // though SKIP_MERGEABLE was not set, so that plugins that expect it can + // still access. + // + // This clone and getter can be removed after plugins migrate to use + // info.mergeable. + // + // assign on getter with existing property will report error + // see Issue: 12286 + const change = Object.assign({}, detail.change, { + get mergeable() { + console.warn('Accessing change.mergeable from SHOW_CHANGE is ' + + 'deprecated! Use info.mergeable instead.'); + return detail.info && detail.info.mergeable; + }, + }); + const patchNum = detail.patchNum; + const info = detail.info; + + let revision; + for (const rev of Object.values(change.revisions || {})) { + if (this.patchNumEquals(rev._number, patchNum)) { + revision = rev; + break; + } + } + + for (const cb of this._getEventCallbacks(EventType.SHOW_CHANGE)) { + try { + cb(change, revision, info); + } catch (err) { + console.error(err); + } + } + } + + /** + * @param {!{change: !Object, revisionActions: !Object}} detail + */ + _handleShowRevisionActions(detail) { + const registeredCallbacks = this._getEventCallbacks( + EventType.SHOW_REVISION_ACTIONS + ); + for (const cb of registeredCallbacks) { + try { + cb(detail.revisionActions, detail.change); + } catch (err) { + console.error(err); + } + } + } + + handleCommitMessage(change, msg) { + for (const cb of this._getEventCallbacks(EventType.COMMIT_MSG_EDIT)) { + try { + cb(change, msg); + } catch (err) { + console.error(err); + } + } + } + + _handleComment(detail) { + for (const cb of this._getEventCallbacks(EventType.COMMENT)) { + try { + cb(detail.node); + } catch (err) { + console.error(err); + } + } + } + + _handleLabelChange(detail) { + for (const cb of this._getEventCallbacks(EventType.LABEL_CHANGE)) { + try { + cb(detail.change); + } catch (err) { + console.error(err); + } + } + } + + _handleHighlightjsLoaded(detail) { + for (const cb of this._getEventCallbacks(EventType.HIGHLIGHTJS_LOADED)) { + try { + cb(detail.hljs); + } catch (err) { + console.error(err); + } + } + } + + modifyRevertMsg(change, revertMsg, origMsg) { + for (const cb of this._getEventCallbacks(EventType.REVERT)) { + try { + revertMsg = cb(change, revertMsg, origMsg); + } catch (err) { + console.error(err); + } + } + return revertMsg; + } + + modifyRevertSubmissionMsg(change, revertSubmissionMsg, origMsg) { + for (const cb of this._getEventCallbacks(EventType.REVERT_SUBMISSION)) { + try { + revertSubmissionMsg = cb(change, revertSubmissionMsg, origMsg); + } catch (err) { + console.error(err); + } + } + return revertSubmissionMsg; + } + + getDiffLayers(path, changeNum, patchNum) { + const layers = []; + for (const annotationApi of + this._getEventCallbacks(EventType.ANNOTATE_DIFF)) { + try { + const layer = annotationApi.getLayer(path, changeNum, patchNum); + layers.push(layer); + } catch (err) { + console.error(err); + } + } + return layers; + } + + /** + * Retrieves coverage data possibly provided by a plugin. + * + * Will wait for plugins to be loaded. If multiple plugins offer a coverage + * provider, the first one is returned. If no plugin offers a coverage provider, + * will resolve to null. + * + * @return {!Promise<?GrAnnotationActionsInterface>} + */ + getCoverageAnnotationApi() { + return Gerrit.awaitPluginsLoaded() + .then(() => this._getEventCallbacks(EventType.ANNOTATE_DIFF) + .find(api => api.getCoverageProvider())); + } + + getAdminMenuLinks() { + const links = []; + for (const adminApi of + this._getEventCallbacks(EventType.ADMIN_MENU_LINKS)) { + links.push(...adminApi.getMenuLinks()); + } + return links; + } + + getLabelValuesPostRevert(change) { + let labels = {}; + for (const cb of this._getEventCallbacks(EventType.POST_REVERT)) { + try { + labels = cb(change); + } catch (err) { + console.error(err); + } + } + return labels; + } + + _getEventCallbacks(type) { + return this._eventCallbacks[type] || []; + } +} + +customElements.define(GrJsApiInterface.is, GrJsApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html deleted file mode 100644 index a4909ec..0000000 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html +++ /dev/null
@@ -1,51 +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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> -<link rel="import" href="../../plugins/gr-admin-api/gr-admin-api.html"> -<link rel="import" href="../../plugins/gr-attribute-helper/gr-attribute-helper.html"> -<link rel="import" href="../../plugins/gr-change-metadata-api/gr-change-metadata-api.html"> -<link rel="import" href="../../plugins/gr-dom-hooks/gr-dom-hooks.html"> -<link rel="import" href="../../plugins/gr-event-helper/gr-event-helper.html"> -<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html"> -<link rel="import" href="../../plugins/gr-repo-api/gr-repo-api.html"> -<link rel="import" href="../../plugins/gr-settings-api/gr-settings-api.html"> -<link rel="import" href="../../plugins/gr-styles-api/gr-styles-api.html"> -<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> -<!-- - Note: the order matters as files depend on each other. - 1. gr-api-utils will be used in multiple files below. - 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and - also gr-plugin-endpoints - 3. gr-public-js-api depends on gr-plugin-rest-api ---> -<script src="gr-api-utils.js"></script> -<script src="../gr-event-interface/gr-event-interface.js"></script> -<script src="gr-annotation-actions-context.js"></script> -<script src="gr-annotation-actions-js-api.js"></script> -<script src="gr-change-actions-js-api.js"></script> -<script src="gr-change-reply-js-api.js"></script> -<link rel="import" href="./gr-js-api-interface-element.html"> -<script src="gr-plugin-endpoints.js"></script> -<script src="gr-plugin-action-context.js"></script> -<script src="gr-plugin-rest-api.js"></script> -<script src="gr-public-js-api.js"></script> -<script src="gr-plugin-loader.js"></script> -<script src="gr-gerrit.js"></script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js new file mode 100644 index 0000000..6b7b13e --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.js
@@ -0,0 +1,58 @@ +/** + * @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 '../../../scripts/bundled-polymer.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import '../../plugins/gr-admin-api/gr-admin-api.js'; +import '../../plugins/gr-attribute-helper/gr-attribute-helper.js'; +import '../../plugins/gr-change-metadata-api/gr-change-metadata-api.js'; +import '../../plugins/gr-dom-hooks/gr-dom-hooks.js'; +import '../../plugins/gr-event-helper/gr-event-helper.js'; +import '../../plugins/gr-popup-interface/gr-popup-interface.js'; +import '../../plugins/gr-repo-api/gr-repo-api.js'; +import '../../plugins/gr-settings-api/gr-settings-api.js'; +import '../../plugins/gr-styles-api/gr-styles-api.js'; +import '../../plugins/gr-theme-api/gr-theme-api.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import './gr-api-utils.js'; +import '../gr-event-interface/gr-event-interface.js'; +import './gr-annotation-actions-context.js'; +import './gr-annotation-actions-js-api.js'; +import './gr-change-actions-js-api.js'; +import './gr-change-reply-js-api.js'; +import './gr-js-api-interface-element.js'; +import './gr-plugin-endpoints.js'; +import './gr-plugin-action-context.js'; +import './gr-plugin-rest-api.js'; +import './gr-public-js-api.js'; +import './gr-plugin-loader.js'; +import './gr-gerrit.js'; + +/* + Note: the order matters as files depend on each other. + 1. gr-api-utils will be used in multiple files below. + 2. gr-gerrit depends on gr-plugin-loader, gr-public-js-api and + also gr-plugin-endpoints + 3. gr-public-js-api depends on gr-plugin-rest-api +*/ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html index efc7206..04ad490 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-api-interface</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,539 +40,541 @@ </template> </test-fixture> -<script> - suite('gr-js-api-interface tests', async () => { - await readyToTest(); - const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils; - let element; - let plugin; - let errorStub; - let sandbox; - let getResponseObjectStub; - let sendStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +suite('gr-js-api-interface tests', () => { + const {PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils; + let element; + let plugin; + let errorStub; + let sandbox; + let getResponseObjectStub; + let sendStub; - const throwErrFn = function() { - throw Error('Unfortunately, this handler has stopped'); + const throwErrFn = function() { + throw Error('Unfortunately, this handler has stopped'); + }; + + setup(() => { + window.clock = sinon.useFakeTimers(); + sandbox = sinon.sandbox.create(); + getResponseObjectStub = sandbox.stub().returns(Promise.resolve()); + sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); + stub('gr-rest-api-interface', { + getAccount() { + return Promise.resolve({name: 'Judy Hopps'}); + }, + getResponseObject: getResponseObjectStub, + send(...args) { + return sendStub(...args); + }, + }); + element = fixture('basic'); + errorStub = sandbox.stub(console, 'error'); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + Gerrit._loadPlugins([]); + }); + + teardown(() => { + window.clock.restore(); + sandbox.restore(); + element._removeEventCallbacks(); + plugin = null; + }); + + test('url', () => { + assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/'); + assert.equal(plugin.url('/static/test.js'), + 'http://test.com/plugins/testplugin/static/test.js'); + }); + + test('url for preloaded plugin without ASSETS_PATH', () => { + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'preloaded:testpluginB'); + assert.equal(plugin.url(), + `${window.location.origin}/plugins/testpluginB/`); + assert.equal(plugin.url('/static/test.js'), + `${window.location.origin}/plugins/testpluginB/static/test.js`); + }); + + test('url for preloaded plugin without ASSETS_PATH', () => { + const oldAssetsPath = window.ASSETS_PATH; + window.ASSETS_PATH = 'http://test.com'; + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', + 'preloaded:testpluginC'); + assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`); + assert.equal(plugin.url('/static/test.js'), + `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`); + window.ASSETS_PATH = oldAssetsPath; + }); + + test('_send on failure rejects with response text', () => { + sendStub.returns(Promise.resolve( + {status: 400, text() { return Promise.resolve('text'); }})); + return plugin._send().catch(r => { + assert.equal(r.message, 'text'); + }); + }); + + test('_send on failure without text rejects with code', () => { + sendStub.returns(Promise.resolve( + {status: 400, text() { return Promise.resolve(null); }})); + return plugin._send().catch(r => { + assert.equal(r.message, '400'); + }); + }); + + test('get', () => { + const response = {foo: 'foo'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.get('/url', r => { + assert.isTrue(sendStub.calledWith( + 'GET', 'http://test.com/plugins/testplugin/url')); + assert.strictEqual(r, response); + }); + }); + + test('get using Promise', () => { + const response = {foo: 'foo'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.get('/url', r => 'rubbish').then(r => { + assert.isTrue(sendStub.calledWith( + 'GET', 'http://test.com/plugins/testplugin/url')); + assert.strictEqual(r, response); + }); + }); + + test('post', () => { + const payload = {foo: 'foo'}; + const response = {bar: 'bar'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.post('/url', payload, r => { + assert.isTrue(sendStub.calledWith( + 'POST', 'http://test.com/plugins/testplugin/url', payload)); + assert.strictEqual(r, response); + }); + }); + + test('put', () => { + const payload = {foo: 'foo'}; + const response = {bar: 'bar'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return plugin.put('/url', payload, r => { + assert.isTrue(sendStub.calledWith( + 'PUT', 'http://test.com/plugins/testplugin/url', payload)); + assert.strictEqual(r, response); + }); + }); + + test('delete works', () => { + const response = {status: 204}; + sendStub.returns(Promise.resolve(response)); + return plugin.delete('/url', r => { + assert.isTrue(sendStub.calledWithExactly( + 'DELETE', 'http://test.com/plugins/testplugin/url')); + assert.strictEqual(r, response); + }); + }); + + test('delete fails', () => { + sendStub.returns(Promise.resolve( + {status: 400, text() { return Promise.resolve('text'); }})); + return plugin.delete('/url', r => { + throw new Error('Should not resolve'); + }).catch(err => { + assert.isTrue(sendStub.calledWith( + 'DELETE', 'http://test.com/plugins/testplugin/url')); + assert.equal('text', err.message); + }); + }); + + test('history event', done => { + plugin.on(element.EventType.HISTORY, throwErrFn); + plugin.on(element.EventType.HISTORY, path => { + assert.equal(path, '/path/to/awesomesauce'); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleEvent(element.EventType.HISTORY, + {path: '/path/to/awesomesauce'}); + }); + + test('showchange event', done => { + const testChange = { + _number: 42, + revisions: {def: {_number: 2}, abc: {_number: 1}}, }; + const expectedChange = Object.assign({mergeable: false}, testChange); + plugin.on(element.EventType.SHOW_CHANGE, throwErrFn); + plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => { + assert.deepEqual(change, expectedChange); + assert.deepEqual(revision, testChange.revisions.abc); + assert.deepEqual(info, {mergeable: false}); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleEvent(element.EventType.SHOW_CHANGE, + {change: testChange, patchNum: 1, info: {mergeable: false}}); + }); + + test('show-revision-actions event', done => { + const testChange = { + _number: 42, + revisions: {def: {_number: 2}, abc: {_number: 1}}, + }; + plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn); + plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => { + assert.deepEqual(change, testChange); + assert.deepEqual(actions, {test: {}}); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS, + {change: testChange, revisionActions: {test: {}}}); + }); + + test('handleEvent awaits plugins load', done => { + const testChange = { + _number: 42, + revisions: {def: {_number: 2}, abc: {_number: 1}}, + }; + const spy = sandbox.spy(); + Gerrit._loadPlugins(['plugins/test.html']); + plugin.on(element.EventType.SHOW_CHANGE, spy); + element.handleEvent(element.EventType.SHOW_CHANGE, + {change: testChange, patchNum: 1}); + assert.isFalse(spy.called); + + // Timeout on loading plugins + window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2); + + flush(() => { + assert.isTrue(spy.called); + done(); + }); + }); + + test('comment event', done => { + const testCommentNode = {foo: 'bar'}; + plugin.on(element.EventType.COMMENT, throwErrFn); + plugin.on(element.EventType.COMMENT, commentNode => { + assert.deepEqual(commentNode, testCommentNode); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleEvent(element.EventType.COMMENT, {node: testCommentNode}); + }); + + test('revert event', () => { + function appendToRevertMsg(c, revertMsg, originalMsg) { + return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo'; + } + + assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test'); + assert.equal(errorStub.callCount, 0); + + plugin.on(element.EventType.REVERT, throwErrFn); + plugin.on(element.EventType.REVERT, appendToRevertMsg); + assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), + 'test\n> origTest\ninfo'); + assert.isTrue(errorStub.calledOnce); + + plugin.on(element.EventType.REVERT, appendToRevertMsg); + assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), + 'test\n> origTest\ninfo\n> origTest\ninfo'); + assert.isTrue(errorStub.calledTwice); + }); + + test('postrevert event', () => { + function getLabels(c) { + return {'Code-Review': 1}; + } + + assert.deepEqual(element.getLabelValuesPostRevert(null), {}); + assert.equal(errorStub.callCount, 0); + + plugin.on(element.EventType.POST_REVERT, throwErrFn); + plugin.on(element.EventType.POST_REVERT, getLabels); + assert.deepEqual( + element.getLabelValuesPostRevert(null), {'Code-Review': 1}); + assert.isTrue(errorStub.calledOnce); + }); + + test('commitmsgedit event', done => { + const testMsg = 'Test CL commit message'; + plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn); + plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => { + assert.deepEqual(msg, testMsg); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleCommitMessage(null, testMsg); + }); + + test('labelchange event', done => { + const testChange = {_number: 42}; + plugin.on(element.EventType.LABEL_CHANGE, throwErrFn); + plugin.on(element.EventType.LABEL_CHANGE, change => { + assert.deepEqual(change, testChange); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange}); + }); + + test('submitchange', () => { + plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn); + plugin.on(element.EventType.SUBMIT_CHANGE, () => true); + assert.isTrue(element.canSubmitChange()); + assert.isTrue(errorStub.calledOnce); + plugin.on(element.EventType.SUBMIT_CHANGE, () => false); + plugin.on(element.EventType.SUBMIT_CHANGE, () => true); + assert.isFalse(element.canSubmitChange()); + assert.isTrue(errorStub.calledTwice); + }); + + test('highlightjs-loaded event', done => { + const testHljs = {_number: 42}; + plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn); + plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => { + assert.deepEqual(hljs, testHljs); + assert.isTrue(errorStub.calledOnce); + done(); + }); + element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs}); + }); + + test('getLoggedIn', done => { + // fake fetch for authCheck + sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204})); + plugin.restApi().getLoggedIn() + .then(loggedIn => { + assert.isTrue(loggedIn); + done(); + }); + }); + + test('attributeHelper', () => { + assert.isOk(plugin.attributeHelper()); + }); + + test('deprecated.install', () => { + plugin.deprecated.install(); + assert.strictEqual(plugin.popup, plugin.deprecated.popup); + assert.strictEqual(plugin.onAction, plugin.deprecated.onAction); + assert.notStrictEqual(plugin.install, plugin.deprecated.install); + }); + + test('getAdminMenuLinks', () => { + const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}]; + const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks') + .returns([ + {getMenuLinks: () => [links[0]]}, + {getMenuLinks: () => [links[1]]}, + ]); + const result = element.getAdminMenuLinks(); + assert.deepEqual(result, links); + assert.isTrue(getCallbacksStub.calledOnce); + assert.equal(getCallbacksStub.lastCall.args[0], + element.EventType.ADMIN_MENU_LINKS); + }); + + suite('test plugin with base url', () => { + let baseUrlPlugin; setup(() => { - window.clock = sinon.useFakeTimers(); - sandbox = sinon.sandbox.create(); - getResponseObjectStub = sandbox.stub().returns(Promise.resolve()); - sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); - stub('gr-rest-api-interface', { - getAccount() { - return Promise.resolve({name: 'Judy Hopps'}); - }, - getResponseObject: getResponseObjectStub, - send(...args) { - return sendStub(...args); - }, - }); - element = fixture('basic'); - errorStub = sandbox.stub(console, 'error'); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - Gerrit._loadPlugins([]); - }); + sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r'); - teardown(() => { - window.clock.restore(); - sandbox.restore(); - element._removeEventCallbacks(); - plugin = null; + Gerrit.install(p => { baseUrlPlugin = p; }, '0.1', + 'http://test.com/r/plugins/baseurlplugin/static/test.js'); }); test('url', () => { - assert.equal(plugin.url(), 'http://test.com/plugins/testplugin/'); - assert.equal(plugin.url('/static/test.js'), - 'http://test.com/plugins/testplugin/static/test.js'); + assert.notEqual(baseUrlPlugin.url(), + 'http://test.com/plugins/baseurlplugin/'); + assert.equal(baseUrlPlugin.url(), + 'http://test.com/r/plugins/baseurlplugin/'); + assert.equal(baseUrlPlugin.url('/static/test.js'), + 'http://test.com/r/plugins/baseurlplugin/static/test.js'); + }); + }); + + suite('popup', () => { + test('popup(element) is deprecated', () => { + plugin.popup(document.createElement('div')); + assert.isTrue(console.error.calledOnce); }); - test('url for preloaded plugin without ASSETS_PATH', () => { - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'preloaded:testpluginB'); - assert.equal(plugin.url(), - `${window.location.origin}/plugins/testpluginB/`); - assert.equal(plugin.url('/static/test.js'), - `${window.location.origin}/plugins/testpluginB/static/test.js`); + test('popup(moduleName) creates popup with component', () => { + const openStub = sandbox.stub(); + sandbox.stub(window, 'GrPopupInterface').returns({ + open: openStub, + }); + plugin.popup('some-name'); + assert.isTrue(openStub.calledOnce); + assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name')); }); - test('url for preloaded plugin without ASSETS_PATH', () => { - const oldAssetsPath = window.ASSETS_PATH; - window.ASSETS_PATH = 'http://test.com'; - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', - 'preloaded:testpluginC'); - assert.equal(plugin.url(), `${window.ASSETS_PATH}/plugins/testpluginC/`); - assert.equal(plugin.url('/static/test.js'), - `${window.ASSETS_PATH}/plugins/testpluginC/static/test.js`); - window.ASSETS_PATH = oldAssetsPath; + test('deprecated.popup(element) creates popup with element', () => { + const el = document.createElement('div'); + el.textContent = 'some text here'; + const openStub = sandbox.stub(GrPopupInterface.prototype, 'open'); + openStub.returns(Promise.resolve({ + _getElement() { + return document.createElement('div'); + }})); + plugin.deprecated.popup(el); + assert.isTrue(openStub.calledOnce); }); + }); - test('_send on failure rejects with response text', () => { - sendStub.returns(Promise.resolve( - {status: 400, text() { return Promise.resolve('text'); }})); - return plugin._send().catch(r => { - assert.equal(r.message, 'text'); + suite('onAction', () => { + let change; + let revision; + let actionDetails; + + setup(() => { + change = {}; + revision = {}; + actionDetails = {__key: 'some'}; + sandbox.stub(plugin, 'on').callsArgWith(1, change, revision); + sandbox.stub(plugin, 'changeActions').returns({ + addTapListener: sandbox.stub().callsArg(1), + getActionDetails: () => actionDetails, }); }); - test('_send on failure without text rejects with code', () => { - sendStub.returns(Promise.resolve( - {status: 400, text() { return Promise.resolve(null); }})); - return plugin._send().catch(r => { - assert.equal(r.message, '400'); + test('returns GrPluginActionContext', () => { + const stub = sandbox.stub(); + plugin.deprecated.onAction('change', 'foo', ctx => { + assert.isTrue(ctx instanceof GrPluginActionContext); + assert.strictEqual(ctx.change, change); + assert.strictEqual(ctx.revision, revision); + assert.strictEqual(ctx.action, actionDetails); + assert.strictEqual(ctx.plugin, plugin); + stub(); }); + assert.isTrue(stub.called); }); - test('get', () => { - const response = {foo: 'foo'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return plugin.get('/url', r => { - assert.isTrue(sendStub.calledWith( - 'GET', 'http://test.com/plugins/testplugin/url')); - assert.strictEqual(r, response); - }); + test('other actions', () => { + const stub = sandbox.stub(); + plugin.deprecated.onAction('project', 'foo', stub); + plugin.deprecated.onAction('edit', 'foo', stub); + plugin.deprecated.onAction('branch', 'foo', stub); + assert.isFalse(stub.called); + }); + }); + + suite('screen', () => { + test('screenUrl()', () => { + sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base'); + assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin'); + assert.equal( + plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo'); }); - test('get using Promise', () => { - const response = {foo: 'foo'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return plugin.get('/url', r => 'rubbish').then(r => { - assert.isTrue(sendStub.calledWith( - 'GET', 'http://test.com/plugins/testplugin/url')); - assert.strictEqual(r, response); - }); + test('deprecated works', () => { + const stub = sandbox.stub(); + const hookStub = {onAttached: sandbox.stub()}; + sandbox.stub(plugin, 'hook').returns(hookStub); + plugin.deprecated.screen('foo', stub); + assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo')); + const fakeEl = {style: {display: ''}}; + hookStub.onAttached.callArgWith(0, fakeEl); + assert.isTrue(stub.called); + assert.equal(fakeEl.style.display, 'none'); }); - test('post', () => { - const payload = {foo: 'foo'}; - const response = {bar: 'bar'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return plugin.post('/url', payload, r => { - assert.isTrue(sendStub.calledWith( - 'POST', 'http://test.com/plugins/testplugin/url', payload)); - assert.strictEqual(r, response); - }); + test('works', () => { + sandbox.stub(plugin, 'registerCustomComponent'); + plugin.screen('foo', 'some-module'); + assert.isTrue(plugin.registerCustomComponent.calledWith( + 'testplugin-screen-foo', 'some-module')); + }); + }); + + suite('panel', () => { + let fakeEl; + let emulateAttached; + + setup(()=> { + fakeEl = {change: {}, revision: {}}; + const hookStub = {onAttached: sandbox.stub()}; + sandbox.stub(plugin, 'hook').returns(hookStub); + emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl); }); - test('put', () => { - const payload = {foo: 'foo'}; - const response = {bar: 'bar'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return plugin.put('/url', payload, r => { - assert.isTrue(sendStub.calledWith( - 'PUT', 'http://test.com/plugins/testplugin/url', payload)); - assert.strictEqual(r, response); - }); + test('plugin.panel is deprecated', () => { + plugin.panel('rubbish'); + assert.isTrue(console.error.called); }); - test('delete works', () => { - const response = {status: 204}; - sendStub.returns(Promise.resolve(response)); - return plugin.delete('/url', r => { - assert.isTrue(sendStub.calledWithExactly( - 'DELETE', 'http://test.com/plugins/testplugin/url')); - assert.strictEqual(r, response); - }); - }); - - test('delete fails', () => { - sendStub.returns(Promise.resolve( - {status: 400, text() { return Promise.resolve('text'); }})); - return plugin.delete('/url', r => { - throw new Error('Should not resolve'); - }).catch(err => { - assert.isTrue(sendStub.calledWith( - 'DELETE', 'http://test.com/plugins/testplugin/url')); - assert.equal('text', err.message); - }); - }); - - test('history event', done => { - plugin.on(element.EventType.HISTORY, throwErrFn); - plugin.on(element.EventType.HISTORY, path => { - assert.equal(path, '/path/to/awesomesauce'); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleEvent(element.EventType.HISTORY, - {path: '/path/to/awesomesauce'}); - }); - - test('showchange event', done => { - const testChange = { - _number: 42, - revisions: {def: {_number: 2}, abc: {_number: 1}}, - }; - const expectedChange = Object.assign({mergeable: false}, testChange); - plugin.on(element.EventType.SHOW_CHANGE, throwErrFn); - plugin.on(element.EventType.SHOW_CHANGE, (change, revision, info) => { - assert.deepEqual(change, expectedChange); - assert.deepEqual(revision, testChange.revisions.abc); - assert.deepEqual(info, {mergeable: false}); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleEvent(element.EventType.SHOW_CHANGE, - {change: testChange, patchNum: 1, info: {mergeable: false}}); - }); - - test('show-revision-actions event', done => { - const testChange = { - _number: 42, - revisions: {def: {_number: 2}, abc: {_number: 1}}, - }; - plugin.on(element.EventType.SHOW_REVISION_ACTIONS, throwErrFn); - plugin.on(element.EventType.SHOW_REVISION_ACTIONS, (actions, change) => { - assert.deepEqual(change, testChange); - assert.deepEqual(actions, {test: {}}); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleEvent(element.EventType.SHOW_REVISION_ACTIONS, - {change: testChange, revisionActions: {test: {}}}); - }); - - test('handleEvent awaits plugins load', done => { - const testChange = { - _number: 42, - revisions: {def: {_number: 2}, abc: {_number: 1}}, - }; - const spy = sandbox.spy(); - Gerrit._loadPlugins(['plugins/test.html']); - plugin.on(element.EventType.SHOW_CHANGE, spy); - element.handleEvent(element.EventType.SHOW_CHANGE, - {change: testChange, patchNum: 1}); - assert.isFalse(spy.called); - - // Timeout on loading plugins - window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2); - - flush(() => { - assert.isTrue(spy.called); - done(); - }); - }); - - test('comment event', done => { - const testCommentNode = {foo: 'bar'}; - plugin.on(element.EventType.COMMENT, throwErrFn); - plugin.on(element.EventType.COMMENT, commentNode => { - assert.deepEqual(commentNode, testCommentNode); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleEvent(element.EventType.COMMENT, {node: testCommentNode}); - }); - - test('revert event', () => { - function appendToRevertMsg(c, revertMsg, originalMsg) { - return revertMsg + '\n' + originalMsg.replace(/^/gm, '> ') + '\ninfo'; - } - - assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), 'test'); - assert.equal(errorStub.callCount, 0); - - plugin.on(element.EventType.REVERT, throwErrFn); - plugin.on(element.EventType.REVERT, appendToRevertMsg); - assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), - 'test\n> origTest\ninfo'); - assert.isTrue(errorStub.calledOnce); - - plugin.on(element.EventType.REVERT, appendToRevertMsg); - assert.equal(element.modifyRevertMsg(null, 'test', 'origTest'), - 'test\n> origTest\ninfo\n> origTest\ninfo'); - assert.isTrue(errorStub.calledTwice); - }); - - test('postrevert event', () => { - function getLabels(c) { - return {'Code-Review': 1}; - } - - assert.deepEqual(element.getLabelValuesPostRevert(null), {}); - assert.equal(errorStub.callCount, 0); - - plugin.on(element.EventType.POST_REVERT, throwErrFn); - plugin.on(element.EventType.POST_REVERT, getLabels); - assert.deepEqual( - element.getLabelValuesPostRevert(null), {'Code-Review': 1}); - assert.isTrue(errorStub.calledOnce); - }); - - test('commitmsgedit event', done => { - const testMsg = 'Test CL commit message'; - plugin.on(element.EventType.COMMIT_MSG_EDIT, throwErrFn); - plugin.on(element.EventType.COMMIT_MSG_EDIT, (change, msg) => { - assert.deepEqual(msg, testMsg); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleCommitMessage(null, testMsg); - }); - - test('labelchange event', done => { - const testChange = {_number: 42}; - plugin.on(element.EventType.LABEL_CHANGE, throwErrFn); - plugin.on(element.EventType.LABEL_CHANGE, change => { - assert.deepEqual(change, testChange); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleEvent(element.EventType.LABEL_CHANGE, {change: testChange}); - }); - - test('submitchange', () => { - plugin.on(element.EventType.SUBMIT_CHANGE, throwErrFn); - plugin.on(element.EventType.SUBMIT_CHANGE, () => true); - assert.isTrue(element.canSubmitChange()); - assert.isTrue(errorStub.calledOnce); - plugin.on(element.EventType.SUBMIT_CHANGE, () => false); - plugin.on(element.EventType.SUBMIT_CHANGE, () => true); - assert.isFalse(element.canSubmitChange()); - assert.isTrue(errorStub.calledTwice); - }); - - test('highlightjs-loaded event', done => { - const testHljs = {_number: 42}; - plugin.on(element.EventType.HIGHLIGHTJS_LOADED, throwErrFn); - plugin.on(element.EventType.HIGHLIGHTJS_LOADED, hljs => { - assert.deepEqual(hljs, testHljs); - assert.isTrue(errorStub.calledOnce); - done(); - }); - element.handleEvent(element.EventType.HIGHLIGHTJS_LOADED, {hljs: testHljs}); - }); - - test('getLoggedIn', done => { - // fake fetch for authCheck - sandbox.stub(window, 'fetch', () => Promise.resolve({status: 204})); - plugin.restApi().getLoggedIn() - .then(loggedIn => { - assert.isTrue(loggedIn); - done(); - }); - }); - - test('attributeHelper', () => { - assert.isOk(plugin.attributeHelper()); - }); - - test('deprecated.install', () => { - plugin.deprecated.install(); - assert.strictEqual(plugin.popup, plugin.deprecated.popup); - assert.strictEqual(plugin.onAction, plugin.deprecated.onAction); - assert.notStrictEqual(plugin.install, plugin.deprecated.install); - }); - - test('getAdminMenuLinks', () => { - const links = [{text: 'a', url: 'b'}, {text: 'c', url: 'd'}]; - const getCallbacksStub = sandbox.stub(element, '_getEventCallbacks') - .returns([ - {getMenuLinks: () => [links[0]]}, - {getMenuLinks: () => [links[1]]}, - ]); - const result = element.getAdminMenuLinks(); - assert.deepEqual(result, links); - assert.isTrue(getCallbacksStub.calledOnce); - assert.equal(getCallbacksStub.lastCall.args[0], - element.EventType.ADMIN_MENU_LINKS); - }); - - suite('test plugin with base url', () => { - let baseUrlPlugin; - - setup(() => { - sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/r'); - - Gerrit.install(p => { baseUrlPlugin = p; }, '0.1', - 'http://test.com/r/plugins/baseurlplugin/static/test.js'); - }); - - test('url', () => { - assert.notEqual(baseUrlPlugin.url(), - 'http://test.com/plugins/baseurlplugin/'); - assert.equal(baseUrlPlugin.url(), - 'http://test.com/r/plugins/baseurlplugin/'); - assert.equal(baseUrlPlugin.url('/static/test.js'), - 'http://test.com/r/plugins/baseurlplugin/static/test.js'); - }); - }); - - suite('popup', () => { - test('popup(element) is deprecated', () => { - plugin.popup(document.createElement('div')); - assert.isTrue(console.error.calledOnce); - }); - - test('popup(moduleName) creates popup with component', () => { - const openStub = sandbox.stub(); - sandbox.stub(window, 'GrPopupInterface').returns({ - open: openStub, - }); - plugin.popup('some-name'); - assert.isTrue(openStub.calledOnce); - assert.isTrue(GrPopupInterface.calledWith(plugin, 'some-name')); - }); - - test('deprecated.popup(element) creates popup with element', () => { - const el = document.createElement('div'); - el.textContent = 'some text here'; - const openStub = sandbox.stub(GrPopupInterface.prototype, 'open'); - openStub.returns(Promise.resolve({ - _getElement() { - return document.createElement('div'); - }})); - plugin.deprecated.popup(el); - assert.isTrue(openStub.calledOnce); - }); - }); - - suite('onAction', () => { - let change; - let revision; - let actionDetails; - - setup(() => { - change = {}; - revision = {}; - actionDetails = {__key: 'some'}; - sandbox.stub(plugin, 'on').callsArgWith(1, change, revision); - sandbox.stub(plugin, 'changeActions').returns({ - addTapListener: sandbox.stub().callsArg(1), - getActionDetails: () => actionDetails, - }); - }); - - test('returns GrPluginActionContext', () => { - const stub = sandbox.stub(); - plugin.deprecated.onAction('change', 'foo', ctx => { - assert.isTrue(ctx instanceof GrPluginActionContext); - assert.strictEqual(ctx.change, change); - assert.strictEqual(ctx.revision, revision); - assert.strictEqual(ctx.action, actionDetails); - assert.strictEqual(ctx.plugin, plugin); - stub(); - }); - assert.isTrue(stub.called); - }); - - test('other actions', () => { - const stub = sandbox.stub(); - plugin.deprecated.onAction('project', 'foo', stub); - plugin.deprecated.onAction('edit', 'foo', stub); - plugin.deprecated.onAction('branch', 'foo', stub); - assert.isFalse(stub.called); - }); - }); - - suite('screen', () => { - test('screenUrl()', () => { - sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns('/base'); - assert.equal(plugin.screenUrl(), 'http://test.com/base/x/testplugin'); - assert.equal( - plugin.screenUrl('foo'), 'http://test.com/base/x/testplugin/foo'); - }); - - test('deprecated works', () => { - const stub = sandbox.stub(); - const hookStub = {onAttached: sandbox.stub()}; - sandbox.stub(plugin, 'hook').returns(hookStub); - plugin.deprecated.screen('foo', stub); - assert.isTrue(plugin.hook.calledWith('testplugin-screen-foo')); - const fakeEl = {style: {display: ''}}; - hookStub.onAttached.callArgWith(0, fakeEl); - assert.isTrue(stub.called); - assert.equal(fakeEl.style.display, 'none'); - }); - - test('works', () => { - sandbox.stub(plugin, 'registerCustomComponent'); - plugin.screen('foo', 'some-module'); - assert.isTrue(plugin.registerCustomComponent.calledWith( - 'testplugin-screen-foo', 'some-module')); - }); - }); - - suite('panel', () => { - let fakeEl; - let emulateAttached; - - setup(()=> { - fakeEl = {change: {}, revision: {}}; - const hookStub = {onAttached: sandbox.stub()}; - sandbox.stub(plugin, 'hook').returns(hookStub); - emulateAttached = () => hookStub.onAttached.callArgWith(0, fakeEl); - }); - - test('plugin.panel is deprecated', () => { - plugin.panel('rubbish'); - assert.isTrue(console.error.called); - }); - - [ - ['CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK', 'change-view-integration'], - ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'], - ].forEach(([panelName, endpointName]) => { - test(`deprecated.panel works for ${panelName}`, () => { - const callback = sandbox.stub(); - plugin.deprecated.panel(panelName, callback); - assert.isTrue(plugin.hook.calledWith(endpointName)); - emulateAttached(); - assert.isTrue(callback.called); - const args = callback.args[0][0]; - assert.strictEqual(args.body, fakeEl); - assert.strictEqual(args.p.CHANGE_INFO, fakeEl.change); - assert.strictEqual(args.p.REVISION_INFO, fakeEl.revision); - }); - }); - }); - - suite('settingsScreen', () => { - test('plugin.settingsScreen is deprecated', () => { - plugin.settingsScreen('rubbish'); - assert.isTrue(console.error.called); - }); - - test('plugin.settings() returns GrSettingsApi', () => { - assert.isOk(plugin.settings()); - assert.isTrue(plugin.settings() instanceof GrSettingsApi); - }); - - test('plugin.deprecated.settingsScreen() works', () => { - const hookStub = {onAttached: sandbox.stub()}; - sandbox.stub(plugin, 'hook').returns(hookStub); - const fakeSettings = {}; - fakeSettings.title = sandbox.stub().returns(fakeSettings); - fakeSettings.token = sandbox.stub().returns(fakeSettings); - fakeSettings.module = sandbox.stub().returns(fakeSettings); - fakeSettings.build = sandbox.stub().returns(hookStub); - sandbox.stub(plugin, 'settings').returns(fakeSettings); + [ + ['CHANGE_SCREEN_BELOW_COMMIT_INFO_BLOCK', 'change-view-integration'], + ['CHANGE_SCREEN_BELOW_CHANGE_INFO_BLOCK', 'change-metadata-item'], + ].forEach(([panelName, endpointName]) => { + test(`deprecated.panel works for ${panelName}`, () => { const callback = sandbox.stub(); - - plugin.deprecated.settingsScreen('path', 'menu', callback); - assert.isTrue(fakeSettings.title.calledWith('menu')); - assert.isTrue(fakeSettings.token.calledWith('path')); - assert.isTrue(fakeSettings.module.calledWith('div')); - assert.equal(fakeSettings.build.callCount, 1); - - const fakeBody = {}; - const fakeEl = { - style: { - display: '', - }, - querySelector: sandbox.stub().returns(fakeBody), - }; - // Emulate settings screen attached - hookStub.onAttached.callArgWith(0, fakeEl); + plugin.deprecated.panel(panelName, callback); + assert.isTrue(plugin.hook.calledWith(endpointName)); + emulateAttached(); assert.isTrue(callback.called); const args = callback.args[0][0]; - assert.strictEqual(args.body, fakeBody); - assert.equal(fakeEl.style.display, 'none'); + assert.strictEqual(args.body, fakeEl); + assert.strictEqual(args.p.CHANGE_INFO, fakeEl.change); + assert.strictEqual(args.p.REVISION_INFO, fakeEl.revision); }); }); }); + + suite('settingsScreen', () => { + test('plugin.settingsScreen is deprecated', () => { + plugin.settingsScreen('rubbish'); + assert.isTrue(console.error.called); + }); + + test('plugin.settings() returns GrSettingsApi', () => { + assert.isOk(plugin.settings()); + assert.isTrue(plugin.settings() instanceof GrSettingsApi); + }); + + test('plugin.deprecated.settingsScreen() works', () => { + const hookStub = {onAttached: sandbox.stub()}; + sandbox.stub(plugin, 'hook').returns(hookStub); + const fakeSettings = {}; + fakeSettings.title = sandbox.stub().returns(fakeSettings); + fakeSettings.token = sandbox.stub().returns(fakeSettings); + fakeSettings.module = sandbox.stub().returns(fakeSettings); + fakeSettings.build = sandbox.stub().returns(hookStub); + sandbox.stub(plugin, 'settings').returns(fakeSettings); + const callback = sandbox.stub(); + + plugin.deprecated.settingsScreen('path', 'menu', callback); + assert.isTrue(fakeSettings.title.calledWith('menu')); + assert.isTrue(fakeSettings.token.calledWith('path')); + assert.isTrue(fakeSettings.module.calledWith('div')); + assert.equal(fakeSettings.build.callCount, 1); + + const fakeBody = {}; + const fakeEl = { + style: { + display: '', + }, + querySelector: sandbox.stub().returns(fakeBody), + }; + // Emulate settings screen attached + hookStub.onAttached.callArgWith(0, fakeEl); + assert.isTrue(callback.called); + const args = callback.args[0][0]; + assert.strictEqual(args.body, fakeBody); + assert.equal(fakeEl.style.display, 'none'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html index c8fb9b1..3ba2acc 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-action-context_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-action-context</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,126 +40,129 @@ </template> </test-fixture> -<script> - suite('gr-plugin-action-context tests', async () => { - await readyToTest(); - let instance; - let sandbox; - let plugin; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-plugin-action-context tests', () => { + let instance; + let sandbox; + let plugin; - setup(() => { - sandbox = sinon.sandbox.create(); - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - instance = new GrPluginActionContext(plugin); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + instance = new GrPluginActionContext(plugin); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('popup() and hide()', () => { - const popupApiStub = { - close: sandbox.stub(), - }; - sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub); - const el = {}; - instance.popup(el); - assert.isTrue(instance.plugin.deprecated.popup.calledWith(el)); + test('popup() and hide()', () => { + const popupApiStub = { + close: sandbox.stub(), + }; + sandbox.stub(plugin.deprecated, 'popup').returns(popupApiStub); + const el = {}; + instance.popup(el); + assert.isTrue(instance.plugin.deprecated.popup.calledWith(el)); - instance.hide(); - assert.isTrue(popupApiStub.close.called); - }); + instance.hide(); + assert.isTrue(popupApiStub.close.called); + }); - test('textfield', () => { - assert.equal(instance.textfield().tagName, 'PAPER-INPUT'); - }); + test('textfield', () => { + assert.equal(instance.textfield().tagName, 'PAPER-INPUT'); + }); - test('br', () => { - assert.equal(instance.br().tagName, 'BR'); - }); + test('br', () => { + assert.equal(instance.br().tagName, 'BR'); + }); - test('msg', () => { - const el = instance.msg('foobar'); - assert.equal(el.tagName, 'GR-LABEL'); - assert.equal(el.textContent, 'foobar'); - }); + test('msg', () => { + const el = instance.msg('foobar'); + assert.equal(el.tagName, 'GR-LABEL'); + assert.equal(el.textContent, 'foobar'); + }); - test('div', () => { - const el1 = document.createElement('span'); - el1.textContent = 'foo'; - const el2 = document.createElement('div'); - el2.textContent = 'bar'; - const div = instance.div(el1, el2); - assert.equal(div.tagName, 'DIV'); - assert.equal(div.textContent, 'foobar'); - }); + test('div', () => { + const el1 = document.createElement('span'); + el1.textContent = 'foo'; + const el2 = document.createElement('div'); + el2.textContent = 'bar'; + const div = instance.div(el1, el2); + assert.equal(div.tagName, 'DIV'); + assert.equal(div.textContent, 'foobar'); + }); - test('button', done => { - const clickStub = sandbox.stub(); - const button = instance.button('foo', {onclick: clickStub}); - // If you don't attach a Polymer element to the DOM, then the ready() - // callback will not be called and then e.g. this.$ is undefined. - Polymer.dom(document.body).appendChild(button); - MockInteractions.tap(button); - flush(() => { - assert.isTrue(clickStub.called); - assert.equal(button.textContent, 'foo'); - done(); - }); - }); - - test('checkbox', () => { - const el = instance.checkbox(); - assert.equal(el.tagName, 'INPUT'); - assert.equal(el.type, 'checkbox'); - }); - - test('label', () => { - const fakeMsg = {}; - const fakeCheckbox = {}; - sandbox.stub(instance, 'div'); - sandbox.stub(instance, 'msg').returns(fakeMsg); - instance.label(fakeCheckbox, 'foo'); - assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg)); - }); - - test('call', () => { - instance.action = { - method: 'METHOD', - __key: 'key', - __url: '/changes/1/revisions/2/foo~bar', - }; - const sendStub = sandbox.stub().returns(Promise.resolve()); - sandbox.stub(plugin, 'restApi').returns({ - send: sendStub, - }); - const payload = {foo: 'foo'}; - const successStub = sandbox.stub(); - instance.call(payload, successStub); - assert.isTrue(sendStub.calledWith( - 'METHOD', '/changes/1/revisions/2/foo~bar', payload)); - }); - - test('call error', done => { - instance.action = { - method: 'METHOD', - __key: 'key', - __url: '/changes/1/revisions/2/foo~bar', - }; - const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom'))); - sandbox.stub(plugin, 'restApi').returns({ - send: sendStub, - }); - const errorStub = sandbox.stub(); - document.addEventListener('show-alert', errorStub); - instance.call(); - flush(() => { - assert.isTrue(errorStub.calledOnce); - assert.equal(errorStub.args[0][0].detail.message, - 'Plugin network error: Error: boom'); - done(); - }); + test('button', done => { + const clickStub = sandbox.stub(); + const button = instance.button('foo', {onclick: clickStub}); + // If you don't attach a Polymer element to the DOM, then the ready() + // callback will not be called and then e.g. this.$ is undefined. + dom(document.body).appendChild(button); + MockInteractions.tap(button); + flush(() => { + assert.isTrue(clickStub.called); + assert.equal(button.textContent, 'foo'); + done(); }); }); + + test('checkbox', () => { + const el = instance.checkbox(); + assert.equal(el.tagName, 'INPUT'); + assert.equal(el.type, 'checkbox'); + }); + + test('label', () => { + const fakeMsg = {}; + const fakeCheckbox = {}; + sandbox.stub(instance, 'div'); + sandbox.stub(instance, 'msg').returns(fakeMsg); + instance.label(fakeCheckbox, 'foo'); + assert.isTrue(instance.div.calledWithExactly(fakeCheckbox, fakeMsg)); + }); + + test('call', () => { + instance.action = { + method: 'METHOD', + __key: 'key', + __url: '/changes/1/revisions/2/foo~bar', + }; + const sendStub = sandbox.stub().returns(Promise.resolve()); + sandbox.stub(plugin, 'restApi').returns({ + send: sendStub, + }); + const payload = {foo: 'foo'}; + const successStub = sandbox.stub(); + instance.call(payload, successStub); + assert.isTrue(sendStub.calledWith( + 'METHOD', '/changes/1/revisions/2/foo~bar', payload)); + }); + + test('call error', done => { + instance.action = { + method: 'METHOD', + __key: 'key', + __url: '/changes/1/revisions/2/foo~bar', + }; + const sendStub = sandbox.stub().returns(Promise.reject(new Error('boom'))); + sandbox.stub(plugin, 'restApi').returns({ + send: sendStub, + }); + const errorStub = sandbox.stub(); + document.addEventListener('show-alert', errorStub); + instance.call(); + flush(() => { + assert.isTrue(errorStub.calledOnce); + assert.equal(errorStub.args[0][0].detail.message, + 'Plugin network error: Error: boom'); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html index 5c0d69a..94bb771 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints_test.html
@@ -19,132 +19,139 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-endpoints</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> -<script> - suite('gr-plugin-endpoints tests', async () => { - await readyToTest(); - let sandbox; - let instance; - let pluginFoo; - let pluginBar; - let domHook; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +suite('gr-plugin-endpoints tests', () => { + let sandbox; + let instance; + let pluginFoo; + let pluginBar; + let domHook; - setup(() => { - sandbox = sinon.sandbox.create(); - domHook = {}; - instance = new GrPluginEndpoints(); - Gerrit.install(p => { pluginFoo = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/foo.html'); - instance.registerModule( - pluginFoo, 'a-place', 'decorate', 'foo-module', domHook); - Gerrit.install(p => { pluginBar = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/bar.html'); - instance.registerModule( - pluginBar, 'a-place', 'style', 'bar-module', domHook); - sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + domHook = {}; + instance = new GrPluginEndpoints(); + Gerrit.install(p => { pluginFoo = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/foo.html'); + instance.registerModule( + pluginFoo, 'a-place', 'decorate', 'foo-module', domHook); + Gerrit.install(p => { pluginBar = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/bar.html'); + instance.registerModule( + pluginBar, 'a-place', 'style', 'bar-module', domHook); + sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('getDetails all', () => { - assert.deepEqual(instance.getDetails('a-place'), [ - { - moduleName: 'foo-module', - plugin: pluginFoo, - pluginUrl: pluginFoo._url, - type: 'decorate', - domHook, - }, - { - moduleName: 'bar-module', - plugin: pluginBar, - pluginUrl: pluginBar._url, - type: 'style', - domHook, - }, - ]); - }); - - test('getDetails by type', () => { - assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [ - { - moduleName: 'bar-module', - plugin: pluginBar, - pluginUrl: pluginBar._url, - type: 'style', - domHook, - }, - ]); - }); - - test('getDetails by module', () => { - assert.deepEqual( - instance.getDetails('a-place', {moduleName: 'foo-module'}), - [ - { - moduleName: 'foo-module', - plugin: pluginFoo, - pluginUrl: pluginFoo._url, - type: 'decorate', - domHook, - }, - ]); - }); - - test('getModules', () => { - assert.deepEqual( - instance.getModules('a-place'), ['foo-module', 'bar-module']); - }); - - test('getPlugins', () => { - assert.deepEqual( - instance.getPlugins('a-place'), [pluginFoo._url]); - }); - - test('onNewEndpoint', () => { - const newModuleStub = sandbox.stub(); - instance.onNewEndpoint('a-place', newModuleStub); - instance.registerModule( - pluginFoo, 'a-place', 'replace', 'zaz-module', domHook); - assert.deepEqual(newModuleStub.lastCall.args[0], { - moduleName: 'zaz-module', + test('getDetails all', () => { + assert.deepEqual(instance.getDetails('a-place'), [ + { + moduleName: 'foo-module', plugin: pluginFoo, pluginUrl: pluginFoo._url, - type: 'replace', + type: 'decorate', domHook, - }); - }); + }, + { + moduleName: 'bar-module', + plugin: pluginBar, + pluginUrl: pluginBar._url, + type: 'style', + domHook, + }, + ]); + }); - test('reuse dom hooks', () => { - instance.registerModule( - pluginFoo, 'a-place', 'decorate', 'foo-module', domHook); - assert.deepEqual(instance.getDetails('a-place'), [ - { - moduleName: 'foo-module', - plugin: pluginFoo, - pluginUrl: pluginFoo._url, - type: 'decorate', - domHook, - }, - { - moduleName: 'bar-module', - plugin: pluginBar, - pluginUrl: pluginBar._url, - type: 'style', - domHook, - }, - ]); + test('getDetails by type', () => { + assert.deepEqual(instance.getDetails('a-place', {type: 'style'}), [ + { + moduleName: 'bar-module', + plugin: pluginBar, + pluginUrl: pluginBar._url, + type: 'style', + domHook, + }, + ]); + }); + + test('getDetails by module', () => { + assert.deepEqual( + instance.getDetails('a-place', {moduleName: 'foo-module'}), + [ + { + moduleName: 'foo-module', + plugin: pluginFoo, + pluginUrl: pluginFoo._url, + type: 'decorate', + domHook, + }, + ]); + }); + + test('getModules', () => { + assert.deepEqual( + instance.getModules('a-place'), ['foo-module', 'bar-module']); + }); + + test('getPlugins', () => { + assert.deepEqual( + instance.getPlugins('a-place'), [pluginFoo._url]); + }); + + test('onNewEndpoint', () => { + const newModuleStub = sandbox.stub(); + instance.onNewEndpoint('a-place', newModuleStub); + instance.registerModule( + pluginFoo, 'a-place', 'replace', 'zaz-module', domHook); + assert.deepEqual(newModuleStub.lastCall.args[0], { + moduleName: 'zaz-module', + plugin: pluginFoo, + pluginUrl: pluginFoo._url, + type: 'replace', + domHook, }); }); + + test('reuse dom hooks', () => { + instance.registerModule( + pluginFoo, 'a-place', 'decorate', 'foo-module', domHook); + assert.deepEqual(instance.getDetails('a-place'), [ + { + moduleName: 'foo-module', + plugin: pluginFoo, + pluginUrl: pluginFoo._url, + type: 'decorate', + domHook, + }, + { + moduleName: 'bar-module', + plugin: pluginBar, + pluginUrl: pluginBar._url, + type: 'style', + domHook, + }, + ]); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html index 1a9174f..46914dc 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-host</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,531 +40,533 @@ </template> </test-fixture> -<script> - suite('gr-plugin-loader tests', async () => { - await readyToTest(); - const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils; - let plugin; - let sandbox; - let url; - let sendStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +suite('gr-plugin-loader tests', () => { + const {PRELOADED_PROTOCOL, PLUGIN_LOADING_TIMEOUT_MS} = window._apiUtils; + let plugin; + let sandbox; + let url; + let sendStub; + setup(() => { + window.clock = sinon.useFakeTimers(); + sandbox = sinon.sandbox.create(); + sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); + stub('gr-rest-api-interface', { + getAccount() { + return Promise.resolve({name: 'Judy Hopps'}); + }, + send(...args) { + return sendStub(...args); + }, + }); + sandbox.stub(document.body, 'appendChild'); + fixture('basic'); + url = window.location.origin; + }); + + teardown(() => { + sandbox.restore(); + window.clock.restore(); + Gerrit._testOnly_resetPlugins(); + }); + + test('reuse plugin for install calls', () => { + Gerrit.install(p => { plugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + + let otherPlugin; + Gerrit.install(p => { otherPlugin = p; }, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + assert.strictEqual(plugin, otherPlugin); + }); + + test('flushes preinstalls if provided', () => { + assert.doesNotThrow(() => { + Gerrit._testOnly_flushPreinstalls(); + }); + window.Gerrit.flushPreinstalls = sandbox.stub(); + Gerrit._testOnly_flushPreinstalls(); + assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce); + delete window.Gerrit.flushPreinstalls; + }); + + test('versioning', () => { + const callback = sandbox.spy(); + Gerrit.install(callback, '0.0pre-alpha'); + assert(callback.notCalled); + }); + + test('report pluginsLoaded', done => { + stub('gr-reporting', { + pluginsLoaded() { + done(); + }, + }); + Gerrit._loadPlugins([]); + }); + + test('arePluginsLoaded', done => { + assert.isFalse(Gerrit._arePluginsLoaded()); + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + + Gerrit._loadPlugins(plugins); + assert.isFalse(Gerrit._arePluginsLoaded()); + // Timeout on loading plugins + window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2); + + flush(() => { + assert.isTrue(Gerrit._arePluginsLoaded()); + done(); + }); + }); + + test('plugins installed successfully', done => { + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => void 0, undefined, url); + }); + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + Gerrit._loadPlugins(plugins); + + flush(() => { + assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar'])); + assert.isTrue(Gerrit._arePluginsLoaded()); + done(); + }); + }); + + test('isPluginEnabled and isPluginLoaded', done => { + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => void 0, undefined, url); + }); + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + 'bar/static/test.js', + ]; + Gerrit._loadPlugins(plugins); + assert.isTrue( + plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin)) + ); + + flush(() => { + assert.isTrue(Gerrit._arePluginsLoaded()); + assert.isTrue( + plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin)) + ); + + done(); + }); + }); + + test('plugins installed mixed result, 1 fail 1 succeed', done => { + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + + const alertStub = sandbox.stub(); + document.addEventListener('show-alert', alertStub); + + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => { + if (url === plugins[0]) { + throw new Error('failed'); + } + }, undefined, url); + }); + + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + Gerrit._loadPlugins(plugins); + + flush(() => { + assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar'])); + assert.isTrue(Gerrit._arePluginsLoaded()); + assert.isTrue(alertStub.calledOnce); + done(); + }); + }); + + test('isPluginEnabled and isPluginLoaded for mixed results', done => { + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + + const alertStub = sandbox.stub(); + document.addEventListener('show-alert', alertStub); + + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => { + if (url === plugins[0]) { + throw new Error('failed'); + } + }, undefined, url); + }); + + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + Gerrit._loadPlugins(plugins); + assert.isTrue( + plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin)) + ); + + flush(() => { + assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar'])); + assert.isTrue(Gerrit._arePluginsLoaded()); + assert.isTrue(alertStub.calledOnce); + assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1])); + assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0])); + done(); + }); + }); + + test('plugins installed all failed', done => { + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + + const alertStub = sandbox.stub(); + document.addEventListener('show-alert', alertStub); + + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => { + throw new Error('failed'); + }, undefined, url); + }); + + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + Gerrit._loadPlugins(plugins); + + flush(() => { + assert.isTrue(pluginsLoadedStub.calledWithExactly([])); + assert.isTrue(Gerrit._arePluginsLoaded()); + assert.isTrue(alertStub.calledTwice); + done(); + }); + }); + + test('plugins installed failed becasue of wrong version', done => { + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + + const alertStub = sandbox.stub(); + document.addEventListener('show-alert', alertStub); + + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => { + }, url === plugins[0] ? '' : 'alpha', url); + }); + + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + Gerrit._loadPlugins(plugins); + + flush(() => { + assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo'])); + assert.isTrue(Gerrit._arePluginsLoaded()); + assert.isTrue(alertStub.calledOnce); + done(); + }); + }); + + test('multiple assets for same plugin installed successfully', done => { + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => void 0, undefined, url); + }); + const pluginsLoadedStub = sandbox.stub(); + stub('gr-reporting', { + pluginsLoaded: (...args) => pluginsLoadedStub(...args), + }); + + const plugins = [ + 'http://test.com/plugins/foo/static/test.js', + 'http://test.com/plugins/foo/static/test2.js', + 'http://test.com/plugins/bar/static/test.js', + ]; + Gerrit._loadPlugins(plugins); + + flush(() => { + assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar'])); + assert.isTrue(Gerrit._arePluginsLoaded()); + done(); + }); + }); + + suite('plugin path and url', () => { + let importHtmlPluginStub; + let loadJsPluginStub; setup(() => { - window.clock = sinon.useFakeTimers(); - sandbox = sinon.sandbox.create(); - sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); - stub('gr-rest-api-interface', { - getAccount() { - return Promise.resolve({name: 'Judy Hopps'}); - }, - send(...args) { - return sendStub(...args); - }, + importHtmlPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => { + importHtmlPluginStub(url); }); - sandbox.stub(document.body, 'appendChild'); - fixture('basic'); - url = window.location.origin; + loadJsPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => { + loadJsPluginStub(url); + }); + }); + + test('invalid plugin path', () => { + const failToLoadStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => { + failToLoadStub(...args); + }); + + Gerrit._loadPlugins([ + 'foo/bar', + ]); + + assert.isTrue(failToLoadStub.calledOnce); + assert.isTrue(failToLoadStub.calledWithExactly( + 'Unrecognized plugin path foo/bar', + 'foo/bar' + )); + }); + + test('relative path for plugins', () => { + Gerrit._loadPlugins([ + 'foo/bar.js', + 'foo/bar.html', + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`) + ); + }); + + test('relative path should honor getBaseUrl', () => { + const testUrl = '/test'; + sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl); + + Gerrit._loadPlugins([ + 'foo/bar.js', + 'foo/bar.html', + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly( + `${url}${testUrl}/foo/bar.html` + ) + ); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`) + ); + }); + + test('absolute path for plugins', () => { + Gerrit._loadPlugins([ + 'http://e.com/foo/bar.js', + 'http://e.com/foo/bar.html', + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`) + ); + }); + }); + + suite('With ASSETS_PATH', () => { + let importHtmlPluginStub; + let loadJsPluginStub; + setup(() => { + window.ASSETS_PATH = 'https://cdn.com'; + importHtmlPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => { + importHtmlPluginStub(url); + }); + loadJsPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => { + loadJsPluginStub(url); + }); }); teardown(() => { - sandbox.restore(); - window.clock.restore(); - Gerrit._testOnly_resetPlugins(); + window.ASSETS_PATH = ''; }); - test('reuse plugin for install calls', () => { - Gerrit.install(p => { plugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - - let otherPlugin; - Gerrit.install(p => { otherPlugin = p; }, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - assert.strictEqual(plugin, otherPlugin); - }); - - test('flushes preinstalls if provided', () => { - assert.doesNotThrow(() => { - Gerrit._testOnly_flushPreinstalls(); - }); - window.Gerrit.flushPreinstalls = sandbox.stub(); - Gerrit._testOnly_flushPreinstalls(); - assert.isTrue(window.Gerrit.flushPreinstalls.calledOnce); - delete window.Gerrit.flushPreinstalls; - }); - - test('versioning', () => { - const callback = sandbox.spy(); - Gerrit.install(callback, '0.0pre-alpha'); - assert(callback.notCalled); - }); - - test('report pluginsLoaded', done => { - stub('gr-reporting', { - pluginsLoaded() { - done(); - }, - }); - Gerrit._loadPlugins([]); - }); - - test('arePluginsLoaded', done => { - assert.isFalse(Gerrit._arePluginsLoaded()); - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - - Gerrit._loadPlugins(plugins); - assert.isFalse(Gerrit._arePluginsLoaded()); - // Timeout on loading plugins - window.clock.tick(PLUGIN_LOADING_TIMEOUT_MS * 2); - - flush(() => { - assert.isTrue(Gerrit._arePluginsLoaded()); - done(); - }); - }); - - test('plugins installed successfully', done => { - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => void 0, undefined, url); - }); - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - Gerrit._loadPlugins(plugins); - - flush(() => { - assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar'])); - assert.isTrue(Gerrit._arePluginsLoaded()); - done(); - }); - }); - - test('isPluginEnabled and isPluginLoaded', done => { - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => void 0, undefined, url); - }); - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - 'bar/static/test.js', - ]; - Gerrit._loadPlugins(plugins); - assert.isTrue( - plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin)) - ); - - flush(() => { - assert.isTrue(Gerrit._arePluginsLoaded()); - assert.isTrue( - plugins.every(plugin => Gerrit._pluginLoader.isPluginLoaded(plugin)) - ); - - done(); - }); - }); - - test('plugins installed mixed result, 1 fail 1 succeed', done => { - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - - const alertStub = sandbox.stub(); - document.addEventListener('show-alert', alertStub); - - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => { - if (url === plugins[0]) { - throw new Error('failed'); - } - }, undefined, url); - }); - - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - Gerrit._loadPlugins(plugins); - - flush(() => { - assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar'])); - assert.isTrue(Gerrit._arePluginsLoaded()); - assert.isTrue(alertStub.calledOnce); - done(); - }); - }); - - test('isPluginEnabled and isPluginLoaded for mixed results', done => { - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - - const alertStub = sandbox.stub(); - document.addEventListener('show-alert', alertStub); - - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => { - if (url === plugins[0]) { - throw new Error('failed'); - } - }, undefined, url); - }); - - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - Gerrit._loadPlugins(plugins); - assert.isTrue( - plugins.every(plugin => Gerrit._pluginLoader.isPluginEnabled(plugin)) - ); - - flush(() => { - assert.isTrue(pluginsLoadedStub.calledWithExactly(['bar'])); - assert.isTrue(Gerrit._arePluginsLoaded()); - assert.isTrue(alertStub.calledOnce); - assert.isTrue(Gerrit._pluginLoader.isPluginLoaded(plugins[1])); - assert.isFalse(Gerrit._pluginLoader.isPluginLoaded(plugins[0])); - done(); - }); - }); - - test('plugins installed all failed', done => { - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - - const alertStub = sandbox.stub(); - document.addEventListener('show-alert', alertStub); - - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => { - throw new Error('failed'); - }, undefined, url); - }); - - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - Gerrit._loadPlugins(plugins); - - flush(() => { - assert.isTrue(pluginsLoadedStub.calledWithExactly([])); - assert.isTrue(Gerrit._arePluginsLoaded()); - assert.isTrue(alertStub.calledTwice); - done(); - }); - }); - - test('plugins installed failed becasue of wrong version', done => { - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - - const alertStub = sandbox.stub(); - document.addEventListener('show-alert', alertStub); - - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => { - }, url === plugins[0] ? '' : 'alpha', url); - }); - - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - Gerrit._loadPlugins(plugins); - - flush(() => { - assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo'])); - assert.isTrue(Gerrit._arePluginsLoaded()); - assert.isTrue(alertStub.calledOnce); - done(); - }); - }); - - test('multiple assets for same plugin installed successfully', done => { - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => void 0, undefined, url); - }); - const pluginsLoadedStub = sandbox.stub(); - stub('gr-reporting', { - pluginsLoaded: (...args) => pluginsLoadedStub(...args), - }); - - const plugins = [ - 'http://test.com/plugins/foo/static/test.js', - 'http://test.com/plugins/foo/static/test2.js', - 'http://test.com/plugins/bar/static/test.js', - ]; - Gerrit._loadPlugins(plugins); - - flush(() => { - assert.isTrue(pluginsLoadedStub.calledWithExactly(['foo', 'bar'])); - assert.isTrue(Gerrit._arePluginsLoaded()); - done(); - }); - }); - - suite('plugin path and url', () => { - let importHtmlPluginStub; - let loadJsPluginStub; - setup(() => { - importHtmlPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => { - importHtmlPluginStub(url); - }); - loadJsPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => { - loadJsPluginStub(url); - }); - }); - - test('invalid plugin path', () => { - const failToLoadStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_failToLoad', (...args) => { - failToLoadStub(...args); - }); - - Gerrit._loadPlugins([ - 'foo/bar', - ]); - - assert.isTrue(failToLoadStub.calledOnce); - assert.isTrue(failToLoadStub.calledWithExactly( - 'Unrecognized plugin path foo/bar', - 'foo/bar' - )); - }); - - test('relative path for plugins', () => { - Gerrit._loadPlugins([ - 'foo/bar.js', - 'foo/bar.html', - ]); - - assert.isTrue(importHtmlPluginStub.calledOnce); - assert.isTrue( - importHtmlPluginStub.calledWithExactly(`${url}/foo/bar.html`) - ); - assert.isTrue(loadJsPluginStub.calledOnce); - assert.isTrue( - loadJsPluginStub.calledWithExactly(`${url}/foo/bar.js`) - ); - }); - - test('relative path should honor getBaseUrl', () => { - const testUrl = '/test'; - sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl', () => testUrl); - - Gerrit._loadPlugins([ - 'foo/bar.js', - 'foo/bar.html', - ]); - - assert.isTrue(importHtmlPluginStub.calledOnce); - assert.isTrue(loadJsPluginStub.calledOnce); - assert.isTrue( - importHtmlPluginStub.calledWithExactly( - `${url}${testUrl}/foo/bar.html` - ) - ); - assert.isTrue( - loadJsPluginStub.calledWithExactly(`${url}${testUrl}/foo/bar.js`) - ); - }); - - test('absolute path for plugins', () => { - Gerrit._loadPlugins([ - 'http://e.com/foo/bar.js', - 'http://e.com/foo/bar.html', - ]); - - assert.isTrue(importHtmlPluginStub.calledOnce); - assert.isTrue( - importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`) - ); - assert.isTrue(loadJsPluginStub.calledOnce); - assert.isTrue( - loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`) - ); - }); - }); - - suite('With ASSETS_PATH', () => { - let importHtmlPluginStub; - let loadJsPluginStub; - setup(() => { - window.ASSETS_PATH = 'https://cdn.com'; - importHtmlPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_loadHtmlPlugin', url => { - importHtmlPluginStub(url); - }); - loadJsPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_createScriptTag', url => { - loadJsPluginStub(url); - }); - }); - - teardown(() => { - window.ASSETS_PATH = ''; - }); - - test('Should try load plugins from assets path instead', () => { - Gerrit._loadPlugins([ - 'foo/bar.js', - 'foo/bar.html', - ]); - - assert.isTrue(importHtmlPluginStub.calledOnce); - assert.isTrue( - importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`) - ); - assert.isTrue(loadJsPluginStub.calledOnce); - assert.isTrue( - loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`)); - }); - - test('Should honor original path if exists', () => { - Gerrit._loadPlugins([ - 'http://e.com/foo/bar.html', - 'http://e.com/foo/bar.js', - ]); - - assert.isTrue(importHtmlPluginStub.calledOnce); - assert.isTrue( - importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`) - ); - assert.isTrue(loadJsPluginStub.calledOnce); - assert.isTrue( - loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)); - }); - - test('Should try replace current host with assetsPath', () => { - const host = window.location.origin; - Gerrit._loadPlugins([ - `${host}/foo/bar.html`, - `${host}/foo/bar.js`, - ]); - - assert.isTrue(importHtmlPluginStub.calledOnce); - assert.isTrue( - importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`) - ); - assert.isTrue(loadJsPluginStub.calledOnce); - assert.isTrue( - loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`)); - }); - }); - - test('adds js plugins will call the body', () => { + test('Should try load plugins from assets path instead', () => { Gerrit._loadPlugins([ - 'http://e.com/foo/bar.js', - 'http://e.com/bar/foo.js', + 'foo/bar.js', + 'foo/bar.html', ]); - assert.isTrue(document.body.appendChild.calledTwice); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`)); }); - test('can call awaitPluginsLoaded multiple times', done => { - const plugins = [ + test('Should honor original path if exists', () => { + Gerrit._loadPlugins([ + 'http://e.com/foo/bar.html', 'http://e.com/foo/bar.js', - 'http://e.com/bar/foo.js', - ]; + ]); - let installed = false; - function pluginCallback(url) { - if (url === plugins[1]) { - installed = true; - } + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`http://e.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`http://e.com/foo/bar.js`)); + }); + + test('Should try replace current host with assetsPath', () => { + const host = window.location.origin; + Gerrit._loadPlugins([ + `${host}/foo/bar.html`, + `${host}/foo/bar.js`, + ]); + + assert.isTrue(importHtmlPluginStub.calledOnce); + assert.isTrue( + importHtmlPluginStub.calledWithExactly(`https://cdn.com/foo/bar.html`) + ); + assert.isTrue(loadJsPluginStub.calledOnce); + assert.isTrue( + loadJsPluginStub.calledWithExactly(`https://cdn.com/foo/bar.js`)); + }); + }); + + test('adds js plugins will call the body', () => { + Gerrit._loadPlugins([ + 'http://e.com/foo/bar.js', + 'http://e.com/bar/foo.js', + ]); + assert.isTrue(document.body.appendChild.calledTwice); + }); + + test('can call awaitPluginsLoaded multiple times', done => { + const plugins = [ + 'http://e.com/foo/bar.js', + 'http://e.com/bar/foo.js', + ]; + + let installed = false; + function pluginCallback(url) { + if (url === plugins[1]) { + installed = true; } - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - Gerrit.install(() => pluginCallback(url), undefined, url); - }); + } + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + Gerrit.install(() => pluginCallback(url), undefined, url); + }); - Gerrit._loadPlugins(plugins); + Gerrit._loadPlugins(plugins); + + Gerrit.awaitPluginsLoaded().then(() => { + assert.isTrue(installed); Gerrit.awaitPluginsLoaded().then(() => { - assert.isTrue(installed); - - Gerrit.awaitPluginsLoaded().then(() => { - done(); - }); - }); - }); - - suite('preloaded plugins', () => { - test('skips preloaded plugins when load plugins', () => { - const importHtmlPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => { - importHtmlPluginStub(url); - }); - const loadJsPluginStub = sandbox.stub(); - sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { - loadJsPluginStub(url); - }); - - Gerrit._preloadedPlugins = { - foo: () => void 0, - bar: () => void 0, - }; - - Gerrit._loadPlugins([ - 'http://e.com/plugins/foo.js', - 'plugins/bar.html', - 'http://e.com/plugins/test/foo.js', - ]); - - assert.isTrue(importHtmlPluginStub.notCalled); - assert.isTrue(loadJsPluginStub.calledOnce); - }); - - test('isPluginPreloaded', () => { - Gerrit._preloadedPlugins = {baz: ()=>{}}; - assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar')); - assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42')); - assert.isTrue( - Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz') - ); - Gerrit._preloadedPlugins = null; - }); - - test('preloaded plugins are installed', () => { - const installStub = sandbox.stub(); - Gerrit._preloadedPlugins = {foo: installStub}; - Gerrit._pluginLoader.installPreloadedPlugins(); - assert.isTrue(installStub.called); - const pluginApi = installStub.lastCall.args[0]; - assert.strictEqual(pluginApi.getPluginName(), 'foo'); - }); - - test('installing preloaded plugin', () => { - let plugin; - Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo'); - assert.strictEqual(plugin.getPluginName(), 'foo'); - assert.strictEqual(plugin.url('/some/thing.html'), - `${window.location.origin}/plugins/foo/some/thing.html`); + done(); }); }); }); + + suite('preloaded plugins', () => { + test('skips preloaded plugins when load plugins', () => { + const importHtmlPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_importHtmlPlugin', url => { + importHtmlPluginStub(url); + }); + const loadJsPluginStub = sandbox.stub(); + sandbox.stub(Gerrit._pluginLoader, '_loadJsPlugin', url => { + loadJsPluginStub(url); + }); + + Gerrit._preloadedPlugins = { + foo: () => void 0, + bar: () => void 0, + }; + + Gerrit._loadPlugins([ + 'http://e.com/plugins/foo.js', + 'plugins/bar.html', + 'http://e.com/plugins/test/foo.js', + ]); + + assert.isTrue(importHtmlPluginStub.notCalled); + assert.isTrue(loadJsPluginStub.calledOnce); + }); + + test('isPluginPreloaded', () => { + Gerrit._preloadedPlugins = {baz: ()=>{}}; + assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('plugins/foo/bar')); + assert.isFalse(Gerrit._pluginLoader.isPluginPreloaded('http://a.com/42')); + assert.isTrue( + Gerrit._pluginLoader.isPluginPreloaded(PRELOADED_PROTOCOL + 'baz') + ); + Gerrit._preloadedPlugins = null; + }); + + test('preloaded plugins are installed', () => { + const installStub = sandbox.stub(); + Gerrit._preloadedPlugins = {foo: installStub}; + Gerrit._pluginLoader.installPreloadedPlugins(); + assert.isTrue(installStub.called); + const pluginApi = installStub.lastCall.args[0]; + assert.strictEqual(pluginApi.getPluginName(), 'foo'); + }); + + test('installing preloaded plugin', () => { + let plugin; + Gerrit.install(p => { plugin = p; }, '0.1', 'preloaded:foo'); + assert.strictEqual(plugin.getPluginName(), 'foo'); + assert.strictEqual(plugin.url('/some/thing.html'), + `${window.location.origin}/plugins/foo/some/thing.html`); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html index a486bf1..64c31d7 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.html
@@ -19,139 +19,141 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-plugin-rest-api</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-js-api-interface.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-js-api-interface.js"></script> -<script> - suite('gr-plugin-rest-api tests', async () => { - await readyToTest(); - let instance; - let sandbox; - let getResponseObjectStub; - let sendStub; - let restApiStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-js-api-interface.js'; +suite('gr-plugin-rest-api tests', () => { + let instance; + let sandbox; + let getResponseObjectStub; + let sendStub; + let restApiStub; - setup(() => { - sandbox = sinon.sandbox.create(); - getResponseObjectStub = sandbox.stub().returns(Promise.resolve()); - sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); - restApiStub = { - getAccount: () => Promise.resolve({name: 'Judy Hopps'}), - getResponseObject: getResponseObjectStub, - send: sendStub, - getLoggedIn: sandbox.stub(), - getVersion: sandbox.stub(), - getConfig: sandbox.stub(), - }; - stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => { - a[k] = (...args) => restApiStub[k](...args); - return a; - }, {})); - Gerrit.install(p => {}, '0.1', - 'http://test.com/plugins/testplugin/static/test.js'); - instance = new GrPluginRestApi(); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + getResponseObjectStub = sandbox.stub().returns(Promise.resolve()); + sendStub = sandbox.stub().returns(Promise.resolve({status: 200})); + restApiStub = { + getAccount: () => Promise.resolve({name: 'Judy Hopps'}), + getResponseObject: getResponseObjectStub, + send: sendStub, + getLoggedIn: sandbox.stub(), + getVersion: sandbox.stub(), + getConfig: sandbox.stub(), + }; + stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => { + a[k] = (...args) => restApiStub[k](...args); + return a; + }, {})); + Gerrit.install(p => {}, '0.1', + 'http://test.com/plugins/testplugin/static/test.js'); + instance = new GrPluginRestApi(); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('fetch', () => { - const payload = {foo: 'foo'}; - return instance.fetch('HTTP_METHOD', '/url', payload).then(r => { - assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload)); - assert.equal(r.status, 200); - assert.isFalse(getResponseObjectStub.called); - }); - }); - - test('send', () => { - const payload = {foo: 'foo'}; - const response = {bar: 'bar'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return instance.send('HTTP_METHOD', '/url', payload).then(r => { - assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload)); - assert.strictEqual(r, response); - }); - }); - - test('get', () => { - const response = {foo: 'foo'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return instance.get('/url').then(r => { - assert.isTrue(sendStub.calledWith('GET', '/url')); - assert.strictEqual(r, response); - }); - }); - - test('post', () => { - const payload = {foo: 'foo'}; - const response = {bar: 'bar'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return instance.post('/url', payload).then(r => { - assert.isTrue(sendStub.calledWith('POST', '/url', payload)); - assert.strictEqual(r, response); - }); - }); - - test('put', () => { - const payload = {foo: 'foo'}; - const response = {bar: 'bar'}; - getResponseObjectStub.returns(Promise.resolve(response)); - return instance.put('/url', payload).then(r => { - assert.isTrue(sendStub.calledWith('PUT', '/url', payload)); - assert.strictEqual(r, response); - }); - }); - - test('delete works', () => { - const response = {status: 204}; - sendStub.returns(Promise.resolve(response)); - return instance.delete('/url').then(r => { - assert.isTrue(sendStub.calledWith('DELETE', '/url')); - assert.strictEqual(r, response); - }); - }); - - test('delete fails', () => { - sendStub.returns(Promise.resolve( - {status: 400, text() { return Promise.resolve('text'); }})); - return instance.delete('/url').then(r => { - throw new Error('Should not resolve'); - }) - .catch(err => { - assert.isTrue(sendStub.calledWith('DELETE', '/url')); - assert.equal('text', err.message); - }); - }); - - test('getLoggedIn', () => { - restApiStub.getLoggedIn.returns(Promise.resolve(true)); - return instance.getLoggedIn().then(result => { - assert.isTrue(restApiStub.getLoggedIn.calledOnce); - assert.isTrue(result); - }); - }); - - test('getVersion', () => { - restApiStub.getVersion.returns(Promise.resolve('foo bar')); - return instance.getVersion().then(result => { - assert.isTrue(restApiStub.getVersion.calledOnce); - assert.equal(result, 'foo bar'); - }); - }); - - test('getConfig', () => { - restApiStub.getConfig.returns(Promise.resolve('foo bar')); - return instance.getConfig().then(result => { - assert.isTrue(restApiStub.getConfig.calledOnce); - assert.equal(result, 'foo bar'); - }); + test('fetch', () => { + const payload = {foo: 'foo'}; + return instance.fetch('HTTP_METHOD', '/url', payload).then(r => { + assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload)); + assert.equal(r.status, 200); + assert.isFalse(getResponseObjectStub.called); }); }); + + test('send', () => { + const payload = {foo: 'foo'}; + const response = {bar: 'bar'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return instance.send('HTTP_METHOD', '/url', payload).then(r => { + assert.isTrue(sendStub.calledWith('HTTP_METHOD', '/url', payload)); + assert.strictEqual(r, response); + }); + }); + + test('get', () => { + const response = {foo: 'foo'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return instance.get('/url').then(r => { + assert.isTrue(sendStub.calledWith('GET', '/url')); + assert.strictEqual(r, response); + }); + }); + + test('post', () => { + const payload = {foo: 'foo'}; + const response = {bar: 'bar'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return instance.post('/url', payload).then(r => { + assert.isTrue(sendStub.calledWith('POST', '/url', payload)); + assert.strictEqual(r, response); + }); + }); + + test('put', () => { + const payload = {foo: 'foo'}; + const response = {bar: 'bar'}; + getResponseObjectStub.returns(Promise.resolve(response)); + return instance.put('/url', payload).then(r => { + assert.isTrue(sendStub.calledWith('PUT', '/url', payload)); + assert.strictEqual(r, response); + }); + }); + + test('delete works', () => { + const response = {status: 204}; + sendStub.returns(Promise.resolve(response)); + return instance.delete('/url').then(r => { + assert.isTrue(sendStub.calledWith('DELETE', '/url')); + assert.strictEqual(r, response); + }); + }); + + test('delete fails', () => { + sendStub.returns(Promise.resolve( + {status: 400, text() { return Promise.resolve('text'); }})); + return instance.delete('/url').then(r => { + throw new Error('Should not resolve'); + }) + .catch(err => { + assert.isTrue(sendStub.calledWith('DELETE', '/url')); + assert.equal('text', err.message); + }); + }); + + test('getLoggedIn', () => { + restApiStub.getLoggedIn.returns(Promise.resolve(true)); + return instance.getLoggedIn().then(result => { + assert.isTrue(restApiStub.getLoggedIn.calledOnce); + assert.isTrue(result); + }); + }); + + test('getVersion', () => { + restApiStub.getVersion.returns(Promise.resolve('foo bar')); + return instance.getVersion().then(result => { + assert.isTrue(restApiStub.getVersion.calledOnce); + assert.equal(result, 'foo bar'); + }); + }); + + test('getConfig', () => { + restApiStub.getConfig.returns(Promise.resolve('foo bar')); + return instance.getConfig().then(result => { + assert.isTrue(restApiStub.getConfig.calledOnce); + assert.equal(result, 'foo bar'); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js index 6e99e01..ad9b852 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info.js
@@ -14,156 +14,170 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrLabelInfo extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-label-info'; } +import '../../../styles/gr-voting-styles.js'; +import '../../../styles/shared-styles.js'; +import '../gr-account-label/gr-account-label.js'; +import '../gr-account-chip/gr-account-chip.js'; +import '../gr-button/gr-button.js'; +import '../gr-icons/gr-icons.js'; +import '../gr-label/gr-label.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-label-info_html.js'; - static get properties() { - return { - labelInfo: Object, - label: String, - /** @type {?} */ - change: Object, - account: Object, - mutable: Boolean, - }; - } +/** @extends Polymer.Element */ +class GrLabelInfo extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** - * @param {!Object} labelInfo - * @param {!Object} account - * @param {Object} changeLabelsRecord not used, but added as a parameter in - * order to trigger computation when a label is removed from the change. - */ - _mapLabelInfo(labelInfo, account, changeLabelsRecord) { - const result = []; - if (!labelInfo || !account) { return result; } - if (!labelInfo.values) { - if (labelInfo.rejected || labelInfo.approved) { - const ok = labelInfo.approved || !labelInfo.rejected; - return [{ - value: ok ? '👍️' : '👎️', - className: ok ? 'positive' : 'negative', - account: ok ? labelInfo.approved : labelInfo.rejected, - }]; - } - return result; - } - // Sort votes by positivity. - const votes = (labelInfo.all || []).sort((a, b) => a.value - b.value); - const values = Object.keys(labelInfo.values); - for (const label of votes) { - if (label.value && label.value != labelInfo.default_value) { - let labelClassName; - let labelValPrefix = ''; - if (label.value > 0) { - labelValPrefix = '+'; - if (parseInt(label.value, 10) === - parseInt(values[values.length - 1], 10)) { - labelClassName = 'max'; - } else { - labelClassName = 'positive'; - } - } else if (label.value < 0) { - if (parseInt(label.value, 10) === parseInt(values[0], 10)) { - labelClassName = 'min'; - } else { - labelClassName = 'negative'; - } - } - const formattedLabel = { - value: labelValPrefix + label.value, - className: labelClassName, - account: label, - }; - if (label._account_id === account._account_id) { - // Put self-votes at the top. - result.unshift(formattedLabel); - } else { - result.push(formattedLabel); - } - } + static get is() { return 'gr-label-info'; } + + static get properties() { + return { + labelInfo: Object, + label: String, + /** @type {?} */ + change: Object, + account: Object, + mutable: Boolean, + }; + } + + /** + * @param {!Object} labelInfo + * @param {!Object} account + * @param {Object} changeLabelsRecord not used, but added as a parameter in + * order to trigger computation when a label is removed from the change. + */ + _mapLabelInfo(labelInfo, account, changeLabelsRecord) { + const result = []; + if (!labelInfo || !account) { return result; } + if (!labelInfo.values) { + if (labelInfo.rejected || labelInfo.approved) { + const ok = labelInfo.approved || !labelInfo.rejected; + return [{ + value: ok ? '👍️' : '👎️', + className: ok ? 'positive' : 'negative', + account: ok ? labelInfo.approved : labelInfo.rejected, + }]; } return result; } - - /** - * A user is able to delete a vote iff the mutable property is true and the - * reviewer that left the vote exists in the list of removable_reviewers - * received from the backend. - * - * @param {!Object} reviewer An object describing the reviewer that left the - * vote. - * @param {boolean} mutable - * @param {!Object} change - */ - _computeDeleteClass(reviewer, mutable, change) { - if (!mutable || !change || !change.removable_reviewers) { - return 'hidden'; - } - const removable = change.removable_reviewers; - if (removable.find(r => r._account_id === reviewer._account_id)) { - return ''; - } - return 'hidden'; - } - - /** - * Closure annotation for Polymer.prototype.splice is off. - * For now, supressing annotations. - * - * @suppress {checkTypes} */ - _onDeleteVote(e) { - e.preventDefault(); - let target = Polymer.dom(e).rootTarget; - while (!target.classList.contains('deleteBtn')) { - if (!target.parentElement) { return; } - target = target.parentElement; - } - - target.disabled = true; - const accountID = parseInt(target.getAttribute('data-account-id'), 10); - this._xhrPromise = - this.$.restAPI.deleteVote(this.change._number, accountID, this.label) - .then(response => { - target.disabled = false; - if (!response.ok) { return; } - Gerrit.Nav.navigateToChange(this.change); - }) - .catch(err => { - target.disabled = false; - return; - }); - } - - _computeValueTooltip(labelInfo, score) { - if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) { - return ''; - } - return labelInfo.values[score]; - } - - /** - * @param {!Object} labelInfo - * @param {Object} changeLabelsRecord not used, but added as a parameter in - * order to trigger computation when a label is removed from the change. - */ - _computeShowPlaceholder(labelInfo, changeLabelsRecord) { - if (labelInfo && labelInfo.all) { - for (const label of labelInfo.all) { - if (label.value && label.value != labelInfo.default_value) { - return 'hidden'; + // Sort votes by positivity. + const votes = (labelInfo.all || []).sort((a, b) => a.value - b.value); + const values = Object.keys(labelInfo.values); + for (const label of votes) { + if (label.value && label.value != labelInfo.default_value) { + let labelClassName; + let labelValPrefix = ''; + if (label.value > 0) { + labelValPrefix = '+'; + if (parseInt(label.value, 10) === + parseInt(values[values.length - 1], 10)) { + labelClassName = 'max'; + } else { + labelClassName = 'positive'; + } + } else if (label.value < 0) { + if (parseInt(label.value, 10) === parseInt(values[0], 10)) { + labelClassName = 'min'; + } else { + labelClassName = 'negative'; } } + const formattedLabel = { + value: labelValPrefix + label.value, + className: labelClassName, + account: label, + }; + if (label._account_id === account._account_id) { + // Put self-votes at the top. + result.unshift(formattedLabel); + } else { + result.push(formattedLabel); + } } - return ''; } + return result; } - customElements.define(GrLabelInfo.is, GrLabelInfo); -})(); + /** + * A user is able to delete a vote iff the mutable property is true and the + * reviewer that left the vote exists in the list of removable_reviewers + * received from the backend. + * + * @param {!Object} reviewer An object describing the reviewer that left the + * vote. + * @param {boolean} mutable + * @param {!Object} change + */ + _computeDeleteClass(reviewer, mutable, change) { + if (!mutable || !change || !change.removable_reviewers) { + return 'hidden'; + } + const removable = change.removable_reviewers; + if (removable.find(r => r._account_id === reviewer._account_id)) { + return ''; + } + return 'hidden'; + } + + /** + * Closure annotation for Polymer.prototype.splice is off. + * For now, supressing annotations. + * + * @suppress {checkTypes} */ + _onDeleteVote(e) { + e.preventDefault(); + let target = dom(e).rootTarget; + while (!target.classList.contains('deleteBtn')) { + if (!target.parentElement) { return; } + target = target.parentElement; + } + + target.disabled = true; + const accountID = parseInt(target.getAttribute('data-account-id'), 10); + this._xhrPromise = + this.$.restAPI.deleteVote(this.change._number, accountID, this.label) + .then(response => { + target.disabled = false; + if (!response.ok) { return; } + Gerrit.Nav.navigateToChange(this.change); + }) + .catch(err => { + target.disabled = false; + return; + }); + } + + _computeValueTooltip(labelInfo, score) { + if (!labelInfo || !labelInfo.values || !labelInfo.values[score]) { + return ''; + } + return labelInfo.values[score]; + } + + /** + * @param {!Object} labelInfo + * @param {Object} changeLabelsRecord not used, but added as a parameter in + * order to trigger computation when a label is removed from the change. + */ + _computeShowPlaceholder(labelInfo, changeLabelsRecord) { + if (labelInfo && labelInfo.all) { + for (const label of labelInfo.all) { + if (label.value && label.value != labelInfo.default_value) { + return 'hidden'; + } + } + } + return ''; + } +} + +customElements.define(GrLabelInfo.is, GrLabelInfo);
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js index 27bd1c7..d19467b 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_html.js
@@ -1,33 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../styles/gr-voting-styles.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../gr-account-label/gr-account-label.html"> -<link rel="import" href="../gr-account-chip/gr-account-chip.html"> -<link rel="import" href="../gr-button/gr-button.html"> -<link rel="import" href="../gr-icons/gr-icons.html"> -<link rel="import" href="../gr-label/gr-label.html"> -<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-label-info"> - <template> +export const htmlTemplate = html` <style include="gr-voting-styles"> /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ </style> @@ -90,36 +79,22 @@ padding-top: var(--spacing-s); } </style> - <table> - <p class$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]"> + <p class\$="placeholder [[_computeShowPlaceholder(labelInfo, change.labels.*)]]"> No votes. - </p> - <template - is="dom-repeat" - items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]" - as="mappedLabel"> + </p><table> + + <template is="dom-repeat" items="[[_mapLabelInfo(labelInfo, account, change.labels.*)]]" as="mappedLabel"> <tr class="labelValueContainer"> <td> - <gr-label - has-tooltip - title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]" - class$="[[mappedLabel.className]] voteChip"> + <gr-label has-tooltip="" title="[[_computeValueTooltip(labelInfo, mappedLabel.value)]]" class\$="[[mappedLabel.className]] voteChip"> [[mappedLabel.value]] </gr-label> </td> <td> - <gr-account-chip - account="[[mappedLabel.account]]" - transparent-background></gr-account-chip> + <gr-account-chip account="[[mappedLabel.account]]" transparent-background=""></gr-account-chip> </td> <td> - <gr-button - link - aria-label="Remove" - on-click="_onDeleteVote" - tooltip="Remove vote" - data-account-id$="[[mappedLabel.account._account_id]]" - class$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"> + <gr-button link="" aria-label="Remove" on-click="_onDeleteVote" tooltip="Remove vote" data-account-id\$="[[mappedLabel.account._account_id]]" class\$="deleteBtn [[_computeDeleteClass(mappedLabel.account, mutable, change)]]"> <iron-icon icon="gr-icons:delete"></iron-icon> </gr-button> </td> @@ -127,6 +102,4 @@ </template> </table> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-label-info.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html index 013a6ee..44627ab 100644 --- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html +++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-label-info</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-label-info.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-label-info.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-label-info.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,205 +39,208 @@ </template> </test-fixture> -<script> - suite('gr-account-link tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-label-info.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-account-link tests', () => { + let element; + let sandbox; + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + // Needed to trigger computed bindings. + element.account = {}; + element.change = {labels: {}}; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('remove reviewer votes', () => { setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - // Needed to trigger computed bindings. - element.account = {}; - element.change = {labels: {}}; + sandbox.stub(element, '_computeValueTooltip').returns(''); + element.account = { + _account_id: 1, + name: 'bojack', + }; + const test = { + all: [{_account_id: 1, name: 'bojack', value: 1}], + default_value: 0, + values: [], + }; + element.change = { + _number: 42, + change_id: 'the id', + actions: [], + topic: 'the topic', + status: 'NEW', + submit_type: 'CHERRY_PICK', + labels: {test}, + removable_reviewers: [], + }; + element.labelInfo = test; + element.label = 'test'; + + flushAsynchronousOperations(); }); - teardown(() => { - sandbox.restore(); + test('_computeCanDeleteVote', () => { + element.mutable = false; + const button = element.shadowRoot + .querySelector('gr-button'); + assert.isTrue(isHidden(button)); + element.change.removable_reviewers = [element.account]; + element.mutable = true; + assert.isFalse(isHidden(button)); }); - suite('remove reviewer votes', () => { - setup(() => { - sandbox.stub(element, '_computeValueTooltip').returns(''); - element.account = { - _account_id: 1, - name: 'bojack', - }; - const test = { - all: [{_account_id: 1, name: 'bojack', value: 1}], - default_value: 0, - values: [], - }; - element.change = { - _number: 42, - change_id: 'the id', - actions: [], - topic: 'the topic', - status: 'NEW', - submit_type: 'CHERRY_PICK', - labels: {test}, - removable_reviewers: [], - }; - element.labelInfo = test; - element.label = 'test'; + test('deletes votes', () => { + const deleteResponse = Promise.resolve({ok: true}); + const deleteStub = sandbox.stub( + element.$.restAPI, 'deleteVote').returns(deleteResponse); - flushAsynchronousOperations(); + element.change.removable_reviewers = [element.account]; + element.change.labels.test.recommended = {_account_id: 1}; + element.mutable = true; + const button = element.shadowRoot + .querySelector('gr-button'); + MockInteractions.tap(button); + assert.isTrue(button.disabled); + return deleteResponse.then(() => { + assert.isFalse(button.disabled); + assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test')); }); - - test('_computeCanDeleteVote', () => { - element.mutable = false; - const button = element.shadowRoot - .querySelector('gr-button'); - assert.isTrue(isHidden(button)); - element.change.removable_reviewers = [element.account]; - element.mutable = true; - assert.isFalse(isHidden(button)); - }); - - test('deletes votes', () => { - const deleteResponse = Promise.resolve({ok: true}); - const deleteStub = sandbox.stub( - element.$.restAPI, 'deleteVote').returns(deleteResponse); - - element.change.removable_reviewers = [element.account]; - element.change.labels.test.recommended = {_account_id: 1}; - element.mutable = true; - const button = element.shadowRoot - .querySelector('gr-button'); - MockInteractions.tap(button); - assert.isTrue(button.disabled); - return deleteResponse.then(() => { - assert.isFalse(button.disabled); - assert.isTrue(deleteStub.calledWithExactly(42, 1, 'test')); - }); - }); - }); - - suite('label color and order', () => { - test('valueless label rejected', () => { - element.labelInfo = {rejected: {name: 'someone'}}; - flushAsynchronousOperations(); - const labels = Polymer.dom(element.root).querySelectorAll('gr-label'); - assert.isTrue(labels[0].classList.contains('negative')); - }); - - test('valueless label approved', () => { - element.labelInfo = {approved: {name: 'someone'}}; - flushAsynchronousOperations(); - const labels = Polymer.dom(element.root).querySelectorAll('gr-label'); - assert.isTrue(labels[0].classList.contains('positive')); - }); - - test('-2 to +2', () => { - element.labelInfo = { - all: [ - {value: 2, name: 'user 2'}, - {value: 1, name: 'user 1'}, - {value: -1, name: 'user 3'}, - {value: -2, name: 'user 4'}, - ], - values: { - '-2': 'Awful', - '-1': 'Don\'t submit as-is', - ' 0': 'No score', - '+1': 'Looks good to me', - '+2': 'Ready to submit', - }, - }; - flushAsynchronousOperations(); - const labels = Polymer.dom(element.root).querySelectorAll('gr-label'); - assert.isTrue(labels[0].classList.contains('max')); - assert.isTrue(labels[1].classList.contains('positive')); - assert.isTrue(labels[2].classList.contains('negative')); - assert.isTrue(labels[3].classList.contains('min')); - }); - - test('-1 to +1', () => { - element.labelInfo = { - all: [ - {value: 1, name: 'user 1'}, - {value: -1, name: 'user 2'}, - ], - values: { - '-1': 'Don\'t submit as-is', - ' 0': 'No score', - '+1': 'Looks good to me', - }, - }; - flushAsynchronousOperations(); - const labels = Polymer.dom(element.root).querySelectorAll('gr-label'); - assert.isTrue(labels[0].classList.contains('max')); - assert.isTrue(labels[1].classList.contains('min')); - }); - - test('0 to +2', () => { - element.labelInfo = { - all: [ - {value: 1, name: 'user 2'}, - {value: 2, name: 'user '}, - ], - values: { - ' 0': 'Don\'t submit as-is', - '+1': 'No score', - '+2': 'Looks good to me', - }, - }; - flushAsynchronousOperations(); - const labels = Polymer.dom(element.root).querySelectorAll('gr-label'); - assert.isTrue(labels[0].classList.contains('max')); - assert.isTrue(labels[1].classList.contains('positive')); - }); - - test('self votes at top', () => { - element.account = { - _account_id: 1, - name: 'bojack', - }; - element.labelInfo = { - all: [ - {value: 1, name: 'user 1', _account_id: 2}, - {value: -1, name: 'bojack', _account_id: 1}, - ], - values: { - '-1': 'Don\'t submit as-is', - ' 0': 'No score', - '+1': 'Looks good to me', - }, - }; - flushAsynchronousOperations(); - const chips = - Polymer.dom(element.root).querySelectorAll('gr-account-chip'); - assert.equal(chips[0].account._account_id, element.account._account_id); - }); - }); - - test('_computeValueTooltip', () => { - // Existing label. - let labelInfo = {values: {0: 'Baz'}}; - let score = '0'; - assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz'); - - // Non-exsistent score. - score = '2'; - assert.equal(element._computeValueTooltip(labelInfo, score), ''); - - // No values on label. - labelInfo = {values: {}}; - score = '0'; - assert.equal(element._computeValueTooltip(labelInfo, score), ''); - }); - - test('placeholder', () => { - element.labelInfo = {}; - assert.isFalse(isHidden(element.shadowRoot - .querySelector('.placeholder'))); - element.labelInfo = {all: []}; - assert.isFalse(isHidden(element.shadowRoot - .querySelector('.placeholder'))); - element.labelInfo = {all: [{value: 1}]}; - assert.isTrue(isHidden(element.shadowRoot - .querySelector('.placeholder'))); }); }); + + suite('label color and order', () => { + test('valueless label rejected', () => { + element.labelInfo = {rejected: {name: 'someone'}}; + flushAsynchronousOperations(); + const labels = dom(element.root).querySelectorAll('gr-label'); + assert.isTrue(labels[0].classList.contains('negative')); + }); + + test('valueless label approved', () => { + element.labelInfo = {approved: {name: 'someone'}}; + flushAsynchronousOperations(); + const labels = dom(element.root).querySelectorAll('gr-label'); + assert.isTrue(labels[0].classList.contains('positive')); + }); + + test('-2 to +2', () => { + element.labelInfo = { + all: [ + {value: 2, name: 'user 2'}, + {value: 1, name: 'user 1'}, + {value: -1, name: 'user 3'}, + {value: -2, name: 'user 4'}, + ], + values: { + '-2': 'Awful', + '-1': 'Don\'t submit as-is', + ' 0': 'No score', + '+1': 'Looks good to me', + '+2': 'Ready to submit', + }, + }; + flushAsynchronousOperations(); + const labels = dom(element.root).querySelectorAll('gr-label'); + assert.isTrue(labels[0].classList.contains('max')); + assert.isTrue(labels[1].classList.contains('positive')); + assert.isTrue(labels[2].classList.contains('negative')); + assert.isTrue(labels[3].classList.contains('min')); + }); + + test('-1 to +1', () => { + element.labelInfo = { + all: [ + {value: 1, name: 'user 1'}, + {value: -1, name: 'user 2'}, + ], + values: { + '-1': 'Don\'t submit as-is', + ' 0': 'No score', + '+1': 'Looks good to me', + }, + }; + flushAsynchronousOperations(); + const labels = dom(element.root).querySelectorAll('gr-label'); + assert.isTrue(labels[0].classList.contains('max')); + assert.isTrue(labels[1].classList.contains('min')); + }); + + test('0 to +2', () => { + element.labelInfo = { + all: [ + {value: 1, name: 'user 2'}, + {value: 2, name: 'user '}, + ], + values: { + ' 0': 'Don\'t submit as-is', + '+1': 'No score', + '+2': 'Looks good to me', + }, + }; + flushAsynchronousOperations(); + const labels = dom(element.root).querySelectorAll('gr-label'); + assert.isTrue(labels[0].classList.contains('max')); + assert.isTrue(labels[1].classList.contains('positive')); + }); + + test('self votes at top', () => { + element.account = { + _account_id: 1, + name: 'bojack', + }; + element.labelInfo = { + all: [ + {value: 1, name: 'user 1', _account_id: 2}, + {value: -1, name: 'bojack', _account_id: 1}, + ], + values: { + '-1': 'Don\'t submit as-is', + ' 0': 'No score', + '+1': 'Looks good to me', + }, + }; + flushAsynchronousOperations(); + const chips = + dom(element.root).querySelectorAll('gr-account-chip'); + assert.equal(chips[0].account._account_id, element.account._account_id); + }); + }); + + test('_computeValueTooltip', () => { + // Existing label. + let labelInfo = {values: {0: 'Baz'}}; + let score = '0'; + assert.equal(element._computeValueTooltip(labelInfo, score), 'Baz'); + + // Non-exsistent score. + score = '2'; + assert.equal(element._computeValueTooltip(labelInfo, score), ''); + + // No values on label. + labelInfo = {values: {}}; + score = '0'; + assert.equal(element._computeValueTooltip(labelInfo, score), ''); + }); + + test('placeholder', () => { + element.labelInfo = {}; + assert.isFalse(isHidden(element.shadowRoot + .querySelector('.placeholder'))); + element.labelInfo = {all: []}; + assert.isFalse(isHidden(element.shadowRoot + .querySelector('.placeholder'))); + element.labelInfo = {all: [{value: 1}]}; + assert.isTrue(isHidden(element.shadowRoot + .querySelector('.placeholder'))); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js index b594757..c797919 100644 --- a/polygerrit-ui/app/elements/shared/gr-label/gr-label.js +++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label.js
@@ -14,20 +14,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.TooltipMixin - * @extends Polymer.Element - */ - class GrLabel extends Polymer.mixinBehaviors( [ - Gerrit.TooltipBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-label'; } - } +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-label_html.js'; - customElements.define(GrLabel.is, GrLabel); -})(); +/** + * @appliesMixin Gerrit.TooltipMixin + * @extends Polymer.Element + */ +class GrLabel extends mixinBehaviors( [ + Gerrit.TooltipBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-label'; } +} + +customElements.define(GrLabel.is, GrLabel);
diff --git a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js index c1c9b23..1644c07 100644 --- a/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js +++ b/polygerrit-ui/app/elements/shared/gr-label/gr-label_html.js
@@ -1,24 +1,21 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<dom-module id="gr-label"> - <template> +export const htmlTemplate = html` <slot></slot> - </template> - <script src="gr-label.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js index cb5ad7c..f585347 100644 --- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js +++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete.js
@@ -14,69 +14,76 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrLabeledAutocomplete extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-labeled-autocomplete'; } - /** - * Fired when a value is chosen. - * - * @event commit - */ +import '../gr-autocomplete/gr-autocomplete.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-labeled-autocomplete_html.js'; - static get properties() { - return { +/** @extends Polymer.Element */ +class GrLabeledAutocomplete extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** - * Used just like the query property of gr-autocomplete. - * - * @type {function(string): Promise<?>} - */ - query: { - type: Function, - value() { - return function() { - return Promise.resolve([]); - }; - }, + static get is() { return 'gr-labeled-autocomplete'; } + /** + * Fired when a value is chosen. + * + * @event commit + */ + + static get properties() { + return { + + /** + * Used just like the query property of gr-autocomplete. + * + * @type {function(string): Promise<?>} + */ + query: { + type: Function, + value() { + return function() { + return Promise.resolve([]); + }; }, + }, - text: { - type: String, - value: '', - notify: true, - }, - label: String, - placeholder: String, - disabled: Boolean, + text: { + type: String, + value: '', + notify: true, + }, + label: String, + placeholder: String, + disabled: Boolean, - _autocompleteThreshold: { - type: Number, - value: 0, - readOnly: true, - }, - }; - } - - _handleTriggerClick(e) { - // Stop propagation here so we don't confuse gr-autocomplete, which - // listens for taps on body to try to determine when it's blurred. - e.stopPropagation(); - this.$.autocomplete.focus(); - } - - setText(text) { - this.$.autocomplete.setText(text); - } - - clear() { - this.setText(''); - } + _autocompleteThreshold: { + type: Number, + value: 0, + readOnly: true, + }, + }; } - customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete); -})(); + _handleTriggerClick(e) { + // Stop propagation here so we don't confuse gr-autocomplete, which + // listens for taps on body to try to determine when it's blurred. + e.stopPropagation(); + this.$.autocomplete.focus(); + } + + setText(text) { + this.$.autocomplete.setText(text); + } + + clear() { + this.setText(''); + } +} + +customElements.define(GrLabeledAutocomplete.is, GrLabeledAutocomplete);
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js index 47be6f7..fe0b03c 100644 --- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js +++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-labeled-autocomplete"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -50,16 +47,8 @@ <div id="container"> <div id="header">[[label]]</div> <div id="body"> - <gr-autocomplete - id="autocomplete" - threshold="[[_autocompleteThreshold]]" - query="[[query]]" - disabled="[[disabled]]" - placeholder="[[placeholder]]" - borderless></gr-autocomplete> + <gr-autocomplete id="autocomplete" threshold="[[_autocompleteThreshold]]" query="[[query]]" disabled="[[disabled]]" placeholder="[[placeholder]]" borderless=""></gr-autocomplete> <div id="trigger" on-click="_handleTriggerClick">▼</div> </div> </div> - </template> - <script src="gr-labeled-autocomplete.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html index e79e741..cd58932 100644 --- a/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html +++ b/polygerrit-ui/app/elements/shared/gr-labeled-autocomplete/gr-labeled-autocomplete_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-labeled-autocomplete</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-labeled-autocomplete.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-labeled-autocomplete.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-labeled-autocomplete.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,32 +39,34 @@ </template> </test-fixture> -<script> - suite('gr-labeled-autocomplete tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-labeled-autocomplete.js'; +suite('gr-labeled-autocomplete tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { sandbox.restore(); }); - - test('tapping trigger focuses autocomplete', () => { - const e = {stopPropagation: () => undefined}; - sandbox.stub(e, 'stopPropagation'); - sandbox.stub(element.$.autocomplete, 'focus'); - element._handleTriggerClick(e); - assert.isTrue(e.stopPropagation.calledOnce); - assert.isTrue(element.$.autocomplete.focus.calledOnce); - }); - - test('setText', () => { - sandbox.stub(element.$.autocomplete, 'setText'); - element.setText('foo-bar'); - assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar')); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { sandbox.restore(); }); + + test('tapping trigger focuses autocomplete', () => { + const e = {stopPropagation: () => undefined}; + sandbox.stub(e, 'stopPropagation'); + sandbox.stub(element.$.autocomplete, 'focus'); + element._handleTriggerClick(e); + assert.isTrue(e.stopPropagation.calledOnce); + assert.isTrue(element.$.autocomplete.focus.calledOnce); + }); + + test('setText', () => { + sandbox.stub(element.$.autocomplete, 'setText'); + element.setText('foo-bar'); + assert.isTrue(element.$.autocomplete.setText.calledWith('foo-bar')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js index 81126ec..9bd8a11 100644 --- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -14,155 +14,163 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; - const DARK_THEME_PATH = 'styles/themes/dark-theme.html'; +import '../gr-js-api-interface/gr-js-api-interface.js'; +import {importHref} from '../../../scripts/import-href.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-lib-loader_html.js'; - /** @extends Polymer.Element */ - class GrLibLoader extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-lib-loader'; } +const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; +const DARK_THEME_PATH = 'styles/themes/dark-theme.html'; - static get properties() { - return { - _hljsState: { - type: Object, +/** @extends Polymer.Element */ +class GrLibLoader extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - // NOTE: intended singleton. - value: { - configured: false, - loading: false, - callbacks: [], - }, + static get is() { return 'gr-lib-loader'; } + + static get properties() { + return { + _hljsState: { + type: Object, + + // NOTE: intended singleton. + value: { + configured: false, + loading: false, + callbacks: [], }, - }; - } - - /** - * Get the HLJS library. Returns a promise that resolves with a reference to - * the library after it's been loaded. The promise resolves immediately if - * it's already been loaded. - * - * @return {!Promise<Object>} - */ - getHLJS() { - return new Promise((resolve, reject) => { - // If the lib is totally loaded, resolve immediately. - if (this._getHighlightLib()) { - resolve(this._getHighlightLib()); - return; - } - - // If the library is not currently being loaded, then start loading it. - if (!this._hljsState.loading) { - this._hljsState.loading = true; - this._loadScript(this._getHLJSUrl()) - .then(this._onHLJSLibLoaded.bind(this)) - .catch(reject); - } - - this._hljsState.callbacks.push(resolve); - }); - } - - /** - * Loads the dark theme document. Returns a promise that resolves with a - * custom-style DOM element. - * - * @return {!Promise<Element>} - * @suppress {checkTypes} - */ - getDarkTheme() { - return new Promise((resolve, reject) => { - Polymer.importHref( - this._getLibRoot() + DARK_THEME_PATH, () => { - const module = document.createElement('style'); - module.setAttribute('include', 'dark-theme'); - const cs = document.createElement('custom-style'); - cs.appendChild(module); - - resolve(cs); - }, - reject); - }); - } - - /** - * Execute callbacks awaiting the HLJS lib load. - */ - _onHLJSLibLoaded() { - const lib = this._getHighlightLib(); - this._hljsState.loading = false; - this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, { - hljs: lib, - }); - for (const cb of this._hljsState.callbacks) { - cb(lib); - } - this._hljsState.callbacks = []; - } - - /** - * Get the HLJS library, assuming it has been loaded. Configure the library - * if it hasn't already been configured. - * - * @return {!Object} - */ - _getHighlightLib() { - const lib = window.hljs; - if (lib && !this._hljsState.configured) { - this._hljsState.configured = true; - - lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'}); - } - return lib; - } - - /** - * Get the resource path used to load the application. If the application - * was loaded through a CDN, then this will be the path to CDN resources. - * - * @return {string} - */ - _getLibRoot() { - if (window.STATIC_RESOURCE_PATH) { - return window.STATIC_RESOURCE_PATH + '/'; - } - return '/'; - } - - /** - * Load and execute a JS file from the lib root. - * - * @param {string} src The path to the JS file without the lib root. - * @return {Promise} a promise that resolves when the script's onload - * executes. - */ - _loadScript(src) { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - - if (!src) { - reject(new Error('Unable to load blank script url.')); - return; - } - - script.setAttribute('src', src); - script.onload = resolve; - script.onerror = reject; - Polymer.dom(document.head).appendChild(script); - }); - } - - _getHLJSUrl() { - const root = this._getLibRoot(); - if (!root) { return null; } - return root + HLJS_PATH; - } + }, + }; } - customElements.define(GrLibLoader.is, GrLibLoader); -})(); + /** + * Get the HLJS library. Returns a promise that resolves with a reference to + * the library after it's been loaded. The promise resolves immediately if + * it's already been loaded. + * + * @return {!Promise<Object>} + */ + getHLJS() { + return new Promise((resolve, reject) => { + // If the lib is totally loaded, resolve immediately. + if (this._getHighlightLib()) { + resolve(this._getHighlightLib()); + return; + } + + // If the library is not currently being loaded, then start loading it. + if (!this._hljsState.loading) { + this._hljsState.loading = true; + this._loadScript(this._getHLJSUrl()) + .then(this._onHLJSLibLoaded.bind(this)) + .catch(reject); + } + + this._hljsState.callbacks.push(resolve); + }); + } + + /** + * Loads the dark theme document. Returns a promise that resolves with a + * custom-style DOM element. + * + * @return {!Promise<Element>} + * @suppress {checkTypes} + */ + getDarkTheme() { + return new Promise((resolve, reject) => { + importHref( + this._getLibRoot() + DARK_THEME_PATH, () => { + const module = document.createElement('style'); + module.setAttribute('include', 'dark-theme'); + const cs = document.createElement('custom-style'); + cs.appendChild(module); + + resolve(cs); + }, + reject); + }); + } + + /** + * Execute callbacks awaiting the HLJS lib load. + */ + _onHLJSLibLoaded() { + const lib = this._getHighlightLib(); + this._hljsState.loading = false; + this.$.jsAPI.handleEvent(this.$.jsAPI.EventType.HIGHLIGHTJS_LOADED, { + hljs: lib, + }); + for (const cb of this._hljsState.callbacks) { + cb(lib); + } + this._hljsState.callbacks = []; + } + + /** + * Get the HLJS library, assuming it has been loaded. Configure the library + * if it hasn't already been configured. + * + * @return {!Object} + */ + _getHighlightLib() { + const lib = window.hljs; + if (lib && !this._hljsState.configured) { + this._hljsState.configured = true; + + lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'}); + } + return lib; + } + + /** + * Get the resource path used to load the application. If the application + * was loaded through a CDN, then this will be the path to CDN resources. + * + * @return {string} + */ + _getLibRoot() { + if (window.STATIC_RESOURCE_PATH) { + return window.STATIC_RESOURCE_PATH + '/'; + } + return '/'; + } + + /** + * Load and execute a JS file from the lib root. + * + * @param {string} src The path to the JS file without the lib root. + * @return {Promise} a promise that resolves when the script's onload + * executes. + */ + _loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + + if (!src) { + reject(new Error('Unable to load blank script url.')); + return; + } + + script.setAttribute('src', src); + script.onload = resolve; + script.onerror = reject; + dom(document.head).appendChild(script); + }); + } + + _getHLJSUrl() { + const root = this._getLibRoot(); + if (!root) { return null; } + return root + HLJS_PATH; + } +} + +customElements.define(GrLibLoader.is, GrLibLoader);
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js index fb55c67..3bc0d72 100644 --- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_html.js
@@ -1,25 +1,21 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html"> - -<dom-module id="gr-lib-loader"> - <template> +export const htmlTemplate = html` <gr-js-api-interface id="jsAPI"></gr-js-api-interface> - </template> - <script src="gr-lib-loader.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html index a1d9c0f..1f726b3 100644 --- a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-lib-loader</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-lib-loader.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-lib-loader.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-lib-loader.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,117 +40,119 @@ </template> </test-fixture> -<script> - suite('gr-lib-loader tests', async () => { - await readyToTest(); - let sandbox; - let element; - let resolveLoad; - let loadStub; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-lib-loader.js'; +suite('gr-lib-loader tests', () => { + let sandbox; + let element; + let resolveLoad; + let loadStub; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + + loadStub = sandbox.stub(element, '_loadScript', () => + new Promise(resolve => resolveLoad = resolve) + ); + + // Assert preconditions: + assert.isFalse(element._hljsState.loading); + }); + + teardown(() => { + if (window.hljs) { + delete window.hljs; + } + sandbox.restore(); + + // Because the element state is a singleton, clean it up. + element._hljsState.configured = false; + element._hljsState.loading = false; + element._hljsState.callbacks = []; + }); + + test('only load once', done => { + sandbox.stub(element, '_getHLJSUrl').returns(''); + const firstCallHandler = sinon.stub(); + element.getHLJS().then(firstCallHandler); + + // It should now be in the loading state. + assert.isTrue(loadStub.called); + assert.isTrue(element._hljsState.loading); + assert.isFalse(firstCallHandler.called); + + const secondCallHandler = sinon.stub(); + element.getHLJS().then(secondCallHandler); + + // No change in state. + assert.isTrue(element._hljsState.loading); + assert.isFalse(firstCallHandler.called); + assert.isFalse(secondCallHandler.called); + + // Now load the library. + resolveLoad(); + flush(() => { + // The state should be loaded and both handlers called. + assert.isFalse(element._hljsState.loading); + assert.isTrue(firstCallHandler.called); + assert.isTrue(secondCallHandler.called); + done(); + }); + }); + + suite('preloaded', () => { + let hljsStub; setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - - loadStub = sandbox.stub(element, '_loadScript', () => - new Promise(resolve => resolveLoad = resolve) - ); - - // Assert preconditions: - assert.isFalse(element._hljsState.loading); + hljsStub = { + configure: sinon.stub(), + }; + window.hljs = hljsStub; }); teardown(() => { - if (window.hljs) { - delete window.hljs; - } - sandbox.restore(); - - // Because the element state is a singleton, clean it up. - element._hljsState.configured = false; - element._hljsState.loading = false; - element._hljsState.callbacks = []; + delete window.hljs; }); - test('only load once', done => { - sandbox.stub(element, '_getHLJSUrl').returns(''); + test('returns hljs', done => { const firstCallHandler = sinon.stub(); element.getHLJS().then(firstCallHandler); - - // It should now be in the loading state. - assert.isTrue(loadStub.called); - assert.isTrue(element._hljsState.loading); - assert.isFalse(firstCallHandler.called); - - const secondCallHandler = sinon.stub(); - element.getHLJS().then(secondCallHandler); - - // No change in state. - assert.isTrue(element._hljsState.loading); - assert.isFalse(firstCallHandler.called); - assert.isFalse(secondCallHandler.called); - - // Now load the library. - resolveLoad(); flush(() => { - // The state should be loaded and both handlers called. - assert.isFalse(element._hljsState.loading); assert.isTrue(firstCallHandler.called); - assert.isTrue(secondCallHandler.called); + assert.isTrue(firstCallHandler.calledWith(hljsStub)); done(); }); }); - suite('preloaded', () => { - let hljsStub; - - setup(() => { - hljsStub = { - configure: sinon.stub(), - }; - window.hljs = hljsStub; - }); - - teardown(() => { - delete window.hljs; - }); - - test('returns hljs', done => { - const firstCallHandler = sinon.stub(); - element.getHLJS().then(firstCallHandler); - flush(() => { - assert.isTrue(firstCallHandler.called); - assert.isTrue(firstCallHandler.calledWith(hljsStub)); - done(); - }); - }); - - test('configures hljs', done => { - element.getHLJS().then(() => { - assert.isTrue(window.hljs.configure.calledOnce); - done(); - }); - }); - }); - - suite('_getHLJSUrl', () => { - suite('checking _getLibRoot', () => { - let root; - - setup(() => { - sandbox.stub(element, '_getLibRoot', () => root); - }); - - test('with no root', () => { - assert.isNull(element._getHLJSUrl()); - }); - - test('with root', () => { - root = 'test-root.com/'; - assert.equal(element._getHLJSUrl(), - 'test-root.com/bower_components/highlightjs/highlight.min.js'); - }); + test('configures hljs', done => { + element.getHLJS().then(() => { + assert.isTrue(window.hljs.configure.calledOnce); + done(); }); }); }); + + suite('_getHLJSUrl', () => { + suite('checking _getLibRoot', () => { + let root; + + setup(() => { + sandbox.stub(element, '_getLibRoot', () => root); + }); + + test('with no root', () => { + assert.isNull(element._getHLJSUrl()); + }); + + test('with root', () => { + root = 'test-root.com/'; + assert.equal(element._getHLJSUrl(), + 'test-root.com/bower_components/highlightjs/highlight.min.js'); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js index ee032f6..d47dbbc 100644 --- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js +++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text.js
@@ -14,92 +14,99 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; + +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-limited-text_html.js'; + +/** + * The gr-limited-text element is for displaying text with a maximum length + * (in number of characters) to display. If the length of the text exceeds the + * configured limit, then an ellipsis indicates that the text was truncated + * and a tooltip containing the full text is enabled. + * + * @appliesMixin Gerrit.TooltipMixin + * @extends Polymer.Element + */ +class GrLimitedText extends mixinBehaviors( [ + Gerrit.TooltipBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-limited-text'; } + + static get properties() { + return { + /** The un-truncated text to display. */ + text: String, + + /** The maximum length for the text to display before truncating. */ + limit: { + type: Number, + value: null, + }, + + /** Boolean property used by Gerrit.TooltipBehavior. */ + hasTooltip: { + type: Boolean, + value: false, + }, + + /** + * Disable the tooltip. + * When set to true, will not show tooltip even text is over limit + */ + disableTooltip: { + type: Boolean, + value: false, + }, + + /** + * The maximum number of characters to display in the tooltop. + */ + tooltipLimit: { + type: Number, + value: 1024, + }, + }; + } + + static get observers() { + return [ + '_updateTitle(text, limit, tooltipLimit)', + ]; + } /** - * The gr-limited-text element is for displaying text with a maximum length - * (in number of characters) to display. If the length of the text exceeds the - * configured limit, then an ellipsis indicates that the text was truncated - * and a tooltip containing the full text is enabled. - * - * @appliesMixin Gerrit.TooltipMixin - * @extends Polymer.Element + * The text or limit have changed. Recompute whether a tooltip needs to be + * enabled. */ - class GrLimitedText extends Polymer.mixinBehaviors( [ - Gerrit.TooltipBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-limited-text'; } - - static get properties() { - return { - /** The un-truncated text to display. */ - text: String, - - /** The maximum length for the text to display before truncating. */ - limit: { - type: Number, - value: null, - }, - - /** Boolean property used by Gerrit.TooltipBehavior. */ - hasTooltip: { - type: Boolean, - value: false, - }, - - /** - * Disable the tooltip. - * When set to true, will not show tooltip even text is over limit - */ - disableTooltip: { - type: Boolean, - value: false, - }, - - /** - * The maximum number of characters to display in the tooltop. - */ - tooltipLimit: { - type: Number, - value: 1024, - }, - }; + _updateTitle(text, limit, tooltipLimit) { + // Polymer 2: check for undefined + if ([text, limit, tooltipLimit].some(arg => arg === undefined)) { + return; } - static get observers() { - return [ - '_updateTitle(text, limit, tooltipLimit)', - ]; - } - - /** - * The text or limit have changed. Recompute whether a tooltip needs to be - * enabled. - */ - _updateTitle(text, limit, tooltipLimit) { - // Polymer 2: check for undefined - if ([text, limit, tooltipLimit].some(arg => arg === undefined)) { - return; - } - - this.hasTooltip = !!limit && !!text && text.length > limit; - if (this.hasTooltip && !this.disableTooltip) { - this.setAttribute('title', text.substr(0, tooltipLimit)); - } else { - this.removeAttribute('title'); - } - } - - _computeDisplayText(text, limit) { - if (!!limit && !!text && text.length > limit) { - return text.substr(0, limit - 1) + '…'; - } - return text; + this.hasTooltip = !!limit && !!text && text.length > limit; + if (this.hasTooltip && !this.disableTooltip) { + this.setAttribute('title', text.substr(0, tooltipLimit)); + } else { + this.removeAttribute('title'); } } - customElements.define(GrLimitedText.is, GrLimitedText); -})(); + _computeDisplayText(text, limit) { + if (!!limit && !!text && text.length > limit) { + return text.substr(0, limit - 1) + '…'; + } + return text; + } +} + +customElements.define(GrLimitedText.is, GrLimitedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js index d00416b..c14f9f9 100644 --- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js +++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_html.js
@@ -1,24 +1,21 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> - -<dom-module id="gr-limited-text"> - <template>[[_computeDisplayText(text, limit)]]</template> - <script src="gr-limited-text.js"></script> -</dom-module> +export const htmlTemplate = html` +[[_computeDisplayText(text, limit)]] +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html index c34a348..8240cbf 100644 --- a/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-limited-text/gr-limited-text_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-limited-text</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-limited-text.html"> +<script type="module" src="./gr-limited-text.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-limited-text.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,72 +41,74 @@ </template> </test-fixture> -<script> - suite('gr-limited-text tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-limited-text.js'; +suite('gr-limited-text tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('_updateTitle', () => { - const updateSpy = sandbox.spy(element, '_updateTitle'); - element.text = 'abc 123'; - flushAsynchronousOperations(); - assert.isTrue(updateSpy.calledOnce); - assert.isNotOk(element.getAttribute('title')); - assert.isFalse(element.hasTooltip); - - element.limit = 10; - flushAsynchronousOperations(); - assert.isTrue(updateSpy.calledTwice); - assert.isNotOk(element.getAttribute('title')); - assert.isFalse(element.hasTooltip); - - element.limit = 3; - flushAsynchronousOperations(); - assert.isTrue(updateSpy.calledThrice); - assert.equal(element.getAttribute('title'), 'abc 123'); - assert.isTrue(element.hasTooltip); - - element.tooltipLimit = 3; - flushAsynchronousOperations(); - assert.equal(element.getAttribute('title'), 'abc'); - - element.tooltipLimit = 1024; - element.limit = 100; - flushAsynchronousOperations(); - assert.equal(updateSpy.callCount, 6); - assert.isNotOk(element.getAttribute('title')); - assert.isFalse(element.hasTooltip); - - element.limit = null; - flushAsynchronousOperations(); - assert.equal(updateSpy.callCount, 7); - assert.isNotOk(element.getAttribute('title')); - assert.isFalse(element.hasTooltip); - }); - - test('_computeDisplayText', () => { - assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar'); - assert.equal(element._computeDisplayText('foo bar', 4), 'foo…'); - assert.equal(element._computeDisplayText('foo bar', null), 'foo bar'); - }); - - test('when disable tooltip', () => { - sandbox.spy(element, '_updateTitle'); - element.text = 'abcdefghijklmn'; - element.disableTooltip = true; - element.limit = 10; - flushAsynchronousOperations(); - assert.equal(element.getAttribute('title'), null); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); }); + + teardown(() => { + sandbox.restore(); + }); + + test('_updateTitle', () => { + const updateSpy = sandbox.spy(element, '_updateTitle'); + element.text = 'abc 123'; + flushAsynchronousOperations(); + assert.isTrue(updateSpy.calledOnce); + assert.isNotOk(element.getAttribute('title')); + assert.isFalse(element.hasTooltip); + + element.limit = 10; + flushAsynchronousOperations(); + assert.isTrue(updateSpy.calledTwice); + assert.isNotOk(element.getAttribute('title')); + assert.isFalse(element.hasTooltip); + + element.limit = 3; + flushAsynchronousOperations(); + assert.isTrue(updateSpy.calledThrice); + assert.equal(element.getAttribute('title'), 'abc 123'); + assert.isTrue(element.hasTooltip); + + element.tooltipLimit = 3; + flushAsynchronousOperations(); + assert.equal(element.getAttribute('title'), 'abc'); + + element.tooltipLimit = 1024; + element.limit = 100; + flushAsynchronousOperations(); + assert.equal(updateSpy.callCount, 6); + assert.isNotOk(element.getAttribute('title')); + assert.isFalse(element.hasTooltip); + + element.limit = null; + flushAsynchronousOperations(); + assert.equal(updateSpy.callCount, 7); + assert.isNotOk(element.getAttribute('title')); + assert.isFalse(element.hasTooltip); + }); + + test('_computeDisplayText', () => { + assert.equal(element._computeDisplayText('foo bar', 100), 'foo bar'); + assert.equal(element._computeDisplayText('foo bar', 4), 'foo…'); + assert.equal(element._computeDisplayText('foo bar', null), 'foo bar'); + }); + + test('when disable tooltip', () => { + sandbox.spy(element, '_updateTitle'); + element.text = 'abcdefghijklmn'; + element.disableTooltip = true; + element.limit = 10; + flushAsynchronousOperations(); + assert.equal(element.getAttribute('title'), null); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js index ccab685..2957c9f 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip.js
@@ -14,52 +14,64 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrLinkedChip extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-linked-chip'; } +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import '../gr-button/gr-button.js'; +import '../gr-icons/gr-icons.js'; +import '../gr-limited-text/gr-limited-text.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-linked-chip_html.js'; - static get properties() { - return { - href: String, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - removable: { - type: Boolean, - value: false, - }, - text: String, - transparentBackground: { - type: Boolean, - value: false, - }, +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrLinkedChip extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** If provided, sets the maximum length of the content. */ - limit: Number, - }; - } + static get is() { return 'gr-linked-chip'; } - _getBackgroundClass(transparent) { - return transparent ? 'transparentBackground' : ''; - } + static get properties() { + return { + href: String, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + removable: { + type: Boolean, + value: false, + }, + text: String, + transparentBackground: { + type: Boolean, + value: false, + }, - _handleRemoveTap(e) { - e.preventDefault(); - this.fire('remove'); - } + /** If provided, sets the maximum length of the content. */ + limit: Number, + }; } - customElements.define(GrLinkedChip.is, GrLinkedChip); -})(); + _getBackgroundClass(transparent) { + return transparent ? 'transparentBackground' : ''; + } + + _handleRemoveTap(e) { + e.preventDefault(); + this.fire('remove'); + } +} + +customElements.define(GrLinkedChip.is, GrLinkedChip);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js index 844b8be..c028d02 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_html.js
@@ -1,30 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<link rel="import" href="../gr-button/gr-button.html"> -<link rel="import" href="../gr-icons/gr-icons.html"> -<link rel="import" href="../gr-limited-text/gr-limited-text.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-linked-chip"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -78,22 +70,12 @@ width: 1.2rem; } </style> - <div class$="container [[_getBackgroundClass(transparentBackground)]]"> - <a href$="[[href]]"> - <gr-limited-text - limit="[[limit]]" - text="[[text]]"></gr-limited-text> + <div class\$="container [[_getBackgroundClass(transparentBackground)]]"> + <a href\$="[[href]]"> + <gr-limited-text limit="[[limit]]" text="[[text]]"></gr-limited-text> </a> - <gr-button - id="remove" - link - hidden$="[[!removable]]" - hidden - class$="remove [[_getBackgroundClass(transparentBackground)]]" - on-click="_handleRemoveTap"> + <gr-button id="remove" link="" hidden\$="[[!removable]]" hidden="" class\$="remove [[_getBackgroundClass(transparentBackground)]]" on-click="_handleRemoveTap"> <iron-icon icon="gr-icons:close"></iron-icon> </gr-button> </div> - </template> - <script src="gr-linked-chip.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html index 2bc7cfa..af8d217 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-chip/gr-linked-chip_test.html
@@ -19,12 +19,12 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-linked-chip</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> <!-- Can't use absolute path below for mock-interaction.js. Web component tester(wct) has a built-in http server and it serves "/components" directory (which is actually /node_modules directory). Also, wct patches some files to load modules from /components. @@ -33,9 +33,14 @@ --> <script src="../../../node_modules/iron-test-helpers/mock-interactions.js"></script> -<link rel="import" href="gr-linked-chip.html"> +<script type="module" src="./gr-linked-chip.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-linked-chip.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -43,27 +48,29 @@ </template> </test-fixture> -<script> - suite('gr-linked-chip tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-linked-chip.js'; +suite('gr-linked-chip tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('remove fired', () => { - const spy = sandbox.spy(); - element.addEventListener('remove', spy); - flushAsynchronousOperations(); - MockInteractions.tap(element.$.remove); - assert.isTrue(spy.called); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('remove fired', () => { + const spy = sandbox.spy(); + element.addEventListener('remove', spy); + flushAsynchronousOperations(); + MockInteractions.tap(element.$.remove); + assert.isTrue(spy.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js index f970734..07ce424 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text.js
@@ -1,6 +1,6 @@ /** * @license - * Copyright (C) 2016 The Android Open Source Project + * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,110 +14,121 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrLinkedText extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-linked-text'; } +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-navigation/gr-navigation.js'; +import './link-text-parser.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import 'ba-linkify/ba-linkify.js'; +import {htmlTemplate} from './gr-linked-text_html.js'; - static get properties() { - return { - removeZeroWidthSpace: Boolean, - content: { - type: String, - observer: '_contentChanged', - }, - pre: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - disabled: { - type: Boolean, - value: false, - reflectToAttribute: true, - }, - config: Object, - }; - } +/** @extends Polymer.Element */ +class GrLinkedText extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - static get observers() { - return [ - '_contentOrConfigChanged(content, config)', - ]; - } + static get is() { return 'gr-linked-text'; } - _contentChanged(content) { - // In the case where the config may not be set (perhaps due to the - // request for it still being in flight), set the content anyway to - // prevent waiting on the config to display the text. - if (this.config != null) { return; } - this.$.output.textContent = content; - } - - /** - * Because either the source text or the linkification config has changed, - * the content should be re-parsed. - * - * @param {string|null|undefined} content The raw, un-linkified source - * string to parse. - * @param {Object|null|undefined} config The server config specifying - * commentLink patterns - */ - _contentOrConfigChanged(content, config) { - if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return; - config = Gerrit.Nav.mapCommentlinks(config); - const output = Polymer.dom(this.$.output); - output.textContent = ''; - const parser = new GrLinkTextParser(config, - this._handleParseResult.bind(this), this.removeZeroWidthSpace); - parser.parse(content); - - // Ensure that external links originating from HTML commentlink configs - // open in a new tab. @see Issue 5567 - // Ensure links to the same host originating from commentlink configs - // open in the same tab. When target is not set - default is _self - // @see Issue 4616 - output.querySelectorAll('a').forEach(anchor => { - if (anchor.hostname === window.location.hostname) { - anchor.removeAttribute('target'); - } else { - anchor.setAttribute('target', '_blank'); - } - anchor.setAttribute('rel', 'noopener'); - }); - } - - /** - * This method is called when the GrLikTextParser emits a partial result - * (used as the "callback" parameter). It will be called in either of two - * ways: - * - To create a link: when called with `text` and `href` arguments, a link - * element should be created and attached to the resulting DOM. - * - To attach an arbitrary fragment: when called with only the `fragment` - * argument, the fragment should be attached to the resulting DOM as is. - * - * @param {string|null} text - * @param {string|null} href - * @param {DocumentFragment|undefined} fragment - */ - _handleParseResult(text, href, fragment) { - const output = Polymer.dom(this.$.output); - if (href) { - const a = document.createElement('a'); - a.href = href; - a.textContent = text; - a.target = '_blank'; - a.rel = 'noopener'; - output.appendChild(a); - } else if (fragment) { - output.appendChild(fragment); - } - } + static get properties() { + return { + removeZeroWidthSpace: Boolean, + content: { + type: String, + observer: '_contentChanged', + }, + pre: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + disabled: { + type: Boolean, + value: false, + reflectToAttribute: true, + }, + config: Object, + }; } - customElements.define(GrLinkedText.is, GrLinkedText); -})(); + static get observers() { + return [ + '_contentOrConfigChanged(content, config)', + ]; + } + + _contentChanged(content) { + // In the case where the config may not be set (perhaps due to the + // request for it still being in flight), set the content anyway to + // prevent waiting on the config to display the text. + if (this.config != null) { return; } + this.$.output.textContent = content; + } + + /** + * Because either the source text or the linkification config has changed, + * the content should be re-parsed. + * + * @param {string|null|undefined} content The raw, un-linkified source + * string to parse. + * @param {Object|null|undefined} config The server config specifying + * commentLink patterns + */ + _contentOrConfigChanged(content, config) { + if (!Gerrit.Nav || !Gerrit.Nav.mapCommentlinks) return; + config = Gerrit.Nav.mapCommentlinks(config); + const output = dom(this.$.output); + output.textContent = ''; + const parser = new GrLinkTextParser(config, + this._handleParseResult.bind(this), this.removeZeroWidthSpace); + parser.parse(content); + + // Ensure that external links originating from HTML commentlink configs + // open in a new tab. @see Issue 5567 + // Ensure links to the same host originating from commentlink configs + // open in the same tab. When target is not set - default is _self + // @see Issue 4616 + output.querySelectorAll('a').forEach(anchor => { + if (anchor.hostname === window.location.hostname) { + anchor.removeAttribute('target'); + } else { + anchor.setAttribute('target', '_blank'); + } + anchor.setAttribute('rel', 'noopener'); + }); + } + + /** + * This method is called when the GrLikTextParser emits a partial result + * (used as the "callback" parameter). It will be called in either of two + * ways: + * - To create a link: when called with `text` and `href` arguments, a link + * element should be created and attached to the resulting DOM. + * - To attach an arbitrary fragment: when called with only the `fragment` + * argument, the fragment should be attached to the resulting DOM as is. + * + * @param {string|null} text + * @param {string|null} href + * @param {DocumentFragment|undefined} fragment + */ + _handleParseResult(text, href, fragment) { + const output = dom(this.$.output); + if (href) { + const a = document.createElement('a'); + a.href = href; + a.textContent = text; + a.target = '_blank'; + a.rel = 'noopener'; + output.appendChild(a); + } else if (fragment) { + output.appendChild(fragment); + } + } +} + +customElements.define(GrLinkedText.is, GrLinkedText);
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js index 61facc0..43d7144 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2015 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-navigation/gr-navigation.html"> - -<script src="/bower_components/ba-linkify/ba-linkify.js"></script> -<script src="link-text-parser.js"></script> -<dom-module id="gr-linked-text"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -39,6 +32,4 @@ } </style> <span id="output"></span> - </template> - <script src="gr-linked-text.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html index 9e373b7..b16acf4 100644 --- a/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html +++ b/polygerrit-ui/app/elements/shared/gr-linked-text/gr-linked-text_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-linked-text</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-linked-text.html"> +<script type="module" src="./gr-linked-text.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-linked-text.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -39,340 +45,344 @@ </template> </test-fixture> -<script> - suite('gr-linked-text tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-linked-text.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-linked-text tests', () => { + let element; + let sandbox; - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x); - element.config = { - ph: { - match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)', - link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2', - }, - prefixsameinlinkandpattern: { - match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)', - link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2', - }, - changeid: { - match: '(I[0-9a-f]{8,40})', - link: '#/q/$1', - }, - changeid2: { - match: 'Change-Id: +(I[0-9a-f]{8,40})', - link: '#/q/$1', - }, - googlesearch: { - match: 'google:(.+)', - link: 'https://bing.com/search?q=$1', // html should supercede link. - html: '<a href="https://google.com/search?q=$1">$1</a>', - }, - hashedhtml: { - match: 'hash:(.+)', - html: '<a href="#/awesomesauce">$1</a>', - }, - baseurl: { - match: 'test (.+)', - html: '<a href="/r/awesomesauce">$1</a>', - }, - anotatstartwithbaseurl: { - match: 'a test (.+)', - html: '[Lookup: <a href="/r/awesomesauce">$1</a>]', - }, - disabledconfig: { - match: 'foo:(.+)', - link: 'https://google.com/search?q=$1', - enabled: false, - }, - }; - }); - - teardown(() => { - sandbox.restore(); - }); - - test('URL pattern was parsed and linked.', () => { - // Regular inline link. - const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; - element.content = url; - const linkEl = element.$.output.childNodes[0]; - assert.equal(linkEl.target, '_blank'); - assert.equal(linkEl.rel, 'noopener'); - assert.equal(linkEl.href, url); - assert.equal(linkEl.textContent, url); - }); - - test('Bug pattern was parsed and linked', () => { - // "Issue/Bug" pattern. - element.content = 'Issue 3650'; - - let linkEl = element.$.output.childNodes[0]; - const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; - assert.equal(linkEl.target, '_blank'); - assert.equal(linkEl.href, url); - assert.equal(linkEl.textContent, 'Issue 3650'); - - element.content = 'Bug 3650'; - linkEl = element.$.output.childNodes[0]; - assert.equal(linkEl.target, '_blank'); - assert.equal(linkEl.rel, 'noopener'); - assert.equal(linkEl.href, url); - assert.equal(linkEl.textContent, 'Bug 3650'); - }); - - test('Pattern with same prefix as link was correctly parsed', () => { - // Pattern starts with the same prefix (`http`) as the url. - element.content = 'httpexample 3650'; - - assert.equal(element.$.output.childNodes.length, 1); - const linkEl = element.$.output.childNodes[0]; - const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; - assert.equal(linkEl.target, '_blank'); - assert.equal(linkEl.href, url); - assert.equal(linkEl.textContent, 'httpexample 3650'); - }); - - test('Change-Id pattern was parsed and linked', () => { - // "Change-Id:" pattern. - const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; - const prefix = 'Change-Id: '; - element.content = prefix + changeID; - - const textNode = element.$.output.childNodes[0]; - const linkEl = element.$.output.childNodes[1]; - assert.equal(textNode.textContent, prefix); - const url = '/q/' + changeID; - assert.isFalse(linkEl.hasAttribute('target')); - // Since url is a path, the host is added automatically. - assert.isTrue(linkEl.href.endsWith(url)); - assert.equal(linkEl.textContent, changeID); - }); - - test('Change-Id pattern was parsed and linked with base url', () => { - window.CANONICAL_PATH = '/r'; - - // "Change-Id:" pattern. - const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; - const prefix = 'Change-Id: '; - element.content = prefix + changeID; - - const textNode = element.$.output.childNodes[0]; - const linkEl = element.$.output.childNodes[1]; - assert.equal(textNode.textContent, prefix); - const url = '/r/q/' + changeID; - assert.isFalse(linkEl.hasAttribute('target')); - // Since url is a path, the host is added automatically. - assert.isTrue(linkEl.href.endsWith(url)); - assert.equal(linkEl.textContent, changeID); - }); - - test('Multiple matches', () => { - element.content = 'Issue 3650\nIssue 3450'; - const linkEl1 = element.$.output.childNodes[0]; - const linkEl2 = element.$.output.childNodes[2]; - - assert.equal(linkEl1.target, '_blank'); - assert.equal(linkEl1.href, - 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'); - assert.equal(linkEl1.textContent, 'Issue 3650'); - - assert.equal(linkEl2.target, '_blank'); - assert.equal(linkEl2.href, - 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450'); - assert.equal(linkEl2.textContent, 'Issue 3450'); - }); - - test('Change-Id pattern parsed before bug pattern', () => { - // "Change-Id:" pattern. - const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; - const prefix = 'Change-Id: '; - - // "Issue/Bug" pattern. - const bug = 'Issue 3650'; - - const changeUrl = '/q/' + changeID; - const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; - - element.content = prefix + changeID + bug; - - const textNode = element.$.output.childNodes[0]; - const changeLinkEl = element.$.output.childNodes[1]; - const bugLinkEl = element.$.output.childNodes[2]; - - assert.equal(textNode.textContent, prefix); - - assert.isFalse(changeLinkEl.hasAttribute('target')); - assert.isTrue(changeLinkEl.href.endsWith(changeUrl)); - assert.equal(changeLinkEl.textContent, changeID); - - assert.equal(bugLinkEl.target, '_blank'); - assert.equal(bugLinkEl.href, bugUrl); - assert.equal(bugLinkEl.textContent, 'Issue 3650'); - }); - - test('html field in link config', () => { - element.content = 'google:do a barrel roll'; - const linkEl = element.$.output.childNodes[0]; - assert.equal(linkEl.getAttribute('href'), - 'https://google.com/search?q=do a barrel roll'); - assert.equal(linkEl.textContent, 'do a barrel roll'); - }); - - test('removing hash from links', () => { - element.content = 'hash:foo'; - const linkEl = element.$.output.childNodes[0]; - assert.isTrue(linkEl.href.endsWith('/awesomesauce')); - assert.equal(linkEl.textContent, 'foo'); - }); - - test('html with base url', () => { - window.CANONICAL_PATH = '/r'; - - element.content = 'test foo'; - const linkEl = element.$.output.childNodes[0]; - assert.isTrue(linkEl.href.endsWith('/r/awesomesauce')); - assert.equal(linkEl.textContent, 'foo'); - }); - - test('a is not at start', () => { - window.CANONICAL_PATH = '/r'; - - element.content = 'a test foo'; - const linkEl = element.$.output.childNodes[1]; - assert.isTrue(linkEl.href.endsWith('/r/awesomesauce')); - assert.equal(linkEl.textContent, 'foo'); - }); - - test('hash html with base url', () => { - window.CANONICAL_PATH = '/r'; - - element.content = 'hash:foo'; - const linkEl = element.$.output.childNodes[0]; - assert.isTrue(linkEl.href.endsWith('/r/awesomesauce')); - assert.equal(linkEl.textContent, 'foo'); - }); - - test('disabled config', () => { - element.content = 'foo:baz'; - assert.equal(element.$.output.innerHTML, 'foo:baz'); - }); - - test('R=email labels link correctly', () => { - element.removeZeroWidthSpace = true; - element.content = 'R=\u200Btest@google.com'; - assert.equal(element.$.output.textContent, 'R=test@google.com'); - assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1); - }); - - test('CC=email labels link correctly', () => { - element.removeZeroWidthSpace = true; - element.content = 'CC=\u200Btest@google.com'; - assert.equal(element.$.output.textContent, 'CC=test@google.com'); - assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1); - }); - - test('only {http,https,mailto} protocols are linkified', () => { - element.content = 'xx mailto:test@google.com yy'; - let links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com'); - assert.equal(links[0].innerHTML, 'mailto:test@google.com'); - - element.content = 'xx http://google.com yy'; - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'http://google.com'); - assert.equal(links[0].innerHTML, 'http://google.com'); - - element.content = 'xx https://google.com yy'; - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'https://google.com'); - assert.equal(links[0].innerHTML, 'https://google.com'); - - element.content = 'xx ssh://google.com yy'; - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 0); - - element.content = 'xx ftp://google.com yy'; - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 0); - }); - - test('links without leading whitespace are linkified', () => { - element.content = 'xx abcmailto:test@google.com yy'; - assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc'); - let links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com'); - assert.equal(links[0].innerHTML, 'mailto:test@google.com'); - - element.content = 'xx defhttp://google.com yy'; - assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def'); - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'http://google.com'); - assert.equal(links[0].innerHTML, 'http://google.com'); - - element.content = 'xx qwehttps://google.com yy'; - assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe'); - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'https://google.com'); - assert.equal(links[0].innerHTML, 'https://google.com'); - - // Non-latin character - element.content = 'xx абвhttps://google.com yy'; - assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв'); - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 1); - assert.equal(links[0].getAttribute('href'), 'https://google.com'); - assert.equal(links[0].innerHTML, 'https://google.com'); - - element.content = 'xx ssh://google.com yy'; - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 0); - - element.content = 'xx ftp://google.com yy'; - links = element.$.output.querySelectorAll('a'); - assert.equal(links.length, 0); - }); - - test('overlapping links', () => { - element.config = { - b1: { - match: '(B:\\s*)(\\d+)', - html: '$1<a href="ftp://foo/$2">$2</a>', - }, - b2: { - match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)', - html: '$1<a href="ftp://foo/$2">$2</a>', - }, - }; - element.content = '- B: 123, 45'; - const links = Polymer.dom(element.root).querySelectorAll('a'); - - assert.equal(links.length, 2); - assert.equal(element.shadowRoot - .querySelector('span').textContent, '- B: 123, 45'); - - assert.equal(links[0].href, 'ftp://foo/123'); - assert.equal(links[0].textContent, '123'); - - assert.equal(links[1].href, 'ftp://foo/45'); - assert.equal(links[1].textContent, '45'); - }); - - test('_contentOrConfigChanged called with config', () => { - const contentStub = sandbox.stub(element, '_contentChanged'); - const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); - element.content = 'some text'; - assert.isTrue(contentStub.called); - assert.isTrue(contentConfigStub.called); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + sandbox.stub(Gerrit.Nav, 'mapCommentlinks', x => x); + element.config = { + ph: { + match: '([Bb]ug|[Ii]ssue)\\s*#?(\\d+)', + link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2', + }, + prefixsameinlinkandpattern: { + match: '([Hh][Tt][Tt][Pp]example)\\s*#?(\\d+)', + link: 'https://bugs.chromium.org/p/gerrit/issues/detail?id=$2', + }, + changeid: { + match: '(I[0-9a-f]{8,40})', + link: '#/q/$1', + }, + changeid2: { + match: 'Change-Id: +(I[0-9a-f]{8,40})', + link: '#/q/$1', + }, + googlesearch: { + match: 'google:(.+)', + link: 'https://bing.com/search?q=$1', // html should supercede link. + html: '<a href="https://google.com/search?q=$1">$1</a>', + }, + hashedhtml: { + match: 'hash:(.+)', + html: '<a href="#/awesomesauce">$1</a>', + }, + baseurl: { + match: 'test (.+)', + html: '<a href="/r/awesomesauce">$1</a>', + }, + anotatstartwithbaseurl: { + match: 'a test (.+)', + html: '[Lookup: <a href="/r/awesomesauce">$1</a>]', + }, + disabledconfig: { + match: 'foo:(.+)', + link: 'https://google.com/search?q=$1', + enabled: false, + }, + }; }); + + teardown(() => { + sandbox.restore(); + }); + + test('URL pattern was parsed and linked.', () => { + // Regular inline link. + const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; + element.content = url; + const linkEl = element.$.output.childNodes[0]; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.rel, 'noopener'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, url); + }); + + test('Bug pattern was parsed and linked', () => { + // "Issue/Bug" pattern. + element.content = 'Issue 3650'; + + let linkEl = element.$.output.childNodes[0]; + const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, 'Issue 3650'); + + element.content = 'Bug 3650'; + linkEl = element.$.output.childNodes[0]; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.rel, 'noopener'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, 'Bug 3650'); + }); + + test('Pattern with same prefix as link was correctly parsed', () => { + // Pattern starts with the same prefix (`http`) as the url. + element.content = 'httpexample 3650'; + + assert.equal(element.$.output.childNodes.length, 1); + const linkEl = element.$.output.childNodes[0]; + const url = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; + assert.equal(linkEl.target, '_blank'); + assert.equal(linkEl.href, url); + assert.equal(linkEl.textContent, 'httpexample 3650'); + }); + + test('Change-Id pattern was parsed and linked', () => { + // "Change-Id:" pattern. + const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; + const prefix = 'Change-Id: '; + element.content = prefix + changeID; + + const textNode = element.$.output.childNodes[0]; + const linkEl = element.$.output.childNodes[1]; + assert.equal(textNode.textContent, prefix); + const url = '/q/' + changeID; + assert.isFalse(linkEl.hasAttribute('target')); + // Since url is a path, the host is added automatically. + assert.isTrue(linkEl.href.endsWith(url)); + assert.equal(linkEl.textContent, changeID); + }); + + test('Change-Id pattern was parsed and linked with base url', () => { + window.CANONICAL_PATH = '/r'; + + // "Change-Id:" pattern. + const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; + const prefix = 'Change-Id: '; + element.content = prefix + changeID; + + const textNode = element.$.output.childNodes[0]; + const linkEl = element.$.output.childNodes[1]; + assert.equal(textNode.textContent, prefix); + const url = '/r/q/' + changeID; + assert.isFalse(linkEl.hasAttribute('target')); + // Since url is a path, the host is added automatically. + assert.isTrue(linkEl.href.endsWith(url)); + assert.equal(linkEl.textContent, changeID); + }); + + test('Multiple matches', () => { + element.content = 'Issue 3650\nIssue 3450'; + const linkEl1 = element.$.output.childNodes[0]; + const linkEl2 = element.$.output.childNodes[2]; + + assert.equal(linkEl1.target, '_blank'); + assert.equal(linkEl1.href, + 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'); + assert.equal(linkEl1.textContent, 'Issue 3650'); + + assert.equal(linkEl2.target, '_blank'); + assert.equal(linkEl2.href, + 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3450'); + assert.equal(linkEl2.textContent, 'Issue 3450'); + }); + + test('Change-Id pattern parsed before bug pattern', () => { + // "Change-Id:" pattern. + const changeID = 'I11d6a37f5e9b5df0486f6c922d8836dfa780e03e'; + const prefix = 'Change-Id: '; + + // "Issue/Bug" pattern. + const bug = 'Issue 3650'; + + const changeUrl = '/q/' + changeID; + const bugUrl = 'https://bugs.chromium.org/p/gerrit/issues/detail?id=3650'; + + element.content = prefix + changeID + bug; + + const textNode = element.$.output.childNodes[0]; + const changeLinkEl = element.$.output.childNodes[1]; + const bugLinkEl = element.$.output.childNodes[2]; + + assert.equal(textNode.textContent, prefix); + + assert.isFalse(changeLinkEl.hasAttribute('target')); + assert.isTrue(changeLinkEl.href.endsWith(changeUrl)); + assert.equal(changeLinkEl.textContent, changeID); + + assert.equal(bugLinkEl.target, '_blank'); + assert.equal(bugLinkEl.href, bugUrl); + assert.equal(bugLinkEl.textContent, 'Issue 3650'); + }); + + test('html field in link config', () => { + element.content = 'google:do a barrel roll'; + const linkEl = element.$.output.childNodes[0]; + assert.equal(linkEl.getAttribute('href'), + 'https://google.com/search?q=do a barrel roll'); + assert.equal(linkEl.textContent, 'do a barrel roll'); + }); + + test('removing hash from links', () => { + element.content = 'hash:foo'; + const linkEl = element.$.output.childNodes[0]; + assert.isTrue(linkEl.href.endsWith('/awesomesauce')); + assert.equal(linkEl.textContent, 'foo'); + }); + + test('html with base url', () => { + window.CANONICAL_PATH = '/r'; + + element.content = 'test foo'; + const linkEl = element.$.output.childNodes[0]; + assert.isTrue(linkEl.href.endsWith('/r/awesomesauce')); + assert.equal(linkEl.textContent, 'foo'); + }); + + test('a is not at start', () => { + window.CANONICAL_PATH = '/r'; + + element.content = 'a test foo'; + const linkEl = element.$.output.childNodes[1]; + assert.isTrue(linkEl.href.endsWith('/r/awesomesauce')); + assert.equal(linkEl.textContent, 'foo'); + }); + + test('hash html with base url', () => { + window.CANONICAL_PATH = '/r'; + + element.content = 'hash:foo'; + const linkEl = element.$.output.childNodes[0]; + assert.isTrue(linkEl.href.endsWith('/r/awesomesauce')); + assert.equal(linkEl.textContent, 'foo'); + }); + + test('disabled config', () => { + element.content = 'foo:baz'; + assert.equal(element.$.output.innerHTML, 'foo:baz'); + }); + + test('R=email labels link correctly', () => { + element.removeZeroWidthSpace = true; + element.content = 'R=\u200Btest@google.com'; + assert.equal(element.$.output.textContent, 'R=test@google.com'); + assert.equal(element.$.output.innerHTML.match(/(R=<a)/g).length, 1); + }); + + test('CC=email labels link correctly', () => { + element.removeZeroWidthSpace = true; + element.content = 'CC=\u200Btest@google.com'; + assert.equal(element.$.output.textContent, 'CC=test@google.com'); + assert.equal(element.$.output.innerHTML.match(/(CC=<a)/g).length, 1); + }); + + test('only {http,https,mailto} protocols are linkified', () => { + element.content = 'xx mailto:test@google.com yy'; + let links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com'); + assert.equal(links[0].innerHTML, 'mailto:test@google.com'); + + element.content = 'xx http://google.com yy'; + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'http://google.com'); + assert.equal(links[0].innerHTML, 'http://google.com'); + + element.content = 'xx https://google.com yy'; + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'https://google.com'); + assert.equal(links[0].innerHTML, 'https://google.com'); + + element.content = 'xx ssh://google.com yy'; + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 0); + + element.content = 'xx ftp://google.com yy'; + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 0); + }); + + test('links without leading whitespace are linkified', () => { + element.content = 'xx abcmailto:test@google.com yy'; + assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx abc'); + let links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'mailto:test@google.com'); + assert.equal(links[0].innerHTML, 'mailto:test@google.com'); + + element.content = 'xx defhttp://google.com yy'; + assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx def'); + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'http://google.com'); + assert.equal(links[0].innerHTML, 'http://google.com'); + + element.content = 'xx qwehttps://google.com yy'; + assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx qwe'); + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'https://google.com'); + assert.equal(links[0].innerHTML, 'https://google.com'); + + // Non-latin character + element.content = 'xx абвhttps://google.com yy'; + assert.equal(element.$.output.innerHTML.substr(0, 6), 'xx абв'); + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 1); + assert.equal(links[0].getAttribute('href'), 'https://google.com'); + assert.equal(links[0].innerHTML, 'https://google.com'); + + element.content = 'xx ssh://google.com yy'; + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 0); + + element.content = 'xx ftp://google.com yy'; + links = element.$.output.querySelectorAll('a'); + assert.equal(links.length, 0); + }); + + test('overlapping links', () => { + element.config = { + b1: { + match: '(B:\\s*)(\\d+)', + html: '$1<a href="ftp://foo/$2">$2</a>', + }, + b2: { + match: '(B:\\s*\\d+\\s*,\\s*)(\\d+)', + html: '$1<a href="ftp://foo/$2">$2</a>', + }, + }; + element.content = '- B: 123, 45'; + const links = dom(element.root).querySelectorAll('a'); + + assert.equal(links.length, 2); + assert.equal(element.shadowRoot + .querySelector('span').textContent, '- B: 123, 45'); + + assert.equal(links[0].href, 'ftp://foo/123'); + assert.equal(links[0].textContent, '123'); + + assert.equal(links[1].href, 'ftp://foo/45'); + assert.equal(links[1].textContent, '45'); + }); + + test('_contentOrConfigChanged called with config', () => { + const contentStub = sandbox.stub(element, '_contentChanged'); + const contentConfigStub = sandbox.stub(element, '_contentOrConfigChanged'); + element.content = 'some text'; + assert.isTrue(contentStub.called); + assert.isTrue(contentConfigStub.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js index 8913cd8..d52a912 100644 --- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view.js
@@ -14,106 +14,119 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const REQUEST_DEBOUNCE_INTERVAL_MS = 200; +import '@polymer/iron-input/iron-input.js'; +import '@polymer/iron-icon/iron-icon.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../../../styles/shared-styles.js'; +import '../gr-button/gr-button.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-list-view_html.js'; - /** - * @appliesMixin Gerrit.BaseUrlMixin - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrListView extends Polymer.mixinBehaviors( [ - Gerrit.BaseUrlBehavior, - Gerrit.FireBehavior, - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-list-view'; } +const REQUEST_DEBOUNCE_INTERVAL_MS = 200; - static get properties() { - return { - createNew: Boolean, - items: Array, - itemsPerPage: Number, - filter: { - type: String, - observer: '_filterChanged', - }, - offset: Number, - loading: Boolean, - path: String, - }; - } +/** + * @appliesMixin Gerrit.BaseUrlMixin + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrListView extends mixinBehaviors( [ + Gerrit.BaseUrlBehavior, + Gerrit.FireBehavior, + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } - /** @override */ - detached() { - super.detached(); - this.cancelDebouncer('reload'); - } + static get is() { return 'gr-list-view'; } - _filterChanged(newFilter, oldFilter) { - if (!newFilter && !oldFilter) { - return; - } - - this._debounceReload(newFilter); - } - - _debounceReload(filter) { - this.debounce('reload', () => { - if (filter) { - return page.show(`${this.path}/q/filter:` + - this.encodeURL(filter, false)); - } - page.show(this.path); - }, REQUEST_DEBOUNCE_INTERVAL_MS); - } - - _createNewItem() { - this.fire('create-clicked'); - } - - _computeNavLink(offset, direction, itemsPerPage, filter, path) { - // Offset could be a string when passed from the router. - offset = +(offset || 0); - const newOffset = Math.max(0, offset + (itemsPerPage * direction)); - let href = this.getBaseUrl() + path; - if (filter) { - href += '/q/filter:' + this.encodeURL(filter, false); - } - if (newOffset > 0) { - href += ',' + newOffset; - } - return href; - } - - _computeCreateClass(createNew) { - return createNew ? 'show' : ''; - } - - _hidePrevArrow(loading, offset) { - return loading || offset === 0; - } - - _hideNextArrow(loading, items) { - if (loading || !items || !items.length) { - return true; - } - const lastPage = items.length < this.itemsPerPage + 1; - return lastPage; - } - - // TODO: fix offset (including itemsPerPage) - // to either support a decimal or make it go to the nearest - // whole number (e.g 3). - _computePage(offset, itemsPerPage) { - return offset / itemsPerPage + 1; - } + static get properties() { + return { + createNew: Boolean, + items: Array, + itemsPerPage: Number, + filter: { + type: String, + observer: '_filterChanged', + }, + offset: Number, + loading: Boolean, + path: String, + }; } - customElements.define(GrListView.is, GrListView); -})(); + /** @override */ + detached() { + super.detached(); + this.cancelDebouncer('reload'); + } + + _filterChanged(newFilter, oldFilter) { + if (!newFilter && !oldFilter) { + return; + } + + this._debounceReload(newFilter); + } + + _debounceReload(filter) { + this.debounce('reload', () => { + if (filter) { + return page.show(`${this.path}/q/filter:` + + this.encodeURL(filter, false)); + } + page.show(this.path); + }, REQUEST_DEBOUNCE_INTERVAL_MS); + } + + _createNewItem() { + this.fire('create-clicked'); + } + + _computeNavLink(offset, direction, itemsPerPage, filter, path) { + // Offset could be a string when passed from the router. + offset = +(offset || 0); + const newOffset = Math.max(0, offset + (itemsPerPage * direction)); + let href = this.getBaseUrl() + path; + if (filter) { + href += '/q/filter:' + this.encodeURL(filter, false); + } + if (newOffset > 0) { + href += ',' + newOffset; + } + return href; + } + + _computeCreateClass(createNew) { + return createNew ? 'show' : ''; + } + + _hidePrevArrow(loading, offset) { + return loading || offset === 0; + } + + _hideNextArrow(loading, items) { + if (loading || !items || !items.length) { + return true; + } + const lastPage = items.length < this.itemsPerPage + 1; + return lastPage; + } + + // TODO: fix offset (including itemsPerPage) + // to either support a decimal or make it go to the nearest + // whole number (e.g 3). + _computePage(offset, itemsPerPage) { + return offset / itemsPerPage + 1; + } +} + +customElements.define(GrListView.is, GrListView);
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js index 3d41a7c..0d33dd0 100644 --- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_html.js
@@ -1,31 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-input/iron-input.html"> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> - -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-button/gr-button.html"> - -<dom-module id="gr-list-view"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> #filter { max-width: 25em; @@ -70,19 +61,12 @@ <div id="topContainer"> <div class="filterContainer"> <label>Filter:</label> - <iron-input - type="text" - bind-value="{{filter}}"> - <input - is="iron-input" - type="text" - id="filter" - bind-value="{{filter}}"> + <iron-input type="text" bind-value="{{filter}}"> + <input is="iron-input" type="text" id="filter" bind-value="{{filter}}"> </iron-input> </div> - <div id="createNewContainer" - class$="[[_computeCreateClass(createNew)]]"> - <gr-button primary link id="createNew" on-click="_createNewItem"> + <div id="createNewContainer" class\$="[[_computeCreateClass(createNew)]]"> + <gr-button primary="" link="" id="createNew" on-click="_createNewItem"> Create New </gr-button> </div> @@ -90,17 +74,11 @@ <slot></slot> <nav> Page [[_computePage(offset, itemsPerPage)]] - <a id="prevArrow" - href$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]" - hidden$="[[_hidePrevArrow(loading, offset)]]" hidden> + <a id="prevArrow" href\$="[[_computeNavLink(offset, -1, itemsPerPage, filter, path)]]" hidden\$="[[_hidePrevArrow(loading, offset)]]" hidden=""> <iron-icon icon="gr-icons:chevron-left"></iron-icon> </a> - <a id="nextArrow" - href$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]" - hidden$="[[_hideNextArrow(loading, items)]]" hidden> + <a id="nextArrow" href\$="[[_computeNavLink(offset, 1, itemsPerPage, filter, path)]]" hidden\$="[[_hideNextArrow(loading, items)]]" hidden=""> <iron-icon icon="gr-icons:chevron-right"></iron-icon> </a> </nav> - </template> - <script src="gr-list-view.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html index 70605a1..cd45650 100644 --- a/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html +++ b/polygerrit-ui/app/elements/shared/gr-list-view/gr-list-view_test.html
@@ -19,16 +19,21 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-list-view</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-list-view.html"> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-list-view.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-list-view.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -36,133 +41,135 @@ </template> </test-fixture> -<script> - suite('gr-list-view tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-list-view.js'; +suite('gr-list-view tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('_computeNavLink', () => { + const offset = 25; + const projectsPerPage = 25; + let filter = 'test'; + const path = '/admin/projects'; + + sandbox.stub(element, 'getBaseUrl', () => ''); + + assert.equal( + element._computeNavLink(offset, 1, projectsPerPage, filter, path), + '/admin/projects/q/filter:test,50'); + + assert.equal( + element._computeNavLink(offset, -1, projectsPerPage, filter, path), + '/admin/projects/q/filter:test'); + + assert.equal( + element._computeNavLink(offset, 1, projectsPerPage, null, path), + '/admin/projects,50'); + + assert.equal( + element._computeNavLink(offset, -1, projectsPerPage, null, path), + '/admin/projects'); + + filter = 'plugins/'; + assert.equal( + element._computeNavLink(offset, 1, projectsPerPage, filter, path), + '/admin/projects/q/filter:plugins%252F,50'); + }); + + test('_onValueChange', done => { + element.path = '/admin/projects'; + sandbox.stub(page, 'show', url => { + assert.equal(url, '/admin/projects/q/filter:test'); + done(); }); + element.filter = 'test'; + }); - teardown(() => { - sandbox.restore(); - }); + test('_filterChanged not reload when swap between falsy values', () => { + sandbox.stub(element, '_debounceReload'); + element.filter = null; + element.filter = undefined; + element.filter = ''; + assert.isFalse(element._debounceReload.called); + }); - test('_computeNavLink', () => { - const offset = 25; - const projectsPerPage = 25; - let filter = 'test'; - const path = '/admin/projects'; + test('next button', done => { + element.itemsPerPage = 25; + let projects = new Array(26); - sandbox.stub(element, 'getBaseUrl', () => ''); - - assert.equal( - element._computeNavLink(offset, 1, projectsPerPage, filter, path), - '/admin/projects/q/filter:test,50'); - - assert.equal( - element._computeNavLink(offset, -1, projectsPerPage, filter, path), - '/admin/projects/q/filter:test'); - - assert.equal( - element._computeNavLink(offset, 1, projectsPerPage, null, path), - '/admin/projects,50'); - - assert.equal( - element._computeNavLink(offset, -1, projectsPerPage, null, path), - '/admin/projects'); - - filter = 'plugins/'; - assert.equal( - element._computeNavLink(offset, 1, projectsPerPage, filter, path), - '/admin/projects/q/filter:plugins%252F,50'); - }); - - test('_onValueChange', done => { - element.path = '/admin/projects'; - sandbox.stub(page, 'show', url => { - assert.equal(url, '/admin/projects/q/filter:test'); - done(); - }); - element.filter = 'test'; - }); - - test('_filterChanged not reload when swap between falsy values', () => { - sandbox.stub(element, '_debounceReload'); - element.filter = null; - element.filter = undefined; - element.filter = ''; - assert.isFalse(element._debounceReload.called); - }); - - test('next button', done => { - element.itemsPerPage = 25; - let projects = new Array(26); - - flush(() => { - let loading; - assert.isFalse(element._hideNextArrow(loading, projects)); - loading = true; - assert.isTrue(element._hideNextArrow(loading, projects)); - loading = false; - assert.isFalse(element._hideNextArrow(loading, projects)); - element._projects = []; - assert.isTrue(element._hideNextArrow(loading, element._projects)); - projects = new Array(4); - assert.isTrue(element._hideNextArrow(loading, projects)); - done(); - }); - }); - - test('prev button', () => { - assert.isTrue(element._hidePrevArrow(true, 0)); - flush(() => { - let offset = 0; - assert.isTrue(element._hidePrevArrow(false, offset)); - offset = 5; - assert.isFalse(element._hidePrevArrow(false, offset)); - }); - }); - - test('createNew link appears correctly', () => { - assert.isFalse(element.shadowRoot - .querySelector('#createNewContainer').classList - .contains('show')); - element.createNew = true; - flushAsynchronousOperations(); - assert.isTrue(element.shadowRoot - .querySelector('#createNewContainer').classList - .contains('show')); - }); - - test('fires create clicked event when button tapped', () => { - const clickHandler = sandbox.stub(); - element.addEventListener('create-clicked', clickHandler); - element.createNew = true; - flushAsynchronousOperations(); - MockInteractions.tap(element.shadowRoot.querySelector('#createNew')); - assert.isTrue(clickHandler.called); - }); - - test('next/prev links change when path changes', () => { - const BRANCHES_PATH = '/path/to/branches'; - const TAGS_PATH = '/path/to/tags'; - sandbox.stub(element, '_computeNavLink'); - element.offset = 0; - element.itemsPerPage = 25; - element.filter = ''; - element.path = BRANCHES_PATH; - assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH); - element.path = TAGS_PATH; - assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH); - }); - - test('_computePage', () => { - assert.equal(element._computePage(0, 25), 1); - assert.equal(element._computePage(50, 25), 3); + flush(() => { + let loading; + assert.isFalse(element._hideNextArrow(loading, projects)); + loading = true; + assert.isTrue(element._hideNextArrow(loading, projects)); + loading = false; + assert.isFalse(element._hideNextArrow(loading, projects)); + element._projects = []; + assert.isTrue(element._hideNextArrow(loading, element._projects)); + projects = new Array(4); + assert.isTrue(element._hideNextArrow(loading, projects)); + done(); }); }); + + test('prev button', () => { + assert.isTrue(element._hidePrevArrow(true, 0)); + flush(() => { + let offset = 0; + assert.isTrue(element._hidePrevArrow(false, offset)); + offset = 5; + assert.isFalse(element._hidePrevArrow(false, offset)); + }); + }); + + test('createNew link appears correctly', () => { + assert.isFalse(element.shadowRoot + .querySelector('#createNewContainer').classList + .contains('show')); + element.createNew = true; + flushAsynchronousOperations(); + assert.isTrue(element.shadowRoot + .querySelector('#createNewContainer').classList + .contains('show')); + }); + + test('fires create clicked event when button tapped', () => { + const clickHandler = sandbox.stub(); + element.addEventListener('create-clicked', clickHandler); + element.createNew = true; + flushAsynchronousOperations(); + MockInteractions.tap(element.shadowRoot.querySelector('#createNew')); + assert.isTrue(clickHandler.called); + }); + + test('next/prev links change when path changes', () => { + const BRANCHES_PATH = '/path/to/branches'; + const TAGS_PATH = '/path/to/tags'; + sandbox.stub(element, '_computeNavLink'); + element.offset = 0; + element.itemsPerPage = 25; + element.filter = ''; + element.path = BRANCHES_PATH; + assert.equal(element._computeNavLink.lastCall.args[4], BRANCHES_PATH); + element.path = TAGS_PATH; + assert.equal(element._computeNavLink.lastCall.args[4], TAGS_PATH); + }); + + test('_computePage', () => { + assert.equal(element._computePage(0, 25), 1); + assert.equal(element._computePage(50, 25), 3); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js index 3a957ef..fd68971 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay.js
@@ -14,108 +14,117 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const AWAIT_MAX_ITERS = 10; - const AWAIT_STEP = 5; - const BREAKPOINT_FULLSCREEN_OVERLAY = '50em'; +import {IronOverlayBehaviorImpl, IronOverlayBehavior} from '@polymer/iron-overlay-behavior/iron-overlay-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../styles/shared-styles.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-overlay_html.js'; + +const AWAIT_MAX_ITERS = 10; +const AWAIT_STEP = 5; +const BREAKPOINT_FULLSCREEN_OVERLAY = '50em'; + +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrOverlay extends mixinBehaviors( [ + Gerrit.FireBehavior, + IronOverlayBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-overlay'; } + /** + * Fired when a fullscreen overlay is closed + * + * @event fullscreen-overlay-closed + */ /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element + * Fired when an overlay is opened in full screen mode + * + * @event fullscreen-overlay-opened */ - class GrOverlay extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Polymer.IronOverlayBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-overlay'; } - /** - * Fired when a fullscreen overlay is closed - * - * @event fullscreen-overlay-closed - */ - /** - * Fired when an overlay is opened in full screen mode - * - * @event fullscreen-overlay-opened - */ + static get properties() { + return { + _fullScreenOpen: { + type: Boolean, + value: false, + }, + }; + } - static get properties() { - return { - _fullScreenOpen: { - type: Boolean, - value: false, - }, - }; - } + /** @override */ + created() { + super.created(); + this.addEventListener('iron-overlay-closed', + () => this._close()); + this.addEventListener('iron-overlay-cancelled', + () => this._close()); + } - /** @override */ - created() { - super.created(); - this.addEventListener('iron-overlay-closed', - () => this._close()); - this.addEventListener('iron-overlay-cancelled', - () => this._close()); - } - - open(...args) { - return new Promise((resolve, reject) => { - Polymer.IronOverlayBehaviorImpl.open.apply(this, args); - if (this._isMobile()) { - this.fire('fullscreen-overlay-opened'); - this._fullScreenOpen = true; - } - this._awaitOpen(resolve, reject); - }); - } - - _isMobile() { - return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`); - } - - _close() { - if (this._fullScreenOpen) { - this.fire('fullscreen-overlay-closed'); - this._fullScreenOpen = false; + open(...args) { + return new Promise((resolve, reject) => { + IronOverlayBehaviorImpl.open.apply(this, args); + if (this._isMobile()) { + this.fire('fullscreen-overlay-opened'); + this._fullScreenOpen = true; } - } + this._awaitOpen(resolve, reject); + }); + } - /** - * Override the focus stops that iron-overlay-behavior tries to find. - */ - setFocusStops(stops) { - this.__firstFocusableNode = stops.start; - this.__lastFocusableNode = stops.end; - } + _isMobile() { + return window.matchMedia(`(max-width: ${BREAKPOINT_FULLSCREEN_OVERLAY})`); + } - /** - * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually - * opening. Eventually replace with a direct way to listen to the overlay. - */ - _awaitOpen(fn, reject) { - let iters = 0; - const step = () => { - this.async(() => { - if (this.style.display !== 'none') { - fn.call(this); - } else if (iters++ < AWAIT_MAX_ITERS) { - step.call(this); - } else { - reject(new Error('gr-overlay _awaitOpen failed to resolve')); - } - }, AWAIT_STEP); - }; - step.call(this); - } - - _id() { - return this.getAttribute('id') || 'global'; + _close() { + if (this._fullScreenOpen) { + this.fire('fullscreen-overlay-closed'); + this._fullScreenOpen = false; } } - customElements.define(GrOverlay.is, GrOverlay); -})(); + /** + * Override the focus stops that iron-overlay-behavior tries to find. + */ + setFocusStops(stops) { + this.__firstFocusableNode = stops.start; + this.__lastFocusableNode = stops.end; + } + + /** + * NOTE: (wyatta) Slightly hacky way to listen to the overlay actually + * opening. Eventually replace with a direct way to listen to the overlay. + */ + _awaitOpen(fn, reject) { + let iters = 0; + const step = () => { + this.async(() => { + if (this.style.display !== 'none') { + fn.call(this); + } else if (iters++ < AWAIT_MAX_ITERS) { + step.call(this); + } else { + reject(new Error('gr-overlay _awaitOpen failed to resolve')); + } + }, AWAIT_STEP); + }; + step.call(this); + } + + _id() { + return this.getAttribute('id') || 'global'; + } +} + +customElements.define(GrOverlay.is, GrOverlay);
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js index 1afd1c9..5fe000a 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_html.js
@@ -1,27 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-overlay-behavior/iron-overlay-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-overlay"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { background: var(--dialog-background-color); @@ -42,6 +37,4 @@ } </style> <slot></slot> - </template> - <script src="gr-overlay.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html index e1218af3..72f01fb 100644 --- a/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html +++ b/polygerrit-ui/app/elements/shared/gr-overlay/gr-overlay_test.html
@@ -19,18 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-overlay</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-overlay.html"> +<script type="module" src="./gr-overlay.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-overlay.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -40,57 +45,59 @@ </template> </test-fixture> -<script> - suite('gr-overlay tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-overlay.js'; +suite('gr-overlay tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('events are fired on fullscreen view', done => { - sandbox.stub(element, '_isMobile').returns(true); - const openHandler = sandbox.stub(); - const closeHandler = sandbox.stub(); - element.addEventListener('fullscreen-overlay-opened', openHandler); - element.addEventListener('fullscreen-overlay-closed', closeHandler); + test('events are fired on fullscreen view', done => { + sandbox.stub(element, '_isMobile').returns(true); + const openHandler = sandbox.stub(); + const closeHandler = sandbox.stub(); + element.addEventListener('fullscreen-overlay-opened', openHandler); + element.addEventListener('fullscreen-overlay-closed', closeHandler); - element.open().then(() => { - assert.isTrue(element._isMobile.called); - assert.isTrue(element._fullScreenOpen); - assert.isTrue(openHandler.called); + element.open().then(() => { + assert.isTrue(element._isMobile.called); + assert.isTrue(element._fullScreenOpen); + assert.isTrue(openHandler.called); - element._close(); - assert.isFalse(element._fullScreenOpen); - assert.isTrue(closeHandler.called); - done(); - }); - }); - - test('events are not fired on desktop view', done => { - sandbox.stub(element, '_isMobile').returns(false); - const openHandler = sandbox.stub(); - const closeHandler = sandbox.stub(); - element.addEventListener('fullscreen-overlay-opened', openHandler); - element.addEventListener('fullscreen-overlay-closed', closeHandler); - - element.open().then(() => { - assert.isTrue(element._isMobile.called); - assert.isFalse(element._fullScreenOpen); - assert.isFalse(openHandler.called); - - element._close(); - assert.isFalse(element._fullScreenOpen); - assert.isFalse(closeHandler.called); - done(); - }); + element._close(); + assert.isFalse(element._fullScreenOpen); + assert.isTrue(closeHandler.called); + done(); }); }); + + test('events are not fired on desktop view', done => { + sandbox.stub(element, '_isMobile').returns(false); + const openHandler = sandbox.stub(); + const closeHandler = sandbox.stub(); + element.addEventListener('fullscreen-overlay-opened', openHandler); + element.addEventListener('fullscreen-overlay-closed', closeHandler); + + element.open().then(() => { + assert.isTrue(element._isMobile.called); + assert.isFalse(element._fullScreenOpen); + assert.isFalse(openHandler.called); + + element._close(); + assert.isFalse(element._fullScreenOpen); + assert.isFalse(closeHandler.called); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js index ac876c4..23b284c 100644 --- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav.js
@@ -14,62 +14,69 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; - /** @extends Polymer.Element */ - class GrPageNav extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-page-nav'; } +import '../../../scripts/bundled-polymer.js'; +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-page-nav_html.js'; - static get properties() { - return { - _headerHeight: Number, - }; - } +/** @extends Polymer.Element */ +class GrPageNav extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - /** @override */ - attached() { - super.attached(); - this.listen(window, 'scroll', '_handleBodyScroll'); - } + static get is() { return 'gr-page-nav'; } - /** @override */ - detached() { - super.detached(); - this.unlisten(window, 'scroll', '_handleBodyScroll'); - } - - _handleBodyScroll() { - if (this._headerHeight === undefined) { - let top = this._getOffsetTop(this); - for (let offsetParent = this.offsetParent; - offsetParent; - offsetParent = this._getOffsetParent(offsetParent)) { - top += this._getOffsetTop(offsetParent); - } - this._headerHeight = top; - } - - this.$.nav.classList.toggle('pinned', - this._getScrollY() >= this._headerHeight); - } - - /* Functions used for test purposes */ - _getOffsetParent(element) { - if (!element || !element.offsetParent) { return ''; } - return element.offsetParent; - } - - _getOffsetTop(element) { - return element.offsetTop; - } - - _getScrollY() { - return window.scrollY; - } + static get properties() { + return { + _headerHeight: Number, + }; } - customElements.define(GrPageNav.is, GrPageNav); -})(); + /** @override */ + attached() { + super.attached(); + this.listen(window, 'scroll', '_handleBodyScroll'); + } + + /** @override */ + detached() { + super.detached(); + this.unlisten(window, 'scroll', '_handleBodyScroll'); + } + + _handleBodyScroll() { + if (this._headerHeight === undefined) { + let top = this._getOffsetTop(this); + for (let offsetParent = this.offsetParent; + offsetParent; + offsetParent = this._getOffsetParent(offsetParent)) { + top += this._getOffsetTop(offsetParent); + } + this._headerHeight = top; + } + + this.$.nav.classList.toggle('pinned', + this._getScrollY() >= this._headerHeight); + } + + /* Functions used for test purposes */ + _getOffsetParent(element) { + if (!element || !element.offsetParent) { return ''; } + return element.offsetParent; + } + + _getOffsetTop(element) { + return element.offsetTop; + } + + _getScrollY() { + return window.scrollY; + } +} + +customElements.define(GrPageNav.is, GrPageNav);
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js index f1c3a6f..fe17a1b 100644 --- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-page-nav"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> #nav { background-color: var(--table-header-background-color); @@ -42,6 +39,4 @@ <nav id="nav"> <slot></slot> </nav> - </template> - <script src="gr-page-nav.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html index fdfe46c..4f47b95 100644 --- a/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html +++ b/polygerrit-ui/app/elements/shared/gr-page-nav/gr-page-nav_test.html
@@ -19,18 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-page-nav</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/page/page.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> +<script src="/node_modules/page/page.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="gr-page-nav.html"> +<script type="module" src="./gr-page-nav.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-page-nav.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -42,53 +47,55 @@ </template> </test-fixture> -<script> - suite('gr-page-nav tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-page-nav.js'; +suite('gr-page-nav tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - flushAsynchronousOperations(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('header is not pinned just below top', () => { - sandbox.stub(element, '_getOffsetParent', () => 0); - sandbox.stub(element, '_getOffsetTop', () => 10); - sandbox.stub(element, '_getScrollY', () => 5); - element._handleBodyScroll(); - assert.isFalse(element.$.nav.classList.contains('pinned')); - }); - - test('header is pinned when scroll down the page', () => { - sandbox.stub(element, '_getOffsetParent', () => 0); - sandbox.stub(element, '_getOffsetTop', () => 10); - sandbox.stub(element, '_getScrollY', () => 25); - window.scrollY = 100; - element._handleBodyScroll(); - assert.isTrue(element.$.nav.classList.contains('pinned')); - }); - - test('header is not pinned just below top with header set', () => { - element._headerHeight = 20; - sandbox.stub(element, '_getScrollY', () => 15); - window.scrollY = 100; - element._handleBodyScroll(); - assert.isFalse(element.$.nav.classList.contains('pinned')); - }); - - test('header is pinned when scroll down the page with header set', () => { - element._headerHeight = 20; - sandbox.stub(element, '_getScrollY', () => 25); - window.scrollY = 100; - element._handleBodyScroll(); - assert.isTrue(element.$.nav.classList.contains('pinned')); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + flushAsynchronousOperations(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('header is not pinned just below top', () => { + sandbox.stub(element, '_getOffsetParent', () => 0); + sandbox.stub(element, '_getOffsetTop', () => 10); + sandbox.stub(element, '_getScrollY', () => 5); + element._handleBodyScroll(); + assert.isFalse(element.$.nav.classList.contains('pinned')); + }); + + test('header is pinned when scroll down the page', () => { + sandbox.stub(element, '_getOffsetParent', () => 0); + sandbox.stub(element, '_getOffsetTop', () => 10); + sandbox.stub(element, '_getScrollY', () => 25); + window.scrollY = 100; + element._handleBodyScroll(); + assert.isTrue(element.$.nav.classList.contains('pinned')); + }); + + test('header is not pinned just below top with header set', () => { + element._headerHeight = 20; + sandbox.stub(element, '_getScrollY', () => 15); + window.scrollY = 100; + element._handleBodyScroll(); + assert.isFalse(element.$.nav.classList.contains('pinned')); + }); + + test('header is pinned when scroll down the page with header set', () => { + element._headerHeight = 20; + sandbox.stub(element, '_getScrollY', () => 25); + window.scrollY = 100; + element._handleBodyScroll(); + assert.isTrue(element.$.nav.classList.contains('pinned')); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js index 18e2596..15d5f76 100644 --- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js +++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker.js
@@ -14,110 +14,122 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const SUGGESTIONS_LIMIT = 15; - const REF_PREFIX = 'refs/heads/'; +import '@polymer/iron-icon/iron-icon.js'; +import '../../../styles/shared-styles.js'; +import '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js'; +import '../gr-icons/gr-icons.js'; +import '../gr-labeled-autocomplete/gr-labeled-autocomplete.js'; +import '../gr-rest-api-interface/gr-rest-api-interface.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-repo-branch-picker_html.js'; - /** - * @appliesMixin Gerrit.URLEncodingMixin - * @extends Polymer.Element - */ - class GrRepoBranchPicker extends Polymer.mixinBehaviors( [ - Gerrit.URLEncodingBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-repo-branch-picker'; } +const SUGGESTIONS_LIMIT = 15; +const REF_PREFIX = 'refs/heads/'; - static get properties() { - return { - repo: { - type: String, - notify: true, - observer: '_repoChanged', +/** + * @appliesMixin Gerrit.URLEncodingMixin + * @extends Polymer.Element + */ +class GrRepoBranchPicker extends mixinBehaviors( [ + Gerrit.URLEncodingBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-repo-branch-picker'; } + + static get properties() { + return { + repo: { + type: String, + notify: true, + observer: '_repoChanged', + }, + branch: { + type: String, + notify: true, + }, + _branchDisabled: Boolean, + _query: { + type: Function, + value() { + return this._getRepoBranchesSuggestions.bind(this); }, - branch: { - type: String, - notify: true, + }, + _repoQuery: { + type: Function, + value() { + return this._getRepoSuggestions.bind(this); }, - _branchDisabled: Boolean, - _query: { - type: Function, - value() { - return this._getRepoBranchesSuggestions.bind(this); - }, - }, - _repoQuery: { - type: Function, - value() { - return this._getRepoSuggestions.bind(this); - }, - }, - }; - } + }, + }; + } - /** @override */ - attached() { - super.attached(); - if (this.repo) { - this.$.repoInput.setText(this.repo); - } - } - - /** @override */ - ready() { - super.ready(); - this._branchDisabled = !this.repo; - } - - _getRepoBranchesSuggestions(input) { - if (!this.repo) { return Promise.resolve([]); } - if (input.startsWith(REF_PREFIX)) { - input = input.substring(REF_PREFIX.length); - } - return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT) - .then(this._branchResponseToSuggestions.bind(this)); - } - - _getRepoSuggestions(input) { - return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT) - .then(this._repoResponseToSuggestions.bind(this)); - } - - _repoResponseToSuggestions(res) { - return res.map(repo => { - return { - name: repo.name, - value: this.singleDecodeURL(repo.id), - }; - }); - } - - _branchResponseToSuggestions(res) { - return Object.keys(res).map(key => { - let branch = res[key].ref; - if (branch.startsWith(REF_PREFIX)) { - branch = branch.substring(REF_PREFIX.length); - } - return {name: branch, value: branch}; - }); - } - - _repoCommitted(e) { - this.repo = e.detail.value; - } - - _branchCommitted(e) { - this.branch = e.detail.value; - } - - _repoChanged() { - this.$.branchInput.clear(); - this._branchDisabled = !this.repo; + /** @override */ + attached() { + super.attached(); + if (this.repo) { + this.$.repoInput.setText(this.repo); } } - customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker); -})(); + /** @override */ + ready() { + super.ready(); + this._branchDisabled = !this.repo; + } + + _getRepoBranchesSuggestions(input) { + if (!this.repo) { return Promise.resolve([]); } + if (input.startsWith(REF_PREFIX)) { + input = input.substring(REF_PREFIX.length); + } + return this.$.restAPI.getRepoBranches(input, this.repo, SUGGESTIONS_LIMIT) + .then(this._branchResponseToSuggestions.bind(this)); + } + + _getRepoSuggestions(input) { + return this.$.restAPI.getRepos(input, SUGGESTIONS_LIMIT) + .then(this._repoResponseToSuggestions.bind(this)); + } + + _repoResponseToSuggestions(res) { + return res.map(repo => { + return { + name: repo.name, + value: this.singleDecodeURL(repo.id), + }; + }); + } + + _branchResponseToSuggestions(res) { + return Object.keys(res).map(key => { + let branch = res[key].ref; + if (branch.startsWith(REF_PREFIX)) { + branch = branch.substring(REF_PREFIX.length); + } + return {name: branch, value: branch}; + }); + } + + _repoCommitted(e) { + this.repo = e.detail.value; + } + + _branchCommitted(e) { + this.branch = e.detail.value; + } + + _repoChanged() { + this.$.branchInput.clear(); + this._branchDisabled = !this.repo; + } +} + +customElements.define(GrRepoBranchPicker.is, GrRepoBranchPicker);
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js index ce596f8..fe6b522 100644 --- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js +++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_html.js
@@ -1,29 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="/bower_components/iron-icon/iron-icon.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.html"> -<link rel="import" href="../../shared/gr-icons/gr-icons.html"> -<link rel="import" href="../../shared/gr-labeled-autocomplete/gr-labeled-autocomplete.html"> -<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> - -<dom-module id="gr-repo-branch-picker"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: block; @@ -37,24 +30,11 @@ } </style> <div> - <gr-labeled-autocomplete - id="repoInput" - label="Repository" - placeholder="Select repo" - on-commit="_repoCommitted" - query="[[_repoQuery]]"> + <gr-labeled-autocomplete id="repoInput" label="Repository" placeholder="Select repo" on-commit="_repoCommitted" query="[[_repoQuery]]"> </gr-labeled-autocomplete> <iron-icon icon="gr-icons:chevron-right"></iron-icon> - <gr-labeled-autocomplete - id="branchInput" - label="Branch" - placeholder="Select branch" - disabled="[[_branchDisabled]]" - on-commit="_branchCommitted" - query="[[_query]]"> + <gr-labeled-autocomplete id="branchInput" label="Branch" placeholder="Select branch" disabled="[[_branchDisabled]]" on-commit="_branchCommitted" query="[[_query]]"> </gr-labeled-autocomplete> </div> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface> - </template> - <script src="gr-repo-branch-picker.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html index b068b25..191b5d5 100644 --- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html +++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-repo-branch-picker</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-repo-branch-picker.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-repo-branch-picker.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-branch-picker.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,113 +39,115 @@ </template> </test-fixture> -<script> - suite('gr-repo-branch-picker tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-repo-branch-picker.js'; +suite('gr-repo-branch-picker tests', () => { + let element; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + }); + + teardown(() => { sandbox.restore(); }); + + suite('_getRepoSuggestions', () => { setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); + sandbox.stub(element.$.restAPI, 'getRepos') + .returns(Promise.resolve([ + { + id: 'plugins%2Favatars-external', + name: 'plugins/avatars-external', + }, { + id: 'plugins%2Favatars-gravatar', + name: 'plugins/avatars-gravatar', + }, { + id: 'plugins%2Favatars%2Fexternal', + name: 'plugins/avatars/external', + }, { + id: 'plugins%2Favatars%2Fgravatar', + name: 'plugins/avatars/gravatar', + }, + ])); }); - teardown(() => { sandbox.restore(); }); - - suite('_getRepoSuggestions', () => { - setup(() => { - sandbox.stub(element.$.restAPI, 'getRepos') - .returns(Promise.resolve([ - { - id: 'plugins%2Favatars-external', - name: 'plugins/avatars-external', - }, { - id: 'plugins%2Favatars-gravatar', - name: 'plugins/avatars-gravatar', - }, { - id: 'plugins%2Favatars%2Fexternal', - name: 'plugins/avatars/external', - }, { - id: 'plugins%2Favatars%2Fgravatar', - name: 'plugins/avatars/gravatar', - }, - ])); - }); - - test('converts to suggestion objects', () => { - const input = 'plugins/avatars'; - return element._getRepoSuggestions(input).then(suggestions => { - assert.isTrue(element.$.restAPI.getRepos.calledWith(input)); - const unencodedNames = [ - 'plugins/avatars-external', - 'plugins/avatars-gravatar', - 'plugins/avatars/external', - 'plugins/avatars/gravatar', - ]; - assert.deepEqual(suggestions.map(s => s.name), unencodedNames); - assert.deepEqual(suggestions.map(s => s.value), unencodedNames); - }); - }); - }); - - suite('_getRepoBranchesSuggestions', () => { - setup(() => { - sandbox.stub(element.$.restAPI, 'getRepoBranches') - .returns(Promise.resolve([ - {ref: 'refs/heads/stable-2.10'}, - {ref: 'refs/heads/stable-2.11'}, - {ref: 'refs/heads/stable-2.12'}, - {ref: 'refs/heads/stable-2.13'}, - {ref: 'refs/heads/stable-2.14'}, - {ref: 'refs/heads/stable-2.15'}, - ])); - }); - - test('converts to suggestion objects', () => { - const repo = 'gerrit'; - const branchInput = 'stable-2.1'; - element.repo = repo; - return element._getRepoBranchesSuggestions(branchInput) - .then(suggestions => { - assert.isTrue(element.$.restAPI.getRepoBranches.calledWith( - branchInput, repo, 15)); - const refNames = [ - 'stable-2.10', - 'stable-2.11', - 'stable-2.12', - 'stable-2.13', - 'stable-2.14', - 'stable-2.15', - ]; - assert.deepEqual(suggestions.map(s => s.name), refNames); - assert.deepEqual(suggestions.map(s => s.value), refNames); - }); - }); - - test('filters out ref prefix', () => { - const repo = 'gerrit'; - const branchInput = 'refs/heads/stable-2.1'; - element.repo = repo; - return element._getRepoBranchesSuggestions(branchInput) - .then(suggestions => { - assert.isTrue(element.$.restAPI.getRepoBranches.calledWith( - 'stable-2.1', repo, 15)); - }); - }); - - test('does not query when repo is unset', done => { - element - ._getRepoBranchesSuggestions('') - .then(() => { - assert.isFalse(element.$.restAPI.getRepoBranches.called); - element.repo = 'gerrit'; - return element._getRepoBranchesSuggestions(''); - }) - .then(() => { - assert.isTrue(element.$.restAPI.getRepoBranches.called); - done(); - }); + test('converts to suggestion objects', () => { + const input = 'plugins/avatars'; + return element._getRepoSuggestions(input).then(suggestions => { + assert.isTrue(element.$.restAPI.getRepos.calledWith(input)); + const unencodedNames = [ + 'plugins/avatars-external', + 'plugins/avatars-gravatar', + 'plugins/avatars/external', + 'plugins/avatars/gravatar', + ]; + assert.deepEqual(suggestions.map(s => s.name), unencodedNames); + assert.deepEqual(suggestions.map(s => s.value), unencodedNames); }); }); }); + + suite('_getRepoBranchesSuggestions', () => { + setup(() => { + sandbox.stub(element.$.restAPI, 'getRepoBranches') + .returns(Promise.resolve([ + {ref: 'refs/heads/stable-2.10'}, + {ref: 'refs/heads/stable-2.11'}, + {ref: 'refs/heads/stable-2.12'}, + {ref: 'refs/heads/stable-2.13'}, + {ref: 'refs/heads/stable-2.14'}, + {ref: 'refs/heads/stable-2.15'}, + ])); + }); + + test('converts to suggestion objects', () => { + const repo = 'gerrit'; + const branchInput = 'stable-2.1'; + element.repo = repo; + return element._getRepoBranchesSuggestions(branchInput) + .then(suggestions => { + assert.isTrue(element.$.restAPI.getRepoBranches.calledWith( + branchInput, repo, 15)); + const refNames = [ + 'stable-2.10', + 'stable-2.11', + 'stable-2.12', + 'stable-2.13', + 'stable-2.14', + 'stable-2.15', + ]; + assert.deepEqual(suggestions.map(s => s.name), refNames); + assert.deepEqual(suggestions.map(s => s.value), refNames); + }); + }); + + test('filters out ref prefix', () => { + const repo = 'gerrit'; + const branchInput = 'refs/heads/stable-2.1'; + element.repo = repo; + return element._getRepoBranchesSuggestions(branchInput) + .then(suggestions => { + assert.isTrue(element.$.restAPI.getRepoBranches.calledWith( + 'stable-2.1', repo, 15)); + }); + }); + + test('does not query when repo is unset', done => { + element + ._getRepoBranchesSuggestions('') + .then(() => { + assert.isFalse(element.$.restAPI.getRepoBranches.called); + element.repo = 'gerrit'; + return element._getRepoBranchesSuggestions(''); + }) + .then(() => { + assert.isTrue(element.$.restAPI.getRepoBranches.called); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html index 091c88e..01baa43 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-auth_test.html
@@ -19,377 +19,380 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-auth</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../behaviors/base-url-behavior/base-url-behavior.js"></script> -<script src="gr-auth.js"></script> +<script type="module" src="./gr-auth.js"></script> -<script> - suite('gr-auth', async () => { - await readyToTest(); - let auth; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import './gr-auth.js'; +suite('gr-auth', () => { + let auth; + let sandbox; + setup(() => { + sandbox = sinon.sandbox.create(); + auth = Gerrit.Auth; + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('Auth class methods', () => { + let fakeFetch; setup(() => { - sandbox = sinon.sandbox.create(); - auth = Gerrit.Auth; + auth = new Auth(); + fakeFetch = sandbox.stub(window, 'fetch'); }); - teardown(() => { - sandbox.restore(); - }); - - suite('Auth class methods', () => { - let fakeFetch; - setup(() => { - auth = new Auth(); - fakeFetch = sandbox.stub(window, 'fetch'); + test('auth-check returns 403', done => { + fakeFetch.returns(Promise.resolve({status: 403})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + done(); }); + }); - test('auth-check returns 403', done => { - fakeFetch.returns(Promise.resolve({status: 403})); - auth.authCheck().then(authed => { + test('auth-check returns 204', done => { + fakeFetch.returns(Promise.resolve({status: 204})); + auth.authCheck().then(authed => { + assert.isTrue(authed); + assert.equal(auth.status, Auth.STATUS.AUTHED); + done(); + }); + }); + + test('auth-check returns 502', done => { + fakeFetch.returns(Promise.resolve({status: 502})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + done(); + }); + }); + + test('auth-check failed', done => { + fakeFetch.returns(Promise.reject(new Error('random error'))); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.ERROR); + done(); + }); + }); + }); + + suite('cache and events behaivor', () => { + let fakeFetch; + let clock; + setup(() => { + auth = new Auth(); + clock = sinon.useFakeTimers(); + fakeFetch = sandbox.stub(window, 'fetch'); + }); + + test('cache auth-check result', done => { + fakeFetch.returns(Promise.resolve({status: 403})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + fakeFetch.returns(Promise.resolve({status: 204})); + auth.authCheck().then(authed2 => { assert.isFalse(authed); assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); done(); }); }); + }); - test('auth-check returns 204', done => { + test('clearCache should refetch auth-check result', done => { + fakeFetch.returns(Promise.resolve({status: 403})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); fakeFetch.returns(Promise.resolve({status: 204})); - auth.authCheck().then(authed => { - assert.isTrue(authed); + auth.clearCache(); + auth.authCheck().then(authed2 => { + assert.isTrue(authed2); assert.equal(auth.status, Auth.STATUS.AUTHED); done(); }); }); + }); - test('auth-check returns 502', done => { - fakeFetch.returns(Promise.resolve({status: 502})); - auth.authCheck().then(authed => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + test('cache expired on auth-check after certain time', done => { + fakeFetch.returns(Promise.resolve({status: 403})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + clock.tick(1000 * 10000); + fakeFetch.returns(Promise.resolve({status: 204})); + auth.authCheck().then(authed2 => { + assert.isTrue(authed2); + assert.equal(auth.status, Auth.STATUS.AUTHED); done(); }); }); + }); - test('auth-check failed', done => { + test('no cache if auth-check failed', done => { + fakeFetch.returns(Promise.reject(new Error('random error'))); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.ERROR); + assert.equal(fakeFetch.callCount, 1); + auth.authCheck().then(() => { + assert.equal(fakeFetch.callCount, 2); + done(); + }); + }); + }); + + test('fire event when switch from authed to unauthed', done => { + fakeFetch.returns(Promise.resolve({status: 204})); + auth.authCheck().then(authed => { + assert.isTrue(authed); + assert.equal(auth.status, Auth.STATUS.AUTHED); + clock.tick(1000 * 10000); + fakeFetch.returns(Promise.resolve({status: 403})); + const emitStub = sinon.stub(); + Gerrit.emit = emitStub; + auth.authCheck().then(authed2 => { + assert.isFalse(authed2); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + assert.isTrue(emitStub.called); + done(); + }); + }); + }); + + test('fire event when switch from authed to error', done => { + fakeFetch.returns(Promise.resolve({status: 204})); + auth.authCheck().then(authed => { + assert.isTrue(authed); + assert.equal(auth.status, Auth.STATUS.AUTHED); + clock.tick(1000 * 10000); fakeFetch.returns(Promise.reject(new Error('random error'))); - auth.authCheck().then(authed => { - assert.isFalse(authed); + const emitStub = sinon.stub(); + Gerrit.emit = emitStub; + auth.authCheck().then(authed2 => { + assert.isFalse(authed2); + assert.isTrue(emitStub.called); assert.equal(auth.status, Auth.STATUS.ERROR); done(); }); }); }); - suite('cache and events behaivor', () => { - let fakeFetch; - let clock; - setup(() => { - auth = new Auth(); - clock = sinon.useFakeTimers(); - fakeFetch = sandbox.stub(window, 'fetch'); - }); - - test('cache auth-check result', done => { - fakeFetch.returns(Promise.resolve({status: 403})); - auth.authCheck().then(authed => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - fakeFetch.returns(Promise.resolve({status: 204})); - auth.authCheck().then(authed2 => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - done(); - }); + test('no event from non-authed to other status', done => { + fakeFetch.returns(Promise.resolve({status: 403})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + clock.tick(1000 * 10000); + fakeFetch.returns(Promise.resolve({status: 204})); + const emitStub = sinon.stub(); + Gerrit.emit = emitStub; + auth.authCheck().then(authed2 => { + assert.isTrue(authed2); + assert.isFalse(emitStub.called); + assert.equal(auth.status, Auth.STATUS.AUTHED); + done(); }); }); + }); - test('clearCache should refetch auth-check result', done => { - fakeFetch.returns(Promise.resolve({status: 403})); - auth.authCheck().then(authed => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - fakeFetch.returns(Promise.resolve({status: 204})); - auth.clearCache(); - auth.authCheck().then(authed2 => { - assert.isTrue(authed2); - assert.equal(auth.status, Auth.STATUS.AUTHED); - done(); - }); - }); - }); - - test('cache expired on auth-check after certain time', done => { - fakeFetch.returns(Promise.resolve({status: 403})); - auth.authCheck().then(authed => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - clock.tick(1000 * 10000); - fakeFetch.returns(Promise.resolve({status: 204})); - auth.authCheck().then(authed2 => { - assert.isTrue(authed2); - assert.equal(auth.status, Auth.STATUS.AUTHED); - done(); - }); - }); - }); - - test('no cache if auth-check failed', done => { + test('no event from non-authed to other status', done => { + fakeFetch.returns(Promise.resolve({status: 403})); + auth.authCheck().then(authed => { + assert.isFalse(authed); + assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); + clock.tick(1000 * 10000); fakeFetch.returns(Promise.reject(new Error('random error'))); - auth.authCheck().then(authed => { - assert.isFalse(authed); + const emitStub = sinon.stub(); + Gerrit.emit = emitStub; + auth.authCheck().then(authed2 => { + assert.isFalse(authed2); + assert.isFalse(emitStub.called); assert.equal(auth.status, Auth.STATUS.ERROR); - assert.equal(fakeFetch.callCount, 1); - auth.authCheck().then(() => { - assert.equal(fakeFetch.callCount, 2); - done(); - }); - }); - }); - - test('fire event when switch from authed to unauthed', done => { - fakeFetch.returns(Promise.resolve({status: 204})); - auth.authCheck().then(authed => { - assert.isTrue(authed); - assert.equal(auth.status, Auth.STATUS.AUTHED); - clock.tick(1000 * 10000); - fakeFetch.returns(Promise.resolve({status: 403})); - const emitStub = sinon.stub(); - Gerrit.emit = emitStub; - auth.authCheck().then(authed2 => { - assert.isFalse(authed2); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - assert.isTrue(emitStub.called); - done(); - }); - }); - }); - - test('fire event when switch from authed to error', done => { - fakeFetch.returns(Promise.resolve({status: 204})); - auth.authCheck().then(authed => { - assert.isTrue(authed); - assert.equal(auth.status, Auth.STATUS.AUTHED); - clock.tick(1000 * 10000); - fakeFetch.returns(Promise.reject(new Error('random error'))); - const emitStub = sinon.stub(); - Gerrit.emit = emitStub; - auth.authCheck().then(authed2 => { - assert.isFalse(authed2); - assert.isTrue(emitStub.called); - assert.equal(auth.status, Auth.STATUS.ERROR); - done(); - }); - }); - }); - - test('no event from non-authed to other status', done => { - fakeFetch.returns(Promise.resolve({status: 403})); - auth.authCheck().then(authed => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - clock.tick(1000 * 10000); - fakeFetch.returns(Promise.resolve({status: 204})); - const emitStub = sinon.stub(); - Gerrit.emit = emitStub; - auth.authCheck().then(authed2 => { - assert.isTrue(authed2); - assert.isFalse(emitStub.called); - assert.equal(auth.status, Auth.STATUS.AUTHED); - done(); - }); - }); - }); - - test('no event from non-authed to other status', done => { - fakeFetch.returns(Promise.resolve({status: 403})); - auth.authCheck().then(authed => { - assert.isFalse(authed); - assert.equal(auth.status, Auth.STATUS.NOT_AUTHED); - clock.tick(1000 * 10000); - fakeFetch.returns(Promise.reject(new Error('random error'))); - const emitStub = sinon.stub(); - Gerrit.emit = emitStub; - auth.authCheck().then(authed2 => { - assert.isFalse(authed2); - assert.isFalse(emitStub.called); - assert.equal(auth.status, Auth.STATUS.ERROR); - done(); - }); - }); - }); - }); - - suite('default (xsrf token header)', () => { - setup(() => { - sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true})); - }); - - test('GET', done => { - auth.fetch('/url', {bar: 'bar'}).then(() => { - const [url, options] = fetch.lastCall.args; - assert.equal(url, '/url'); - assert.equal(options.credentials, 'same-origin'); - done(); - }); - }); - - test('POST', done => { - sandbox.stub(auth, '_getCookie') - .withArgs('XSRF_TOKEN') - .returns('foobar'); - auth.fetch('/url', {method: 'POST'}).then(() => { - const [url, options] = fetch.lastCall.args; - assert.equal(url, '/url'); - assert.equal(options.credentials, 'same-origin'); - assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar'); - done(); - }); - }); - }); - - suite('cors (access token)', () => { - setup(() => { - sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true})); - }); - - let getToken; - - const makeToken = opt_accessToken => { - return { - access_token: opt_accessToken || 'zbaz', - expires_at: new Date(Date.now() + 10e8).getTime(), - }; - }; - - setup(() => { - getToken = sandbox.stub(); - getToken.returns(Promise.resolve(makeToken())); - auth.setup(getToken); - }); - - test('base url support', done => { - const baseUrl = 'http://foo'; - sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl); - auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => { - const [url] = fetch.lastCall.args; - assert.equal(url, 'http://foo/a/url?access_token=zbaz'); - done(); - }); - }); - - test('fetch not signed in', done => { - getToken.returns(Promise.resolve()); - auth.fetch('/url', {bar: 'bar'}).then(() => { - const [url, options] = fetch.lastCall.args; - assert.equal(url, '/url'); - assert.equal(options.bar, 'bar'); - assert.equal(Object.keys(options.headers).length, 0); - done(); - }); - }); - - test('fetch signed in', done => { - auth.fetch('/url', {bar: 'bar'}).then(() => { - const [url, options] = fetch.lastCall.args; - assert.equal(url, '/a/url?access_token=zbaz'); - assert.equal(options.bar, 'bar'); - done(); - }); - }); - - test('getToken calls are cached', done => { - Promise.all([ - auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => { - assert.equal(getToken.callCount, 1); - done(); - }); - }); - - test('getToken refreshes token', done => { - sandbox.stub(auth, '_isTokenValid'); - auth._isTokenValid - .onFirstCall().returns(true) - .onSecondCall() - .returns(false) - .onThirdCall() - .returns(true); - auth.fetch('/url-one') - .then(() => { - getToken.returns(Promise.resolve(makeToken('bzzbb'))); - return auth.fetch('/url-two'); - }) - .then(() => { - const [[firstUrl], [secondUrl]] = fetch.args; - assert.equal(firstUrl, '/a/url-one?access_token=zbaz'); - assert.equal(secondUrl, '/a/url-two?access_token=bzzbb'); - done(); - }); - }); - - test('signed in token error falls back to anonymous', done => { - getToken.returns(Promise.resolve('rubbish')); - auth.fetch('/url', {bar: 'bar'}).then(() => { - const [url, options] = fetch.lastCall.args; - assert.equal(url, '/url'); - assert.equal(options.bar, 'bar'); - done(); - }); - }); - - test('_isTokenValid', () => { - assert.isFalse(auth._isTokenValid()); - assert.isFalse(auth._isTokenValid({})); - assert.isFalse(auth._isTokenValid({access_token: 'foo'})); - assert.isFalse(auth._isTokenValid({ - access_token: 'foo', - expires_at: Date.now()/1000 - 1, - })); - assert.isTrue(auth._isTokenValid({ - access_token: 'foo', - expires_at: Date.now()/1000 + 1, - })); - }); - - test('HTTP PUT with content type', done => { - const originalOptions = { - method: 'PUT', - headers: new Headers({'Content-Type': 'mail/pigeon'}), - }; - auth.fetch('/url', originalOptions).then(() => { - assert.isTrue(getToken.called); - const [url, options] = fetch.lastCall.args; - assert.include(url, '$ct=mail%2Fpigeon'); - assert.include(url, '$m=PUT'); - assert.include(url, 'access_token=zbaz'); - assert.equal(options.method, 'POST'); - assert.equal(options.headers.get('Content-Type'), 'text/plain'); - done(); - }); - }); - - test('HTTP PUT without content type', done => { - const originalOptions = { - method: 'PUT', - }; - auth.fetch('/url', originalOptions).then(() => { - assert.isTrue(getToken.called); - const [url, options] = fetch.lastCall.args; - assert.include(url, '$ct=text%2Fplain'); - assert.include(url, '$m=PUT'); - assert.include(url, 'access_token=zbaz'); - assert.equal(options.method, 'POST'); - assert.equal(options.headers.get('Content-Type'), 'text/plain'); done(); }); }); }); }); + + suite('default (xsrf token header)', () => { + setup(() => { + sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true})); + }); + + test('GET', done => { + auth.fetch('/url', {bar: 'bar'}).then(() => { + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/url'); + assert.equal(options.credentials, 'same-origin'); + done(); + }); + }); + + test('POST', done => { + sandbox.stub(auth, '_getCookie') + .withArgs('XSRF_TOKEN') + .returns('foobar'); + auth.fetch('/url', {method: 'POST'}).then(() => { + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/url'); + assert.equal(options.credentials, 'same-origin'); + assert.equal(options.headers.get('X-Gerrit-Auth'), 'foobar'); + done(); + }); + }); + }); + + suite('cors (access token)', () => { + setup(() => { + sandbox.stub(window, 'fetch').returns(Promise.resolve({ok: true})); + }); + + let getToken; + + const makeToken = opt_accessToken => { + return { + access_token: opt_accessToken || 'zbaz', + expires_at: new Date(Date.now() + 10e8).getTime(), + }; + }; + + setup(() => { + getToken = sandbox.stub(); + getToken.returns(Promise.resolve(makeToken())); + auth.setup(getToken); + }); + + test('base url support', done => { + const baseUrl = 'http://foo'; + sandbox.stub(Gerrit.BaseUrlBehavior, 'getBaseUrl').returns(baseUrl); + auth.fetch(baseUrl + '/url', {bar: 'bar'}).then(() => { + const [url] = fetch.lastCall.args; + assert.equal(url, 'http://foo/a/url?access_token=zbaz'); + done(); + }); + }); + + test('fetch not signed in', done => { + getToken.returns(Promise.resolve()); + auth.fetch('/url', {bar: 'bar'}).then(() => { + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/url'); + assert.equal(options.bar, 'bar'); + assert.equal(Object.keys(options.headers).length, 0); + done(); + }); + }); + + test('fetch signed in', done => { + auth.fetch('/url', {bar: 'bar'}).then(() => { + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/a/url?access_token=zbaz'); + assert.equal(options.bar, 'bar'); + done(); + }); + }); + + test('getToken calls are cached', done => { + Promise.all([ + auth.fetch('/url-one'), auth.fetch('/url-two')]).then(() => { + assert.equal(getToken.callCount, 1); + done(); + }); + }); + + test('getToken refreshes token', done => { + sandbox.stub(auth, '_isTokenValid'); + auth._isTokenValid + .onFirstCall().returns(true) + .onSecondCall() + .returns(false) + .onThirdCall() + .returns(true); + auth.fetch('/url-one') + .then(() => { + getToken.returns(Promise.resolve(makeToken('bzzbb'))); + return auth.fetch('/url-two'); + }) + .then(() => { + const [[firstUrl], [secondUrl]] = fetch.args; + assert.equal(firstUrl, '/a/url-one?access_token=zbaz'); + assert.equal(secondUrl, '/a/url-two?access_token=bzzbb'); + done(); + }); + }); + + test('signed in token error falls back to anonymous', done => { + getToken.returns(Promise.resolve('rubbish')); + auth.fetch('/url', {bar: 'bar'}).then(() => { + const [url, options] = fetch.lastCall.args; + assert.equal(url, '/url'); + assert.equal(options.bar, 'bar'); + done(); + }); + }); + + test('_isTokenValid', () => { + assert.isFalse(auth._isTokenValid()); + assert.isFalse(auth._isTokenValid({})); + assert.isFalse(auth._isTokenValid({access_token: 'foo'})); + assert.isFalse(auth._isTokenValid({ + access_token: 'foo', + expires_at: Date.now()/1000 - 1, + })); + assert.isTrue(auth._isTokenValid({ + access_token: 'foo', + expires_at: Date.now()/1000 + 1, + })); + }); + + test('HTTP PUT with content type', done => { + const originalOptions = { + method: 'PUT', + headers: new Headers({'Content-Type': 'mail/pigeon'}), + }; + auth.fetch('/url', originalOptions).then(() => { + assert.isTrue(getToken.called); + const [url, options] = fetch.lastCall.args; + assert.include(url, '$ct=mail%2Fpigeon'); + assert.include(url, '$m=PUT'); + assert.include(url, 'access_token=zbaz'); + assert.equal(options.method, 'POST'); + assert.equal(options.headers.get('Content-Type'), 'text/plain'); + done(); + }); + }); + + test('HTTP PUT without content type', done => { + const originalOptions = { + method: 'PUT', + }; + auth.fetch('/url', originalOptions).then(() => { + assert.isTrue(getToken.called); + const [url, options] = fetch.lastCall.args; + assert.include(url, '$ct=text%2Fplain'); + assert.include(url, '$m=PUT'); + assert.include(url, 'access_token=zbaz'); + assert.equal(options.method, 'POST'); + assert.equal(options.headers.get('Content-Type'), 'text/plain'); + done(); + }); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html deleted file mode 100644 index d3500d8..0000000 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.html +++ /dev/null
@@ -1,22 +0,0 @@ -<!-- -@license -Copyright (C) 2017 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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<dom-module id="gr-etag-decorator"> - <script src="gr-etag-decorator.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js index 7022d23..33c8d8e 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator.js
@@ -14,6 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import '../../../scripts/bundled-polymer.js'; + +const $_documentContainer = document.createElement('template'); + +$_documentContainer.innerHTML = `<dom-module id="gr-etag-decorator"> + +</dom-module>`; + +document.head.appendChild($_documentContainer.content); + (function(window) { 'use strict';
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html index 21cbe89e..3eae300 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-etag-decorator_test.html
@@ -19,83 +19,85 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-etag-decorator</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> -<link rel="import" href="./gr-etag-decorator.html"> +<script type="module" src="./gr-etag-decorator.js"></script> -<script> - suite('gr-etag-decorator', async () => { - await readyToTest(); - let etag; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-etag-decorator.js'; +suite('gr-etag-decorator', () => { + let etag; + let sandbox; - const fakeRequest = (opt_etag, opt_status) => { - const headers = new Headers(); - if (opt_etag) { - headers.set('etag', opt_etag); - } - const status = opt_status || 200; - return {ok: true, status, headers}; - }; + const fakeRequest = (opt_etag, opt_status) => { + const headers = new Headers(); + if (opt_etag) { + headers.set('etag', opt_etag); + } + const status = opt_status || 200; + return {ok: true, status, headers}; + }; - setup(() => { - sandbox = sinon.sandbox.create(); - etag = new GrEtagDecorator(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('exists', () => { - assert.isOk(etag); - }); - - test('works', () => { - etag.collect('/foo', fakeRequest('bar')); - const options = etag.getOptions('/foo'); - assert.strictEqual(options.headers.get('If-None-Match'), 'bar'); - }); - - test('updates etags', () => { - etag.collect('/foo', fakeRequest('bar')); - etag.collect('/foo', fakeRequest('baz')); - const options = etag.getOptions('/foo'); - assert.strictEqual(options.headers.get('If-None-Match'), 'baz'); - }); - - test('discards empty etags', () => { - etag.collect('/foo', fakeRequest('bar')); - etag.collect('/foo', fakeRequest()); - const options = etag.getOptions('/foo', {headers: new Headers()}); - assert.isNull(options.headers.get('If-None-Match')); - }); - - test('discards etags in order used', () => { - etag.collect('/foo', fakeRequest('bar')); - _.times(29, i => { - etag.collect('/qaz/' + i, fakeRequest('qaz')); - }); - let options = etag.getOptions('/foo'); - assert.strictEqual(options.headers.get('If-None-Match'), 'bar'); - etag.collect('/zaq', fakeRequest('zaq')); - options = etag.getOptions('/foo', {headers: new Headers()}); - assert.isNull(options.headers.get('If-None-Match')); - }); - - test('getCachedPayload', () => { - const payload = 'payload'; - etag.collect('/foo', fakeRequest('bar'), payload); - assert.strictEqual(etag.getCachedPayload('/foo'), payload); - etag.collect('/foo', fakeRequest('bar', 304), 'garbage'); - assert.strictEqual(etag.getCachedPayload('/foo'), payload); - etag.collect('/foo', fakeRequest('bar', 200), 'new payload'); - assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload'); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + etag = new GrEtagDecorator(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('exists', () => { + assert.isOk(etag); + }); + + test('works', () => { + etag.collect('/foo', fakeRequest('bar')); + const options = etag.getOptions('/foo'); + assert.strictEqual(options.headers.get('If-None-Match'), 'bar'); + }); + + test('updates etags', () => { + etag.collect('/foo', fakeRequest('bar')); + etag.collect('/foo', fakeRequest('baz')); + const options = etag.getOptions('/foo'); + assert.strictEqual(options.headers.get('If-None-Match'), 'baz'); + }); + + test('discards empty etags', () => { + etag.collect('/foo', fakeRequest('bar')); + etag.collect('/foo', fakeRequest()); + const options = etag.getOptions('/foo', {headers: new Headers()}); + assert.isNull(options.headers.get('If-None-Match')); + }); + + test('discards etags in order used', () => { + etag.collect('/foo', fakeRequest('bar')); + _.times(29, i => { + etag.collect('/qaz/' + i, fakeRequest('qaz')); + }); + let options = etag.getOptions('/foo'); + assert.strictEqual(options.headers.get('If-None-Match'), 'bar'); + etag.collect('/zaq', fakeRequest('zaq')); + options = etag.getOptions('/foo', {headers: new Headers()}); + assert.isNull(options.headers.get('If-None-Match')); + }); + + test('getCachedPayload', () => { + const payload = 'payload'; + etag.collect('/foo', fakeRequest('bar'), payload); + assert.strictEqual(etag.getCachedPayload('/foo'), payload); + etag.collect('/foo', fakeRequest('bar', 304), 'garbage'); + assert.strictEqual(etag.getCachedPayload('/foo'), payload); + etag.collect('/foo', fakeRequest('bar', 200), 'new payload'); + assert.strictEqual(etag.getCachedPayload('/foo'), 'new payload'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html deleted file mode 100644 index 2047f91..0000000 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.html +++ /dev/null
@@ -1,37 +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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<link rel="import" href="../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.html"> -<link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html"> -<link rel="import" href="gr-etag-decorator.html"> - -<!-- NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 --> -<script src="/bower_components/es6-promise/dist/es6-promise.min.js"></script> -<script src="/bower_components/fetch/fetch.js"></script> - -<!-- NB: Order is important, because of namespaced classes. --> -<script src="gr-rest-apis/gr-rest-api-helper.js"></script> -<script src="gr-auth.js"></script> -<script src="gr-reviewer-updates-parser.js"></script> - -<dom-module id="gr-rest-api-interface"> - <script src="gr-rest-api-interface.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js index 5b88d34..3e78bd3 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -14,2756 +14,2777 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +/* NB: es6-promise Needed for IE11 and fetch polyfill support, see Issue 4308 */ +/* NB: Order is important, because of namespaced classes. */ +/* + FIXME(polymer-modulizer): the above comments were extracted + from HTML and may be out of place here. Review them and + then delete this comment! +*/ +import '../../../scripts/bundled-polymer.js'; - const DiffViewMode = { - SIDE_BY_SIDE: 'SIDE_BY_SIDE', - UNIFIED: 'UNIFIED_DIFF', - }; - const JSON_PREFIX = ')]}\''; - const MAX_PROJECT_RESULTS = 25; - // This value is somewhat arbitrary and not based on research or calculations. - const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850; - const PARENT_PATCH_NUM = 'PARENT'; +import '../../../behaviors/base-url-behavior/base-url-behavior.js'; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; +import '../../../behaviors/gr-path-list-behavior/gr-path-list-behavior.js'; +import '../../../behaviors/rest-client-behavior/rest-client-behavior.js'; +import './gr-etag-decorator.js'; +import './gr-rest-apis/gr-rest-api-helper.js'; +import './gr-auth.js'; +import './gr-reviewer-updates-parser.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import 'es6-promise/lib/es6-promise.js'; +import 'whatwg-fetch/fetch.js'; - const Requests = { - SEND_DIFF_DRAFT: 'sendDiffDraft', - }; +const DiffViewMode = { + SIDE_BY_SIDE: 'SIDE_BY_SIDE', + UNIFIED: 'UNIFIED_DIFF', +}; +const JSON_PREFIX = ')]}\''; +const MAX_PROJECT_RESULTS = 25; +// This value is somewhat arbitrary and not based on research or calculations. +const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850; +const PARENT_PATCH_NUM = 'PARENT'; - const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE = - 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)'; - const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i; +const Requests = { + SEND_DIFF_DRAFT: 'sendDiffDraft', +}; - const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*'; - const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL + - '/revisions/*'; +const CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE = + 'Saving draft resulted in HTTP 200 (OK) but expected HTTP 201 (Created)'; +const HEADER_REPORTING_BLACKLIST = /^set-cookie$/i; + +const ANONYMIZED_CHANGE_BASE_URL = '/changes/*~*'; +const ANONYMIZED_REVISION_BASE_URL = ANONYMIZED_CHANGE_BASE_URL + + '/revisions/*'; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.PathListMixin + * @appliesMixin Gerrit.PatchSetMixin + * @appliesMixin Gerrit.RESTClientMixin + * @extends Polymer.Element + */ +class GrRestApiInterface extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.PathListBehavior, + Gerrit.PatchSetBehavior, + Gerrit.RESTClientBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get is() { return 'gr-rest-api-interface'; } + /** + * Fired when an server error occurs. + * + * @event server-error + */ /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.PathListMixin - * @appliesMixin Gerrit.PatchSetMixin - * @appliesMixin Gerrit.RESTClientMixin - * @extends Polymer.Element + * Fired when a network error occurs. + * + * @event network-error */ - class GrRestApiInterface extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.PathListBehavior, - Gerrit.PatchSetBehavior, - Gerrit.RESTClientBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-rest-api-interface'; } - /** - * Fired when an server error occurs. - * - * @event server-error - */ - /** - * Fired when a network error occurs. - * - * @event network-error - */ + /** + * Fired after an RPC completes. + * + * @event rpc-log + */ - /** - * Fired after an RPC completes. - * - * @event rpc-log - */ + constructor() { + super(); + this.JSON_PREFIX = JSON_PREFIX; + } - constructor() { - super(); - this.JSON_PREFIX = JSON_PREFIX; + static get properties() { + return { + _cache: { + type: Object, + value: new SiteBasedCache(), // Shared across instances. + }, + _sharedFetchPromises: { + type: Object, + value: new FetchPromisesCache(), // Shared across instances. + }, + _pendingRequests: { + type: Object, + value: {}, // Intentional to share the object across instances. + }, + _etags: { + type: Object, + value: new GrEtagDecorator(), // Share across instances. + }, + /** + * Used to maintain a mapping of changeNums to project names. + */ + _projectLookup: { + type: Object, + value: {}, // Intentional to share the object across instances. + }, + }; + } + + /** @override */ + created() { + super.created(); + this._auth = Gerrit.Auth; + this._initRestApiHelper(); + } + + _initRestApiHelper() { + if (this._restApiHelper) { + return; } - - static get properties() { - return { - _cache: { - type: Object, - value: new SiteBasedCache(), // Shared across instances. - }, - _sharedFetchPromises: { - type: Object, - value: new FetchPromisesCache(), // Shared across instances. - }, - _pendingRequests: { - type: Object, - value: {}, // Intentional to share the object across instances. - }, - _etags: { - type: Object, - value: new GrEtagDecorator(), // Share across instances. - }, - /** - * Used to maintain a mapping of changeNums to project names. - */ - _projectLookup: { - type: Object, - value: {}, // Intentional to share the object across instances. - }, - }; + if (this._cache && this._auth && this._sharedFetchPromises) { + this._restApiHelper = new GrRestApiHelper(this._cache, this._auth, + this._sharedFetchPromises, this); } + } - /** @override */ - created() { - super.created(); - this._auth = Gerrit.Auth; - this._initRestApiHelper(); - } + _fetchSharedCacheURL(req) { + // Cache is shared across instances + return this._restApiHelper.fetchCacheURL(req); + } - _initRestApiHelper() { - if (this._restApiHelper) { - return; - } - if (this._cache && this._auth && this._sharedFetchPromises) { - this._restApiHelper = new GrRestApiHelper(this._cache, this._auth, - this._sharedFetchPromises, this); - } - } + /** + * @param {!Object} response + * @return {?} + */ + getResponseObject(response) { + return this._restApiHelper.getResponseObject(response); + } - _fetchSharedCacheURL(req) { - // Cache is shared across instances - return this._restApiHelper.fetchCacheURL(req); - } - - /** - * @param {!Object} response - * @return {?} - */ - getResponseObject(response) { - return this._restApiHelper.getResponseObject(response); - } - - getConfig(noCache) { - if (!noCache) { - return this._fetchSharedCacheURL({ - url: '/config/server/info', - reportUrlAsIs: true, - }); - } - - return this._restApiHelper.fetchJSON({ + getConfig(noCache) { + if (!noCache) { + return this._fetchSharedCacheURL({ url: '/config/server/info', reportUrlAsIs: true, }); } - getRepo(repo, opt_errFn) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._fetchSharedCacheURL({ - url: '/projects/' + encodeURIComponent(repo), - errFn: opt_errFn, - anonymizedUrl: '/projects/*', - }); - } + return this._restApiHelper.fetchJSON({ + url: '/config/server/info', + reportUrlAsIs: true, + }); + } - getProjectConfig(repo, opt_errFn) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._fetchSharedCacheURL({ - url: '/projects/' + encodeURIComponent(repo) + '/config', - errFn: opt_errFn, - anonymizedUrl: '/projects/*/config', - }); - } + getRepo(repo, opt_errFn) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._fetchSharedCacheURL({ + url: '/projects/' + encodeURIComponent(repo), + errFn: opt_errFn, + anonymizedUrl: '/projects/*', + }); + } - getRepoAccess(repo) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._fetchSharedCacheURL({ - url: '/access/?project=' + encodeURIComponent(repo), - anonymizedUrl: '/access/?project=*', - }); - } + getProjectConfig(repo, opt_errFn) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._fetchSharedCacheURL({ + url: '/projects/' + encodeURIComponent(repo) + '/config', + errFn: opt_errFn, + anonymizedUrl: '/projects/*/config', + }); + } - getRepoDashboards(repo, opt_errFn) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._fetchSharedCacheURL({ - url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/dashboards?inherited', - }); - } + getRepoAccess(repo) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._fetchSharedCacheURL({ + url: '/access/?project=' + encodeURIComponent(repo), + anonymizedUrl: '/access/?project=*', + }); + } - saveRepoConfig(repo, config, opt_errFn) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const url = `/projects/${encodeURIComponent(repo)}/config`; - this._cache.delete(url); - return this._restApiHelper.send({ - method: 'PUT', - url, - body: config, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/config', - }); - } + getRepoDashboards(repo, opt_errFn) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._fetchSharedCacheURL({ + url: `/projects/${encodeURIComponent(repo)}/dashboards?inherited`, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/dashboards?inherited', + }); + } - runRepoGC(repo, opt_errFn) { - if (!repo) { return ''; } - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const encodeName = encodeURIComponent(repo); - return this._restApiHelper.send({ - method: 'POST', - url: `/projects/${encodeName}/gc`, - body: '', - errFn: opt_errFn, - anonymizedUrl: '/projects/*/gc', - }); - } + saveRepoConfig(repo, config, opt_errFn) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const url = `/projects/${encodeURIComponent(repo)}/config`; + this._cache.delete(url); + return this._restApiHelper.send({ + method: 'PUT', + url, + body: config, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/config', + }); + } - /** - * @param {?Object} config - * @param {function(?Response, string=)=} opt_errFn - */ - createRepo(config, opt_errFn) { - if (!config.name) { return ''; } - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const encodeName = encodeURIComponent(config.name); - return this._restApiHelper.send({ - method: 'PUT', - url: `/projects/${encodeName}`, - body: config, - errFn: opt_errFn, - anonymizedUrl: '/projects/*', - }); - } + runRepoGC(repo, opt_errFn) { + if (!repo) { return ''; } + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const encodeName = encodeURIComponent(repo); + return this._restApiHelper.send({ + method: 'POST', + url: `/projects/${encodeName}/gc`, + body: '', + errFn: opt_errFn, + anonymizedUrl: '/projects/*/gc', + }); + } - /** - * @param {?Object} config - * @param {function(?Response, string=)=} opt_errFn - */ - createGroup(config, opt_errFn) { - if (!config.name) { return ''; } - const encodeName = encodeURIComponent(config.name); - return this._restApiHelper.send({ - method: 'PUT', - url: `/groups/${encodeName}`, - body: config, - errFn: opt_errFn, - anonymizedUrl: '/groups/*', - }); - } + /** + * @param {?Object} config + * @param {function(?Response, string=)=} opt_errFn + */ + createRepo(config, opt_errFn) { + if (!config.name) { return ''; } + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const encodeName = encodeURIComponent(config.name); + return this._restApiHelper.send({ + method: 'PUT', + url: `/projects/${encodeName}`, + body: config, + errFn: opt_errFn, + anonymizedUrl: '/projects/*', + }); + } - getGroupConfig(group, opt_errFn) { - return this._restApiHelper.fetchJSON({ - url: `/groups/${encodeURIComponent(group)}/detail`, - errFn: opt_errFn, - anonymizedUrl: '/groups/*/detail', - }); - } + /** + * @param {?Object} config + * @param {function(?Response, string=)=} opt_errFn + */ + createGroup(config, opt_errFn) { + if (!config.name) { return ''; } + const encodeName = encodeURIComponent(config.name); + return this._restApiHelper.send({ + method: 'PUT', + url: `/groups/${encodeName}`, + body: config, + errFn: opt_errFn, + anonymizedUrl: '/groups/*', + }); + } - /** - * @param {string} repo - * @param {string} ref - * @param {function(?Response, string=)=} opt_errFn - */ - deleteRepoBranches(repo, ref, opt_errFn) { - if (!repo || !ref) { return ''; } - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const encodeName = encodeURIComponent(repo); - const encodeRef = encodeURIComponent(ref); - return this._restApiHelper.send({ - method: 'DELETE', - url: `/projects/${encodeName}/branches/${encodeRef}`, - body: '', - errFn: opt_errFn, - anonymizedUrl: '/projects/*/branches/*', - }); - } + getGroupConfig(group, opt_errFn) { + return this._restApiHelper.fetchJSON({ + url: `/groups/${encodeURIComponent(group)}/detail`, + errFn: opt_errFn, + anonymizedUrl: '/groups/*/detail', + }); + } - /** - * @param {string} repo - * @param {string} ref - * @param {function(?Response, string=)=} opt_errFn - */ - deleteRepoTags(repo, ref, opt_errFn) { - if (!repo || !ref) { return ''; } - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const encodeName = encodeURIComponent(repo); - const encodeRef = encodeURIComponent(ref); - return this._restApiHelper.send({ - method: 'DELETE', - url: `/projects/${encodeName}/tags/${encodeRef}`, - body: '', - errFn: opt_errFn, - anonymizedUrl: '/projects/*/tags/*', - }); - } + /** + * @param {string} repo + * @param {string} ref + * @param {function(?Response, string=)=} opt_errFn + */ + deleteRepoBranches(repo, ref, opt_errFn) { + if (!repo || !ref) { return ''; } + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const encodeName = encodeURIComponent(repo); + const encodeRef = encodeURIComponent(ref); + return this._restApiHelper.send({ + method: 'DELETE', + url: `/projects/${encodeName}/branches/${encodeRef}`, + body: '', + errFn: opt_errFn, + anonymizedUrl: '/projects/*/branches/*', + }); + } - /** - * @param {string} name - * @param {string} branch - * @param {string} revision - * @param {function(?Response, string=)=} opt_errFn - */ - createRepoBranch(name, branch, revision, opt_errFn) { - if (!name || !branch || !revision) { return ''; } - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const encodeName = encodeURIComponent(name); - const encodeBranch = encodeURIComponent(branch); - return this._restApiHelper.send({ - method: 'PUT', - url: `/projects/${encodeName}/branches/${encodeBranch}`, - body: revision, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/branches/*', - }); - } + /** + * @param {string} repo + * @param {string} ref + * @param {function(?Response, string=)=} opt_errFn + */ + deleteRepoTags(repo, ref, opt_errFn) { + if (!repo || !ref) { return ''; } + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const encodeName = encodeURIComponent(repo); + const encodeRef = encodeURIComponent(ref); + return this._restApiHelper.send({ + method: 'DELETE', + url: `/projects/${encodeName}/tags/${encodeRef}`, + body: '', + errFn: opt_errFn, + anonymizedUrl: '/projects/*/tags/*', + }); + } - /** - * @param {string} name - * @param {string} tag - * @param {string} revision - * @param {function(?Response, string=)=} opt_errFn - */ - createRepoTag(name, tag, revision, opt_errFn) { - if (!name || !tag || !revision) { return ''; } - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - const encodeName = encodeURIComponent(name); - const encodeTag = encodeURIComponent(tag); - return this._restApiHelper.send({ - method: 'PUT', - url: `/projects/${encodeName}/tags/${encodeTag}`, - body: revision, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/tags/*', - }); - } + /** + * @param {string} name + * @param {string} branch + * @param {string} revision + * @param {function(?Response, string=)=} opt_errFn + */ + createRepoBranch(name, branch, revision, opt_errFn) { + if (!name || !branch || !revision) { return ''; } + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const encodeName = encodeURIComponent(name); + const encodeBranch = encodeURIComponent(branch); + return this._restApiHelper.send({ + method: 'PUT', + url: `/projects/${encodeName}/branches/${encodeBranch}`, + body: revision, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/branches/*', + }); + } - /** - * @param {!string} groupName - * @returns {!Promise<boolean>} - */ - getIsGroupOwner(groupName) { - const encodeName = encodeURIComponent(groupName); - const req = { - url: `/groups/?owned&g=${encodeName}`, - anonymizedUrl: '/groups/owned&g=*', - }; - return this._fetchSharedCacheURL(req) - .then(configs => configs.hasOwnProperty(groupName)); - } + /** + * @param {string} name + * @param {string} tag + * @param {string} revision + * @param {function(?Response, string=)=} opt_errFn + */ + createRepoTag(name, tag, revision, opt_errFn) { + if (!name || !tag || !revision) { return ''; } + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + const encodeName = encodeURIComponent(name); + const encodeTag = encodeURIComponent(tag); + return this._restApiHelper.send({ + method: 'PUT', + url: `/projects/${encodeName}/tags/${encodeTag}`, + body: revision, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/tags/*', + }); + } - getGroupMembers(groupName, opt_errFn) { - const encodeName = encodeURIComponent(groupName); - return this._restApiHelper.fetchJSON({ - url: `/groups/${encodeName}/members/`, - errFn: opt_errFn, - anonymizedUrl: '/groups/*/members', - }); - } + /** + * @param {!string} groupName + * @returns {!Promise<boolean>} + */ + getIsGroupOwner(groupName) { + const encodeName = encodeURIComponent(groupName); + const req = { + url: `/groups/?owned&g=${encodeName}`, + anonymizedUrl: '/groups/owned&g=*', + }; + return this._fetchSharedCacheURL(req) + .then(configs => configs.hasOwnProperty(groupName)); + } - getIncludedGroup(groupName) { - return this._restApiHelper.fetchJSON({ - url: `/groups/${encodeURIComponent(groupName)}/groups/`, - anonymizedUrl: '/groups/*/groups', - }); - } + getGroupMembers(groupName, opt_errFn) { + const encodeName = encodeURIComponent(groupName); + return this._restApiHelper.fetchJSON({ + url: `/groups/${encodeName}/members/`, + errFn: opt_errFn, + anonymizedUrl: '/groups/*/members', + }); + } - saveGroupName(groupId, name) { - const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ - method: 'PUT', - url: `/groups/${encodeId}/name`, - body: {name}, - anonymizedUrl: '/groups/*/name', - }); - } + getIncludedGroup(groupName) { + return this._restApiHelper.fetchJSON({ + url: `/groups/${encodeURIComponent(groupName)}/groups/`, + anonymizedUrl: '/groups/*/groups', + }); + } - saveGroupOwner(groupId, ownerId) { - const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ - method: 'PUT', - url: `/groups/${encodeId}/owner`, - body: {owner: ownerId}, - anonymizedUrl: '/groups/*/owner', - }); - } + saveGroupName(groupId, name) { + const encodeId = encodeURIComponent(groupId); + return this._restApiHelper.send({ + method: 'PUT', + url: `/groups/${encodeId}/name`, + body: {name}, + anonymizedUrl: '/groups/*/name', + }); + } - saveGroupDescription(groupId, description) { - const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ - method: 'PUT', - url: `/groups/${encodeId}/description`, - body: {description}, - anonymizedUrl: '/groups/*/description', - }); - } + saveGroupOwner(groupId, ownerId) { + const encodeId = encodeURIComponent(groupId); + return this._restApiHelper.send({ + method: 'PUT', + url: `/groups/${encodeId}/owner`, + body: {owner: ownerId}, + anonymizedUrl: '/groups/*/owner', + }); + } - saveGroupOptions(groupId, options) { - const encodeId = encodeURIComponent(groupId); - return this._restApiHelper.send({ - method: 'PUT', - url: `/groups/${encodeId}/options`, - body: options, - anonymizedUrl: '/groups/*/options', - }); - } + saveGroupDescription(groupId, description) { + const encodeId = encodeURIComponent(groupId); + return this._restApiHelper.send({ + method: 'PUT', + url: `/groups/${encodeId}/description`, + body: {description}, + anonymizedUrl: '/groups/*/description', + }); + } - getGroupAuditLog(group, opt_errFn) { - return this._fetchSharedCacheURL({ - url: '/groups/' + group + '/log.audit', - errFn: opt_errFn, - anonymizedUrl: '/groups/*/log.audit', - }); - } + saveGroupOptions(groupId, options) { + const encodeId = encodeURIComponent(groupId); + return this._restApiHelper.send({ + method: 'PUT', + url: `/groups/${encodeId}/options`, + body: options, + anonymizedUrl: '/groups/*/options', + }); + } - saveGroupMembers(groupName, groupMembers) { - const encodeName = encodeURIComponent(groupName); - const encodeMember = encodeURIComponent(groupMembers); - return this._restApiHelper.send({ - method: 'PUT', - url: `/groups/${encodeName}/members/${encodeMember}`, - parseResponse: true, - anonymizedUrl: '/groups/*/members/*', - }); - } + getGroupAuditLog(group, opt_errFn) { + return this._fetchSharedCacheURL({ + url: '/groups/' + group + '/log.audit', + errFn: opt_errFn, + anonymizedUrl: '/groups/*/log.audit', + }); + } - saveIncludedGroup(groupName, includedGroup, opt_errFn) { - const encodeName = encodeURIComponent(groupName); - const encodeIncludedGroup = encodeURIComponent(includedGroup); - const req = { - method: 'PUT', - url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`, - errFn: opt_errFn, - anonymizedUrl: '/groups/*/groups/*', - }; - return this._restApiHelper.send(req).then(response => { - if (response.ok) { - return this.getResponseObject(response); - } - }); - } + saveGroupMembers(groupName, groupMembers) { + const encodeName = encodeURIComponent(groupName); + const encodeMember = encodeURIComponent(groupMembers); + return this._restApiHelper.send({ + method: 'PUT', + url: `/groups/${encodeName}/members/${encodeMember}`, + parseResponse: true, + anonymizedUrl: '/groups/*/members/*', + }); + } - deleteGroupMembers(groupName, groupMembers) { - const encodeName = encodeURIComponent(groupName); - const encodeMember = encodeURIComponent(groupMembers); - return this._restApiHelper.send({ - method: 'DELETE', - url: `/groups/${encodeName}/members/${encodeMember}`, - anonymizedUrl: '/groups/*/members/*', - }); - } - - deleteIncludedGroup(groupName, includedGroup) { - const encodeName = encodeURIComponent(groupName); - const encodeIncludedGroup = encodeURIComponent(includedGroup); - return this._restApiHelper.send({ - method: 'DELETE', - url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`, - anonymizedUrl: '/groups/*/groups/*', - }); - } - - getVersion() { - return this._fetchSharedCacheURL({ - url: '/config/server/version', - reportUrlAsIs: true, - }); - } - - getDiffPreferences() { - return this.getLoggedIn().then(loggedIn => { - if (loggedIn) { - return this._fetchSharedCacheURL({ - url: '/accounts/self/preferences.diff', - reportUrlAsIs: true, - }); - } - // These defaults should match the defaults in - // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java - // NOTE: There are some settings that don't apply to PolyGerrit - // (Render mode being at least one of them). - return Promise.resolve({ - auto_hide_diff_table_header: true, - context: 10, - cursor_blink_rate: 0, - font_size: 12, - ignore_whitespace: 'IGNORE_NONE', - intraline_difference: true, - line_length: 100, - line_wrapping: false, - show_line_endings: true, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - tab_size: 8, - theme: 'DEFAULT', - }); - }); - } - - getEditPreferences() { - return this.getLoggedIn().then(loggedIn => { - if (loggedIn) { - return this._fetchSharedCacheURL({ - url: '/accounts/self/preferences.edit', - reportUrlAsIs: true, - }); - } - // These defaults should match the defaults in - // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java - return Promise.resolve({ - auto_close_brackets: false, - cursor_blink_rate: 0, - hide_line_numbers: false, - hide_top_menu: false, - indent_unit: 2, - indent_with_tabs: false, - key_map_type: 'DEFAULT', - line_length: 100, - line_wrapping: false, - match_brackets: true, - show_base: false, - show_tabs: true, - show_whitespace_errors: true, - syntax_highlighting: true, - tab_size: 8, - theme: 'DEFAULT', - }); - }); - } - - /** - * @param {?Object} prefs - * @param {function(?Response, string=)=} opt_errFn - */ - savePreferences(prefs, opt_errFn) { - // Note (Issue 5142): normalize the download scheme with lower case before - // saving. - if (prefs.download_scheme) { - prefs.download_scheme = prefs.download_scheme.toLowerCase(); + saveIncludedGroup(groupName, includedGroup, opt_errFn) { + const encodeName = encodeURIComponent(groupName); + const encodeIncludedGroup = encodeURIComponent(includedGroup); + const req = { + method: 'PUT', + url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`, + errFn: opt_errFn, + anonymizedUrl: '/groups/*/groups/*', + }; + return this._restApiHelper.send(req).then(response => { + if (response.ok) { + return this.getResponseObject(response); } + }); + } - return this._restApiHelper.send({ - method: 'PUT', - url: '/accounts/self/preferences', - body: prefs, - errFn: opt_errFn, - reportUrlAsIs: true, + deleteGroupMembers(groupName, groupMembers) { + const encodeName = encodeURIComponent(groupName); + const encodeMember = encodeURIComponent(groupMembers); + return this._restApiHelper.send({ + method: 'DELETE', + url: `/groups/${encodeName}/members/${encodeMember}`, + anonymizedUrl: '/groups/*/members/*', + }); + } + + deleteIncludedGroup(groupName, includedGroup) { + const encodeName = encodeURIComponent(groupName); + const encodeIncludedGroup = encodeURIComponent(includedGroup); + return this._restApiHelper.send({ + method: 'DELETE', + url: `/groups/${encodeName}/groups/${encodeIncludedGroup}`, + anonymizedUrl: '/groups/*/groups/*', + }); + } + + getVersion() { + return this._fetchSharedCacheURL({ + url: '/config/server/version', + reportUrlAsIs: true, + }); + } + + getDiffPreferences() { + return this.getLoggedIn().then(loggedIn => { + if (loggedIn) { + return this._fetchSharedCacheURL({ + url: '/accounts/self/preferences.diff', + reportUrlAsIs: true, + }); + } + // These defaults should match the defaults in + // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java + // NOTE: There are some settings that don't apply to PolyGerrit + // (Render mode being at least one of them). + return Promise.resolve({ + auto_hide_diff_table_header: true, + context: 10, + cursor_blink_rate: 0, + font_size: 12, + ignore_whitespace: 'IGNORE_NONE', + intraline_difference: true, + line_length: 100, + line_wrapping: false, + show_line_endings: true, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + tab_size: 8, + theme: 'DEFAULT', }); + }); + } + + getEditPreferences() { + return this.getLoggedIn().then(loggedIn => { + if (loggedIn) { + return this._fetchSharedCacheURL({ + url: '/accounts/self/preferences.edit', + reportUrlAsIs: true, + }); + } + // These defaults should match the defaults in + // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java + return Promise.resolve({ + auto_close_brackets: false, + cursor_blink_rate: 0, + hide_line_numbers: false, + hide_top_menu: false, + indent_unit: 2, + indent_with_tabs: false, + key_map_type: 'DEFAULT', + line_length: 100, + line_wrapping: false, + match_brackets: true, + show_base: false, + show_tabs: true, + show_whitespace_errors: true, + syntax_highlighting: true, + tab_size: 8, + theme: 'DEFAULT', + }); + }); + } + + /** + * @param {?Object} prefs + * @param {function(?Response, string=)=} opt_errFn + */ + savePreferences(prefs, opt_errFn) { + // Note (Issue 5142): normalize the download scheme with lower case before + // saving. + if (prefs.download_scheme) { + prefs.download_scheme = prefs.download_scheme.toLowerCase(); } - /** - * @param {?Object} prefs - * @param {function(?Response, string=)=} opt_errFn - */ - saveDiffPreferences(prefs, opt_errFn) { - // Invalidate the cache. - this._cache.delete('/accounts/self/preferences.diff'); - return this._restApiHelper.send({ - method: 'PUT', - url: '/accounts/self/preferences.diff', - body: prefs, - errFn: opt_errFn, - reportUrlAsIs: true, - }); - } + return this._restApiHelper.send({ + method: 'PUT', + url: '/accounts/self/preferences', + body: prefs, + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } - /** - * @param {?Object} prefs - * @param {function(?Response, string=)=} opt_errFn - */ - saveEditPreferences(prefs, opt_errFn) { - // Invalidate the cache. - this._cache.delete('/accounts/self/preferences.edit'); - return this._restApiHelper.send({ - method: 'PUT', - url: '/accounts/self/preferences.edit', - body: prefs, - errFn: opt_errFn, - reportUrlAsIs: true, - }); - } + /** + * @param {?Object} prefs + * @param {function(?Response, string=)=} opt_errFn + */ + saveDiffPreferences(prefs, opt_errFn) { + // Invalidate the cache. + this._cache.delete('/accounts/self/preferences.diff'); + return this._restApiHelper.send({ + method: 'PUT', + url: '/accounts/self/preferences.diff', + body: prefs, + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } - getAccount() { - return this._fetchSharedCacheURL({ - url: '/accounts/self/detail', - reportUrlAsIs: true, - errFn: resp => { - if (!resp || resp.status === 403) { - this._cache.delete('/accounts/self/detail'); - } - }, - }); - } + /** + * @param {?Object} prefs + * @param {function(?Response, string=)=} opt_errFn + */ + saveEditPreferences(prefs, opt_errFn) { + // Invalidate the cache. + this._cache.delete('/accounts/self/preferences.edit'); + return this._restApiHelper.send({ + method: 'PUT', + url: '/accounts/self/preferences.edit', + body: prefs, + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } - getAvatarChangeUrl() { - return this._fetchSharedCacheURL({ - url: '/accounts/self/avatar.change.url', - reportUrlAsIs: true, - errFn: resp => { - if (!resp || resp.status === 403) { - this._cache.delete('/accounts/self/avatar.change.url'); - } - }, - }); - } - - getExternalIds() { - return this._restApiHelper.fetchJSON({ - url: '/accounts/self/external.ids', - reportUrlAsIs: true, - }); - } - - deleteAccountIdentity(id) { - return this._restApiHelper.send({ - method: 'POST', - url: '/accounts/self/external.ids:delete', - body: id, - parseResponse: true, - reportUrlAsIs: true, - }); - } - - /** - * @param {string} userId the ID of the user usch as an email address. - * @return {!Promise<!Object>} - */ - getAccountDetails(userId) { - return this._restApiHelper.fetchJSON({ - url: `/accounts/${encodeURIComponent(userId)}/detail`, - anonymizedUrl: '/accounts/*/detail', - }); - } - - getAccountEmails() { - return this._fetchSharedCacheURL({ - url: '/accounts/self/emails', - reportUrlAsIs: true, - }); - } - - /** - * @param {string} email - * @param {function(?Response, string=)=} opt_errFn - */ - addAccountEmail(email, opt_errFn) { - return this._restApiHelper.send({ - method: 'PUT', - url: '/accounts/self/emails/' + encodeURIComponent(email), - errFn: opt_errFn, - anonymizedUrl: '/account/self/emails/*', - }); - } - - /** - * @param {string} email - * @param {function(?Response, string=)=} opt_errFn - */ - deleteAccountEmail(email, opt_errFn) { - return this._restApiHelper.send({ - method: 'DELETE', - url: '/accounts/self/emails/' + encodeURIComponent(email), - errFn: opt_errFn, - anonymizedUrl: '/accounts/self/email/*', - }); - } - - /** - * @param {string} email - * @param {function(?Response, string=)=} opt_errFn - */ - setPreferredAccountEmail(email, opt_errFn) { - const encodedEmail = encodeURIComponent(email); - const req = { - method: 'PUT', - url: `/accounts/self/emails/${encodedEmail}/preferred`, - errFn: opt_errFn, - anonymizedUrl: '/accounts/self/emails/*/preferred', - }; - return this._restApiHelper.send(req).then(() => { - // If result of getAccountEmails is in cache, update it in the cache - // so we don't have to invalidate it. - const cachedEmails = this._cache.get('/accounts/self/emails'); - if (cachedEmails) { - const emails = cachedEmails.map(entry => { - if (entry.email === email) { - return {email, preferred: true}; - } else { - return {email}; - } - }); - this._cache.set('/accounts/self/emails', emails); + getAccount() { + return this._fetchSharedCacheURL({ + url: '/accounts/self/detail', + reportUrlAsIs: true, + errFn: resp => { + if (!resp || resp.status === 403) { + this._cache.delete('/accounts/self/detail'); } - }); - } + }, + }); + } - /** - * @param {?Object} obj - */ - _updateCachedAccount(obj) { - // If result of getAccount is in cache, update it in the cache + getAvatarChangeUrl() { + return this._fetchSharedCacheURL({ + url: '/accounts/self/avatar.change.url', + reportUrlAsIs: true, + errFn: resp => { + if (!resp || resp.status === 403) { + this._cache.delete('/accounts/self/avatar.change.url'); + } + }, + }); + } + + getExternalIds() { + return this._restApiHelper.fetchJSON({ + url: '/accounts/self/external.ids', + reportUrlAsIs: true, + }); + } + + deleteAccountIdentity(id) { + return this._restApiHelper.send({ + method: 'POST', + url: '/accounts/self/external.ids:delete', + body: id, + parseResponse: true, + reportUrlAsIs: true, + }); + } + + /** + * @param {string} userId the ID of the user usch as an email address. + * @return {!Promise<!Object>} + */ + getAccountDetails(userId) { + return this._restApiHelper.fetchJSON({ + url: `/accounts/${encodeURIComponent(userId)}/detail`, + anonymizedUrl: '/accounts/*/detail', + }); + } + + getAccountEmails() { + return this._fetchSharedCacheURL({ + url: '/accounts/self/emails', + reportUrlAsIs: true, + }); + } + + /** + * @param {string} email + * @param {function(?Response, string=)=} opt_errFn + */ + addAccountEmail(email, opt_errFn) { + return this._restApiHelper.send({ + method: 'PUT', + url: '/accounts/self/emails/' + encodeURIComponent(email), + errFn: opt_errFn, + anonymizedUrl: '/account/self/emails/*', + }); + } + + /** + * @param {string} email + * @param {function(?Response, string=)=} opt_errFn + */ + deleteAccountEmail(email, opt_errFn) { + return this._restApiHelper.send({ + method: 'DELETE', + url: '/accounts/self/emails/' + encodeURIComponent(email), + errFn: opt_errFn, + anonymizedUrl: '/accounts/self/email/*', + }); + } + + /** + * @param {string} email + * @param {function(?Response, string=)=} opt_errFn + */ + setPreferredAccountEmail(email, opt_errFn) { + const encodedEmail = encodeURIComponent(email); + const req = { + method: 'PUT', + url: `/accounts/self/emails/${encodedEmail}/preferred`, + errFn: opt_errFn, + anonymizedUrl: '/accounts/self/emails/*/preferred', + }; + return this._restApiHelper.send(req).then(() => { + // If result of getAccountEmails is in cache, update it in the cache // so we don't have to invalidate it. - const cachedAccount = this._cache.get('/accounts/self/detail'); - if (cachedAccount) { - // Replace object in cache with new object to force UI updates. - this._cache.set('/accounts/self/detail', - Object.assign({}, cachedAccount, obj)); - } - } - - /** - * @param {string} name - * @param {function(?Response, string=)=} opt_errFn - */ - setAccountName(name, opt_errFn) { - const req = { - method: 'PUT', - url: '/accounts/self/name', - body: {name}, - errFn: opt_errFn, - parseResponse: true, - reportUrlAsIs: true, - }; - return this._restApiHelper.send(req) - .then(newName => this._updateCachedAccount({name: newName})); - } - - /** - * @param {string} username - * @param {function(?Response, string=)=} opt_errFn - */ - setAccountUsername(username, opt_errFn) { - const req = { - method: 'PUT', - url: '/accounts/self/username', - body: {username}, - errFn: opt_errFn, - parseResponse: true, - reportUrlAsIs: true, - }; - return this._restApiHelper.send(req) - .then(newName => this._updateCachedAccount({username: newName})); - } - - /** - * @param {string} status - * @param {function(?Response, string=)=} opt_errFn - */ - setAccountStatus(status, opt_errFn) { - const req = { - method: 'PUT', - url: '/accounts/self/status', - body: {status}, - errFn: opt_errFn, - parseResponse: true, - reportUrlAsIs: true, - }; - return this._restApiHelper.send(req) - .then(newStatus => this._updateCachedAccount({status: newStatus})); - } - - getAccountStatus(userId) { - return this._restApiHelper.fetchJSON({ - url: `/accounts/${encodeURIComponent(userId)}/status`, - anonymizedUrl: '/accounts/*/status', - }); - } - - getAccountGroups() { - return this._restApiHelper.fetchJSON({ - url: '/accounts/self/groups', - reportUrlAsIs: true, - }); - } - - getAccountAgreements() { - return this._restApiHelper.fetchJSON({ - url: '/accounts/self/agreements', - reportUrlAsIs: true, - }); - } - - saveAccountAgreement(name) { - return this._restApiHelper.send({ - method: 'PUT', - url: '/accounts/self/agreements', - body: name, - reportUrlAsIs: true, - }); - } - - /** - * @param {string=} opt_params - */ - getAccountCapabilities(opt_params) { - let queryString = ''; - if (opt_params) { - queryString = '?q=' + opt_params - .map(param => encodeURIComponent(param)) - .join('&q='); - } - return this._fetchSharedCacheURL({ - url: '/accounts/self/capabilities' + queryString, - anonymizedUrl: '/accounts/self/capabilities?q=*', - }); - } - - getLoggedIn() { - return this._auth.authCheck(); - } - - getIsAdmin() { - return this.getLoggedIn() - .then(isLoggedIn => { - if (isLoggedIn) { - return this.getAccountCapabilities(); - } else { - return Promise.resolve(); - } - }) - .then( - capabilities => capabilities && capabilities.administrateServer - ); - } - - getDefaultPreferences() { - return this._fetchSharedCacheURL({ - url: '/config/server/preferences', - reportUrlAsIs: true, - }); - } - - getPreferences() { - return this.getLoggedIn().then(loggedIn => { - if (loggedIn) { - const req = {url: '/accounts/self/preferences', reportUrlAsIs: true}; - return this._fetchSharedCacheURL(req).then(res => { - if (this._isNarrowScreen()) { - // Note that this can be problematic, because the diff will stay - // unified even after increasing the window width. - res.default_diff_view = DiffViewMode.UNIFIED; - } else { - res.default_diff_view = res.diff_view; - } - return Promise.resolve(res); - }); - } - - return Promise.resolve({ - changes_per_page: 25, - default_diff_view: this._isNarrowScreen() ? - DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE, - diff_view: 'SIDE_BY_SIDE', - size_bar_in_change_table: true, - }); - }); - } - - getWatchedProjects() { - return this._fetchSharedCacheURL({ - url: '/accounts/self/watched.projects', - reportUrlAsIs: true, - }); - } - - /** - * @param {string} projects - * @param {function(?Response, string=)=} opt_errFn - */ - saveWatchedProjects(projects, opt_errFn) { - return this._restApiHelper.send({ - method: 'POST', - url: '/accounts/self/watched.projects', - body: projects, - errFn: opt_errFn, - parseResponse: true, - reportUrlAsIs: true, - }); - } - - /** - * @param {string} projects - * @param {function(?Response, string=)=} opt_errFn - */ - deleteWatchedProjects(projects, opt_errFn) { - return this._restApiHelper.send({ - method: 'POST', - url: '/accounts/self/watched.projects:delete', - body: projects, - errFn: opt_errFn, - reportUrlAsIs: true, - }); - } - - _isNarrowScreen() { - return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX; - } - - /** - * @param {number=} opt_changesPerPage - * @param {string|!Array<string>=} opt_query A query or an array of queries. - * @param {number|string=} opt_offset - * @param {!Object=} opt_options - * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an - * array, _fetchJSON will return an array of arrays of changeInfos. If it - * is unspecified or a string, _fetchJSON will return an array of - * changeInfos. - */ - getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) { - const options = opt_options || this.listChangesOptionsToHex( - this.ListChangesOption.LABELS, - this.ListChangesOption.DETAILED_ACCOUNTS - ); - // Issue 4524: respect legacy token with max sortkey. - if (opt_offset === 'n,z') { - opt_offset = 0; - } - const params = { - O: options, - S: opt_offset || 0, - }; - if (opt_changesPerPage) { params.n = opt_changesPerPage; } - if (opt_query && opt_query.length > 0) { - params.q = opt_query; - } - const iterateOverChanges = arr => { - for (const change of (arr || [])) { - this._maybeInsertInLookup(change); - } - }; - const req = { - url: '/changes/', - params, - reportUrlAsIs: true, - }; - return this._restApiHelper.fetchJSON(req).then(response => { - // Response may be an array of changes OR an array of arrays of - // changes. - if (opt_query instanceof Array) { - // Normalize the response to look like a multi-query response - // when there is only one query. - if (opt_query.length === 1) { - response = [response]; - } - for (const arr of response) { - iterateOverChanges(arr); - } - } else { - iterateOverChanges(response); - } - return response; - }); - } - - /** - * Inserts a change into _projectLookup iff it has a valid structure. - * - * @param {?{ _number: (number|string) }} change - */ - _maybeInsertInLookup(change) { - if (change && change.project && change._number) { - this.setInProjectLookup(change._number, change.project); - } - } - - /** - * TODO (beckysiegel) this needs to be rewritten with the optional param - * at the end. - * - * @param {number|string} changeNum - * @param {?number|string=} opt_patchNum passed as null sometimes. - * @param {?=} endpoint - * @return {!Promise<string>} - */ - getChangeActionURL(changeNum, opt_patchNum, endpoint) { - return this._changeBaseURL(changeNum, opt_patchNum) - .then(url => url + endpoint); - } - - /** - * @param {number|string} changeNum - * @param {function(?Response, string=)=} opt_errFn - * @param {function()=} opt_cancelCondition - */ - getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { - return this.getConfig(false).then(config => { - const optionsHex = this._getChangeOptionsHex(config); - return this._getChangeDetail( - changeNum, optionsHex, opt_errFn, opt_cancelCondition) - .then(GrReviewerUpdatesParser.parse); - }); - } - - _getChangeOptionsHex(config) { - if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage - && !(config.receive && config.receive.enable_signed_push)) { - return window.DEFAULT_DETAIL_HEXES.changePage; - } - - // This list MUST be kept in sync with - // ChangeIT#changeDetailsDoesNotRequireIndex - const options = [ - this.ListChangesOption.ALL_COMMITS, - this.ListChangesOption.ALL_REVISIONS, - this.ListChangesOption.CHANGE_ACTIONS, - this.ListChangesOption.DETAILED_LABELS, - this.ListChangesOption.DOWNLOAD_COMMANDS, - this.ListChangesOption.MESSAGES, - this.ListChangesOption.SUBMITTABLE, - this.ListChangesOption.WEB_LINKS, - this.ListChangesOption.SKIP_DIFFSTAT, - ]; - if (config.receive && config.receive.enable_signed_push) { - options.push(this.ListChangesOption.PUSH_CERTIFICATES); - } - return this.listChangesOptionsToHex(...options); - } - - /** - * @param {number|string} changeNum - * @param {function(?Response, string=)=} opt_errFn - * @param {function()=} opt_cancelCondition - */ - getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { - let optionsHex = ''; - if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) { - optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage; - } else { - optionsHex = this.listChangesOptionsToHex( - this.ListChangesOption.ALL_COMMITS, - this.ListChangesOption.ALL_REVISIONS, - this.ListChangesOption.SKIP_DIFFSTAT - ); - } - return this._getChangeDetail(changeNum, optionsHex, opt_errFn, - opt_cancelCondition); - } - - /** - * @param {number|string} changeNum - * @param {string|undefined} optionsHex list changes options in hex - * @param {function(?Response, string=)=} opt_errFn - * @param {function()=} opt_cancelCondition - */ - _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) { - return this.getChangeActionURL(changeNum, null, '/detail').then(url => { - const urlWithParams = this._restApiHelper - .urlWithParams(url, optionsHex); - const params = {O: optionsHex}; - const req = { - url, - errFn: opt_errFn, - cancelCondition: opt_cancelCondition, - params, - fetchOptions: this._etags.getOptions(urlWithParams), - anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex, - }; - return this._restApiHelper.fetchRawJSON(req).then(response => { - if (response && response.status === 304) { - return Promise.resolve(this._restApiHelper.parsePrefixedJSON( - this._etags.getCachedPayload(urlWithParams))); - } - - if (response && !response.ok) { - if (opt_errFn) { - opt_errFn.call(null, response); - } else { - this.fire('server-error', {request: req, response}); - } - return; - } - - const payloadPromise = response ? - this._restApiHelper.readResponsePayload(response) : - Promise.resolve(null); - - return payloadPromise.then(payload => { - if (!payload) { return null; } - this._etags.collect(urlWithParams, response, payload.raw); - this._maybeInsertInLookup(payload.parsed); - - return payload.parsed; - }); - }); - }); - } - - /** - * @param {number|string} changeNum - * @param {number|string} patchNum - */ - getChangeCommitInfo(changeNum, patchNum) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/commit?links', - patchNum, - reportEndpointAsIs: true, - }); - } - - /** - * @param {number|string} changeNum - * @param {Gerrit.PatchRange} patchRange - * @param {number=} opt_parentIndex - */ - getChangeFiles(changeNum, patchRange, opt_parentIndex) { - let params = undefined; - if (this.isMergeParent(patchRange.basePatchNum)) { - params = {parent: this.getParentIndex(patchRange.basePatchNum)}; - } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) { - params = {base: patchRange.basePatchNum}; - } - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/files', - patchNum: patchRange.patchNum, - params, - reportEndpointAsIs: true, - }); - } - - /** - * @param {number|string} changeNum - * @param {Gerrit.PatchRange} patchRange - */ - getChangeEditFiles(changeNum, patchRange) { - let endpoint = '/edit?list'; - let anonymizedEndpoint = endpoint; - if (patchRange.basePatchNum !== 'PARENT') { - endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + ''); - anonymizedEndpoint += '&base=*'; - } - return this._getChangeURLAndFetch({ - changeNum, - endpoint, - anonymizedEndpoint, - }); - } - - /** - * @param {number|string} changeNum - * @param {number|string} patchNum - * @param {string} query - * @return {!Promise<!Object>} - */ - queryChangeFiles(changeNum, patchNum, query) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: `/files?q=${encodeURIComponent(query)}`, - patchNum, - anonymizedEndpoint: '/files?q=*', - }); - } - - /** - * @param {number|string} changeNum - * @param {Gerrit.PatchRange} patchRange - * @return {!Promise<!Array<!Object>>} - */ - getChangeOrEditFiles(changeNum, patchRange) { - if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) { - return this.getChangeEditFiles(changeNum, patchRange).then(res => - res.files); - } - return this.getChangeFiles(changeNum, patchRange); - } - - getChangeRevisionActions(changeNum, patchNum) { - const req = { - changeNum, - endpoint: '/actions', - patchNum, - reportEndpointAsIs: true, - }; - return this._getChangeURLAndFetch(req).then(revisionActions => { - // The rebase button on change screen is always enabled. - if (revisionActions.rebase) { - revisionActions.rebase.rebaseOnCurrent = - !!revisionActions.rebase.enabled; - revisionActions.rebase.enabled = true; - } - return revisionActions; - }); - } - - /** - * @param {number|string} changeNum - * @param {string} inputVal - * @param {function(?Response, string=)=} opt_errFn - */ - getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) { - return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal, - opt_errFn); - } - - /** - * @param {number|string} changeNum - * @param {string} inputVal - * @param {function(?Response, string=)=} opt_errFn - */ - getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) { - return this._getChangeSuggestedGroup('CC', changeNum, inputVal, - opt_errFn); - } - - _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) { - // More suggestions may obscure content underneath in the reply dialog, - // see issue 10793. - const params = {'n': 6, 'reviewer-state': reviewerState}; - if (inputVal) { params.q = inputVal; } - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/suggest_reviewers', - errFn: opt_errFn, - params, - reportEndpointAsIs: true, - }); - } - - /** - * @param {number|string} changeNum - */ - getChangeIncludedIn(changeNum) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/in', - reportEndpointAsIs: true, - }); - } - - _computeFilter(filter) { - if (filter && filter.startsWith('^')) { - filter = '&r=' + encodeURIComponent(filter); - } else if (filter) { - filter = '&m=' + encodeURIComponent(filter); - } else { - filter = ''; - } - return filter; - } - - /** - * @param {string} filter - * @param {number} groupsPerPage - * @param {number=} opt_offset - */ - _getGroupsUrl(filter, groupsPerPage, opt_offset) { - const offset = opt_offset || 0; - - return `/groups/?n=${groupsPerPage + 1}&S=${offset}` + - this._computeFilter(filter); - } - - /** - * @param {string} filter - * @param {number} reposPerPage - * @param {number=} opt_offset - */ - _getReposUrl(filter, reposPerPage, opt_offset) { - const defaultFilter = 'state:active OR state:read-only'; - const namePartDelimiters = /[@.\-\s\/_]/g; - const offset = opt_offset || 0; - - if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) { - // The query language specifies hyphens as operators. Split the string - // by hyphens and 'AND' the parts together as 'inname:' queries. - // If the filter includes a semicolon, the user is using a more complex - // query so we trust them and don't do any magic under the hood. - const originalFilter = filter; - filter = ''; - originalFilter.split(namePartDelimiters).forEach(part => { - if (part) { - filter += (filter === '' ? 'inname:' : ' AND inname:') + part; + const cachedEmails = this._cache.get('/accounts/self/emails'); + if (cachedEmails) { + const emails = cachedEmails.map(entry => { + if (entry.email === email) { + return {email, preferred: true}; + } else { + return {email}; } }); + this._cache.set('/accounts/self/emails', emails); } - // Check if filter is now empty which could be either because the user did - // not provide it or because the user provided only a split character. - if (!filter) { - filter = defaultFilter; - } + }); + } - filter = filter.trim(); - const encodedFilter = encodeURIComponent(filter); - - return `/projects/?n=${reposPerPage + 1}&S=${offset}` + - `&query=${encodedFilter}`; - } - - invalidateGroupsCache() { - this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?'); - } - - invalidateReposCache() { - this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?'); - } - - invalidateAccountsCache() { - this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/'); - } - - /** - * @param {string} filter - * @param {number} groupsPerPage - * @param {number=} opt_offset - * @return {!Promise<?Object>} - */ - getGroups(filter, groupsPerPage, opt_offset) { - const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset); - - return this._fetchSharedCacheURL({ - url, - anonymizedUrl: '/groups/?*', - }); - } - - /** - * @param {string} filter - * @param {number} reposPerPage - * @param {number=} opt_offset - * @return {!Promise<?Object>} - */ - getRepos(filter, reposPerPage, opt_offset) { - const url = this._getReposUrl(filter, reposPerPage, opt_offset); - - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._fetchSharedCacheURL({ - url, - anonymizedUrl: '/projects/?*', - }); - } - - setRepoHead(repo, ref) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._restApiHelper.send({ - method: 'PUT', - url: `/projects/${encodeURIComponent(repo)}/HEAD`, - body: {ref}, - anonymizedUrl: '/projects/*/HEAD', - }); - } - - /** - * @param {string} filter - * @param {string} repo - * @param {number} reposBranchesPerPage - * @param {number=} opt_offset - * @param {?function(?Response, string=)=} opt_errFn - * @return {!Promise<?Object>} - */ - getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) { - const offset = opt_offset || 0; - const count = reposBranchesPerPage + 1; - filter = this._computeFilter(filter); - repo = encodeURIComponent(repo); - const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`; - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._restApiHelper.fetchJSON({ - url, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/branches?*', - }); - } - - /** - * @param {string} filter - * @param {string} repo - * @param {number} reposTagsPerPage - * @param {number=} opt_offset - * @param {?function(?Response, string=)=} opt_errFn - * @return {!Promise<?Object>} - */ - getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) { - const offset = opt_offset || 0; - const encodedRepo = encodeURIComponent(repo); - const n = reposTagsPerPage + 1; - const encodedFilter = this._computeFilter(filter); - const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` + - encodedFilter; - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._restApiHelper.fetchJSON({ - url, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/tags', - }); - } - - /** - * @param {string} filter - * @param {number} pluginsPerPage - * @param {number=} opt_offset - * @param {?function(?Response, string=)=} opt_errFn - * @return {!Promise<?Object>} - */ - getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) { - const offset = opt_offset || 0; - const encodedFilter = this._computeFilter(filter); - const n = pluginsPerPage + 1; - const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`; - return this._restApiHelper.fetchJSON({ - url, - errFn: opt_errFn, - anonymizedUrl: '/plugins/?all', - }); - } - - getRepoAccessRights(repoName, opt_errFn) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._restApiHelper.fetchJSON({ - url: `/projects/${encodeURIComponent(repoName)}/access`, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/access', - }); - } - - setRepoAccessRights(repoName, repoInfo) { - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._restApiHelper.send({ - method: 'POST', - url: `/projects/${encodeURIComponent(repoName)}/access`, - body: repoInfo, - anonymizedUrl: '/projects/*/access', - }); - } - - setRepoAccessRightsForReview(projectName, projectInfo) { - return this._restApiHelper.send({ - method: 'PUT', - url: `/projects/${encodeURIComponent(projectName)}/access:review`, - body: projectInfo, - parseResponse: true, - anonymizedUrl: '/projects/*/access:review', - }); - } - - /** - * @param {string} inputVal - * @param {number} opt_n - * @param {function(?Response, string=)=} opt_errFn - */ - getSuggestedGroups(inputVal, opt_n, opt_errFn) { - const params = {s: inputVal}; - if (opt_n) { params.n = opt_n; } - return this._restApiHelper.fetchJSON({ - url: '/groups/', - errFn: opt_errFn, - params, - reportUrlAsIs: true, - }); - } - - /** - * @param {string} inputVal - * @param {number} opt_n - * @param {function(?Response, string=)=} opt_errFn - */ - getSuggestedProjects(inputVal, opt_n, opt_errFn) { - const params = { - m: inputVal, - n: MAX_PROJECT_RESULTS, - type: 'ALL', - }; - if (opt_n) { params.n = opt_n; } - return this._restApiHelper.fetchJSON({ - url: '/projects/', - errFn: opt_errFn, - params, - reportUrlAsIs: true, - }); - } - - /** - * @param {string} inputVal - * @param {number} opt_n - * @param {function(?Response, string=)=} opt_errFn - */ - getSuggestedAccounts(inputVal, opt_n, opt_errFn) { - if (!inputVal) { - return Promise.resolve([]); - } - const params = {suggest: null, q: inputVal}; - if (opt_n) { params.n = opt_n; } - return this._restApiHelper.fetchJSON({ - url: '/accounts/', - errFn: opt_errFn, - params, - anonymizedUrl: '/accounts/?n=*', - }); - } - - addChangeReviewer(changeNum, reviewerID) { - return this._sendChangeReviewerRequest('POST', changeNum, reviewerID); - } - - removeChangeReviewer(changeNum, reviewerID) { - return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID); - } - - _sendChangeReviewerRequest(method, changeNum, reviewerID) { - return this.getChangeActionURL(changeNum, null, '/reviewers') - .then(url => { - let body; - switch (method) { - case 'POST': - body = {reviewer: reviewerID}; - break; - case 'DELETE': - url += '/' + encodeURIComponent(reviewerID); - break; - default: - throw Error('Unsupported HTTP method: ' + method); - } - - return this._restApiHelper.send({method, url, body}); - }); - } - - getRelatedChanges(changeNum, patchNum) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/related', - patchNum, - reportEndpointAsIs: true, - }); - } - - getChangesSubmittedTogether(changeNum) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES', - reportEndpointAsIs: true, - }); - } - - getChangeConflicts(changeNum) { - const options = this.listChangesOptionsToHex( - this.ListChangesOption.CURRENT_REVISION, - this.ListChangesOption.CURRENT_COMMIT - ); - const params = { - O: options, - q: 'status:open conflicts:' + changeNum, - }; - return this._restApiHelper.fetchJSON({ - url: '/changes/', - params, - anonymizedUrl: '/changes/conflicts:*', - }); - } - - getChangeCherryPicks(project, changeID, changeNum) { - const options = this.listChangesOptionsToHex( - this.ListChangesOption.CURRENT_REVISION, - this.ListChangesOption.CURRENT_COMMIT - ); - const query = [ - 'project:' + project, - 'change:' + changeID, - '-change:' + changeNum, - '-is:abandoned', - ].join(' '); - const params = { - O: options, - q: query, - }; - return this._restApiHelper.fetchJSON({ - url: '/changes/', - params, - anonymizedUrl: '/changes/change:*', - }); - } - - getChangesWithSameTopic(topic, changeNum) { - const options = this.listChangesOptionsToHex( - this.ListChangesOption.LABELS, - this.ListChangesOption.CURRENT_REVISION, - this.ListChangesOption.CURRENT_COMMIT, - this.ListChangesOption.DETAILED_LABELS - ); - const query = [ - 'status:open', - '-change:' + changeNum, - `topic:"${topic}"`, - ].join(' '); - const params = { - O: options, - q: query, - }; - return this._restApiHelper.fetchJSON({ - url: '/changes/', - params, - anonymizedUrl: '/changes/topic:*', - }); - } - - getReviewedFiles(changeNum, patchNum) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/files?reviewed', - patchNum, - reportEndpointAsIs: true, - }); - } - - /** - * @param {number|string} changeNum - * @param {number|string} patchNum - * @param {string} path - * @param {boolean} reviewed - * @param {function(?Response, string=)=} opt_errFn - */ - saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) { - return this._getChangeURLAndSend({ - changeNum, - method: reviewed ? 'PUT' : 'DELETE', - patchNum, - endpoint: `/files/${encodeURIComponent(path)}/reviewed`, - errFn: opt_errFn, - anonymizedEndpoint: '/files/*/reviewed', - }); - } - - /** - * @param {number|string} changeNum - * @param {number|string} patchNum - * @param {!Object} review - * @param {function(?Response, string=)=} opt_errFn - */ - saveChangeReview(changeNum, patchNum, review, opt_errFn) { - const promises = [ - this.awaitPendingDiffDrafts(), - this.getChangeActionURL(changeNum, patchNum, '/review'), - ]; - return Promise.all(promises).then(([, url]) => this._restApiHelper.send({ - method: 'POST', - url, - body: review, - errFn: opt_errFn, - })); - } - - getChangeEdit(changeNum, opt_download_commands) { - const params = opt_download_commands ? {'download-commands': true} : null; - return this.getLoggedIn().then(loggedIn => { - if (!loggedIn) { return false; } - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/edit/', - params, - reportEndpointAsIs: true, - }, true); - }); - } - - /** - * @param {string} project - * @param {string} branch - * @param {string} subject - * @param {string=} opt_topic - * @param {boolean=} opt_isPrivate - * @param {boolean=} opt_workInProgress - * @param {string=} opt_baseChange - * @param {string=} opt_baseCommit - */ - createChange(project, branch, subject, opt_topic, opt_isPrivate, - opt_workInProgress, opt_baseChange, opt_baseCommit) { - return this._restApiHelper.send({ - method: 'POST', - url: '/changes/', - body: { - project, - branch, - subject, - topic: opt_topic, - is_private: opt_isPrivate, - work_in_progress: opt_workInProgress, - base_change: opt_baseChange, - base_commit: opt_baseCommit, - }, - parseResponse: true, - reportUrlAsIs: true, - }); - } - - /** - * @param {number|string} changeNum - * @param {string} path - * @param {number|string} patchNum - */ - getFileContent(changeNum, path, patchNum) { - // 404s indicate the file does not exist yet in the revision, so suppress - // them. - const suppress404s = res => { - if (res && res.status !== 404) { this.fire('server-error', {res}); } - return res; - }; - const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ? - this._getFileInChangeEdit(changeNum, path) : - this._getFileInRevision(changeNum, path, patchNum, suppress404s); - - return promise.then(res => { - if (!res.ok) { return res; } - - // The file type (used for syntax highlighting) is identified in the - // X-FYI-Content-Type header of the response. - const type = res.headers.get('X-FYI-Content-Type'); - return this.getResponseObject(res).then(content => { - return {content, type, ok: true}; - }); - }); - } - - /** - * Gets a file in a specific change and revision. - * - * @param {number|string} changeNum - * @param {string} path - * @param {number|string} patchNum - * @param {?function(?Response, string=)=} opt_errFn - */ - _getFileInRevision(changeNum, path, patchNum, opt_errFn) { - return this._getChangeURLAndSend({ - changeNum, - method: 'GET', - patchNum, - endpoint: `/files/${encodeURIComponent(path)}/content`, - errFn: opt_errFn, - headers: {Accept: 'application/json'}, - anonymizedEndpoint: '/files/*/content', - }); - } - - /** - * Gets a file in a change edit. - * - * @param {number|string} changeNum - * @param {string} path - */ - _getFileInChangeEdit(changeNum, path) { - return this._getChangeURLAndSend({ - changeNum, - method: 'GET', - endpoint: '/edit/' + encodeURIComponent(path), - headers: {Accept: 'application/json'}, - anonymizedEndpoint: '/edit/*', - }); - } - - rebaseChangeEdit(changeNum) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - endpoint: '/edit:rebase', - reportEndpointAsIs: true, - }); - } - - deleteChangeEdit(changeNum) { - return this._getChangeURLAndSend({ - changeNum, - method: 'DELETE', - endpoint: '/edit', - reportEndpointAsIs: true, - }); - } - - restoreFileInChangeEdit(changeNum, restore_path) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - endpoint: '/edit', - body: {restore_path}, - reportEndpointAsIs: true, - }); - } - - renameFileInChangeEdit(changeNum, old_path, new_path) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - endpoint: '/edit', - body: {old_path, new_path}, - reportEndpointAsIs: true, - }); - } - - deleteFileInChangeEdit(changeNum, path) { - return this._getChangeURLAndSend({ - changeNum, - method: 'DELETE', - endpoint: '/edit/' + encodeURIComponent(path), - anonymizedEndpoint: '/edit/*', - }); - } - - saveChangeEdit(changeNum, path, contents) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', - endpoint: '/edit/' + encodeURIComponent(path), - body: contents, - contentType: 'text/plain', - anonymizedEndpoint: '/edit/*', - }); - } - - getRobotCommentFixPreview(changeNum, patchNum, fixId) { - return this._getChangeURLAndFetch({ - changeNum, - patchNum, - endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`, - reportEndpointAsId: true, - }); - } - - applyFixSuggestion(changeNum, patchNum, fixId) { - return this._getChangeURLAndSend({ - method: 'POST', - changeNum, - patchNum, - endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`, - reportEndpointAsId: true, - }); - } - - // Deprecated, prefer to use putChangeCommitMessage instead. - saveChangeCommitMessageEdit(changeNum, message) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', - endpoint: '/edit:message', - body: {message}, - reportEndpointAsIs: true, - }); - } - - publishChangeEdit(changeNum) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - endpoint: '/edit:publish', - reportEndpointAsIs: true, - }); - } - - putChangeCommitMessage(changeNum, message) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', - endpoint: '/message', - body: {message}, - reportEndpointAsIs: true, - }); - } - - deleteChangeCommitMessage(changeNum, messageId) { - return this._getChangeURLAndSend({ - changeNum, - method: 'DELETE', - endpoint: '/messages/' + messageId, - reportEndpointAsIs: true, - }); - } - - saveChangeStarred(changeNum, starred) { - // Some servers may require the project name to be provided - // alongside the change number, so resolve the project name - // first. - return this.getFromProjectLookup(changeNum).then(project => { - const url = '/accounts/self/starred.changes/' + - (project ? encodeURIComponent(project) + '~' : '') + changeNum; - return this._restApiHelper.send({ - method: starred ? 'PUT' : 'DELETE', - url, - anonymizedUrl: '/accounts/self/starred.changes/*', - }); - }); - } - - saveChangeReviewed(changeNum, reviewed) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', - endpoint: reviewed ? '/reviewed' : '/unreviewed', - }); - } - - /** - * Public version of the _restApiHelper.send method preserved for plugins. - * - * @param {string} method - * @param {string} url - * @param {?string|number|Object=} opt_body passed as null sometimes - * and also apparently a number. TODO (beckysiegel) remove need for - * number at least. - * @param {?function(?Response, string=)=} opt_errFn - * passed as null sometimes. - * @param {?string=} opt_contentType - * @param {Object=} opt_headers - */ - send(method, url, opt_body, opt_errFn, opt_contentType, - opt_headers) { - return this._restApiHelper.send({ - method, - url, - body: opt_body, - errFn: opt_errFn, - contentType: opt_contentType, - headers: opt_headers, - }); - } - - /** - * @param {number|string} changeNum - * @param {number|string} basePatchNum Negative values specify merge parent - * index. - * @param {number|string} patchNum - * @param {string} path - * @param {string=} opt_whitespace the ignore-whitespace level for the diff - * algorithm. - * @param {function(?Response, string=)=} opt_errFn - */ - getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace, - opt_errFn) { - const params = { - context: 'ALL', - intraline: null, - whitespace: opt_whitespace || 'IGNORE_NONE', - }; - if (this.isMergeParent(basePatchNum)) { - params.parent = this.getParentIndex(basePatchNum); - } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) { - params.base = basePatchNum; - } - const endpoint = `/files/${encodeURIComponent(path)}/diff`; - const req = { - changeNum, - endpoint, - patchNum, - errFn: opt_errFn, - params, - anonymizedEndpoint: '/files/*/diff', - }; - - // Invalidate the cache if its edit patch to make sure we always get latest. - if (patchNum === this.EDIT_NAME) { - if (!req.fetchOptions) req.fetchOptions = {}; - if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers(); - req.fetchOptions.headers.append('Cache-Control', 'no-cache'); - } - - return this._getChangeURLAndFetch(req); - } - - /** - * @param {number|string} changeNum - * @param {number|string=} opt_basePatchNum - * @param {number|string=} opt_patchNum - * @param {string=} opt_path - * @return {!Promise<!Object>} - */ - getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { - return this._getDiffComments(changeNum, '/comments', opt_basePatchNum, - opt_patchNum, opt_path); - } - - /** - * @param {number|string} changeNum - * @param {number|string=} opt_basePatchNum - * @param {number|string=} opt_patchNum - * @param {string=} opt_path - * @return {!Promise<!Object>} - */ - getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { - return this._getDiffComments(changeNum, '/robotcomments', - opt_basePatchNum, opt_patchNum, opt_path); - } - - /** - * If the user is logged in, fetch the user's draft diff comments. If there - * is no logged in user, the request is not made and the promise yields an - * empty object. - * - * @param {number|string} changeNum - * @param {number|string=} opt_basePatchNum - * @param {number|string=} opt_patchNum - * @param {string=} opt_path - * @return {!Promise<!Object>} - */ - getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { - return this.getLoggedIn().then(loggedIn => { - if (!loggedIn) { return Promise.resolve({}); } - return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum, - opt_patchNum, opt_path); - }); - } - - _setRange(comments, comment) { - if (comment.in_reply_to && !comment.range) { - for (let i = 0; i < comments.length; i++) { - if (comments[i].id === comment.in_reply_to) { - comment.range = comments[i].range; - break; - } - } - } - return comment; - } - - _setRanges(comments) { - comments = comments || []; - comments.sort( - (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated) - ); - for (const comment of comments) { - this._setRange(comments, comment); - } - return comments; - } - - /** - * @param {number|string} changeNum - * @param {string} endpoint - * @param {number|string=} opt_basePatchNum - * @param {number|string=} opt_patchNum - * @param {string=} opt_path - * @return {!Promise<!Object>} - */ - _getDiffComments(changeNum, endpoint, opt_basePatchNum, - opt_patchNum, opt_path) { - /** - * Fetches the comments for a given patchNum. - * Helper function to make promises more legible. - * - * @param {string|number=} opt_patchNum - * @return {!Promise<!Object>} Diff comments response. - */ - // We don't want to add accept header, since preloading of comments is - // working only without accept header. - const noAcceptHeader = true; - const fetchComments = opt_patchNum => this._getChangeURLAndFetch({ - changeNum, - endpoint, - patchNum: opt_patchNum, - reportEndpointAsIs: true, - }, noAcceptHeader); - - if (!opt_basePatchNum && !opt_patchNum && !opt_path) { - return fetchComments(); - } - function onlyParent(c) { return c.side == PARENT_PATCH_NUM; } - function withoutParent(c) { return c.side != PARENT_PATCH_NUM; } - function setPath(c) { c.path = opt_path; } - - const promises = []; - let comments; - let baseComments; - let fetchPromise; - fetchPromise = fetchComments(opt_patchNum).then(response => { - comments = response[opt_path] || []; - // TODO(kaspern): Implement this on in the backend so this can - // be removed. - // Sort comments by date so that parent ranges can be propagated - // in a single pass. - comments = this._setRanges(comments); - - if (opt_basePatchNum == PARENT_PATCH_NUM) { - baseComments = comments.filter(onlyParent); - baseComments.forEach(setPath); - } - comments = comments.filter(withoutParent); - - comments.forEach(setPath); - }); - promises.push(fetchPromise); - - if (opt_basePatchNum != PARENT_PATCH_NUM) { - fetchPromise = fetchComments(opt_basePatchNum).then(response => { - baseComments = (response[opt_path] || []) - .filter(withoutParent); - baseComments = this._setRanges(baseComments); - baseComments.forEach(setPath); - }); - promises.push(fetchPromise); - } - - return Promise.all(promises).then(() => Promise.resolve({ - baseComments, - comments, - })); - } - - /** - * @param {number|string} changeNum - * @param {string} endpoint - * @param {number|string=} opt_patchNum - */ - _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) { - return this._changeBaseURL(changeNum, opt_patchNum) - .then(url => url + endpoint); - } - - saveDiffDraft(changeNum, patchNum, draft) { - return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft); - } - - deleteDiffDraft(changeNum, patchNum, draft) { - return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft); - } - - /** - * @returns {boolean} Whether there are pending diff draft sends. - */ - hasPendingDiffDrafts() { - const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT]; - return promises && promises.length; - } - - /** - * @returns {!Promise<undefined>} A promise that resolves when all pending - * diff draft sends have resolved. - */ - awaitPendingDiffDrafts() { - return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || []) - .then(() => { - this._pendingRequests[Requests.SEND_DIFF_DRAFT] = []; - }); - } - - _sendDiffDraftRequest(method, changeNum, patchNum, draft) { - const isCreate = !draft.id && method === 'PUT'; - let endpoint = '/drafts'; - let anonymizedEndpoint = endpoint; - if (draft.id) { - endpoint += '/' + draft.id; - anonymizedEndpoint += '/*'; - } - let body; - if (method === 'PUT') { - body = draft; - } - - if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) { - this._pendingRequests[Requests.SEND_DIFF_DRAFT] = []; - } - - const req = { - changeNum, - method, - patchNum, - endpoint, - body, - anonymizedEndpoint, - }; - - const promise = this._getChangeURLAndSend(req); - this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise); - - if (isCreate) { - return this._failForCreate200(promise); - } - - return promise; - } - - getCommitInfo(project, commit) { - return this._restApiHelper.fetchJSON({ - url: '/projects/' + encodeURIComponent(project) + - '/commits/' + encodeURIComponent(commit), - anonymizedUrl: '/projects/*/comments/*', - }); - } - - _fetchB64File(url) { - return this._restApiHelper.fetch({url: this.getBaseUrl() + url}) - .then(response => { - if (!response.ok) { - return Promise.reject(new Error(response.statusText)); - } - const type = response.headers.get('X-FYI-Content-Type'); - return response.text() - .then(text => { - return {body: text, type}; - }); - }); - } - - /** - * @param {string} changeId - * @param {string|number} patchNum - * @param {string} path - * @param {number=} opt_parentIndex - */ - getB64FileContents(changeId, patchNum, path, opt_parentIndex) { - const parent = typeof opt_parentIndex === 'number' ? - '?parent=' + opt_parentIndex : ''; - return this._changeBaseURL(changeId, patchNum).then(url => { - url = `${url}/files/${encodeURIComponent(path)}/content${parent}`; - return this._fetchB64File(url); - }); - } - - getImagesForDiff(changeNum, diff, patchRange) { - let promiseA; - let promiseB; - - if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) { - if (patchRange.basePatchNum === 'PARENT') { - // Note: we only attempt to get the image from the first parent. - promiseA = this.getB64FileContents(changeNum, patchRange.patchNum, - diff.meta_a.name, 1); - } else { - promiseA = this.getB64FileContents(changeNum, - patchRange.basePatchNum, diff.meta_a.name); - } - } else { - promiseA = Promise.resolve(null); - } - - if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) { - promiseB = this.getB64FileContents(changeNum, patchRange.patchNum, - diff.meta_b.name); - } else { - promiseB = Promise.resolve(null); - } - - return Promise.all([promiseA, promiseB]).then(results => { - const baseImage = results[0]; - const revisionImage = results[1]; - - // Sometimes the server doesn't send back the content type. - if (baseImage) { - baseImage._expectedType = diff.meta_a.content_type; - baseImage._name = diff.meta_a.name; - } - if (revisionImage) { - revisionImage._expectedType = diff.meta_b.content_type; - revisionImage._name = diff.meta_b.name; - } - - return {baseImage, revisionImage}; - }); - } - - /** - * @param {number|string} changeNum - * @param {?number|string=} opt_patchNum passed as null sometimes. - * @param {string=} opt_project - * @return {!Promise<string>} - */ - _changeBaseURL(changeNum, opt_patchNum, opt_project) { - // TODO(kaspern): For full slicer migration, app should warn with a call - // stack every time _changeBaseURL is called without a project. - const projectPromise = opt_project ? - Promise.resolve(opt_project) : - this.getFromProjectLookup(changeNum); - return projectPromise.then(project => { - let url = `/changes/${encodeURIComponent(project)}~${changeNum}`; - if (opt_patchNum) { - url += `/revisions/${opt_patchNum}`; - } - return url; - }); - } - - /** - * @suppress {checkTypes} - * Resulted in error: Promise.prototype.then does not match formal - * parameter. - */ - setChangeTopic(changeNum, topic) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', - endpoint: '/topic', - body: {topic}, - parseResponse: true, - reportUrlAsIs: true, - }); - } - - /** - * @suppress {checkTypes} - * Resulted in error: Promise.prototype.then does not match formal - * parameter. - */ - setChangeHashtag(changeNum, hashtag) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - endpoint: '/hashtags', - body: hashtag, - parseResponse: true, - reportUrlAsIs: true, - }); - } - - deleteAccountHttpPassword() { - return this._restApiHelper.send({ - method: 'DELETE', - url: '/accounts/self/password.http', - reportUrlAsIs: true, - }); - } - - /** - * @suppress {checkTypes} - * Resulted in error: Promise.prototype.then does not match formal - * parameter. - */ - generateAccountHttpPassword() { - return this._restApiHelper.send({ - method: 'PUT', - url: '/accounts/self/password.http', - body: {generate: true}, - parseResponse: true, - reportUrlAsIs: true, - }); - } - - getAccountSSHKeys() { - return this._fetchSharedCacheURL({ - url: '/accounts/self/sshkeys', - reportUrlAsIs: true, - }); - } - - addAccountSSHKey(key) { - const req = { - method: 'POST', - url: '/accounts/self/sshkeys', - body: key, - contentType: 'text/plain', - reportUrlAsIs: true, - }; - return this._restApiHelper.send(req) - .then(response => { - if (response.status < 200 && response.status >= 300) { - return Promise.reject(new Error('error')); - } - return this.getResponseObject(response); - }) - .then(obj => { - if (!obj.valid) { return Promise.reject(new Error('error')); } - return obj; - }); - } - - deleteAccountSSHKey(id) { - return this._restApiHelper.send({ - method: 'DELETE', - url: '/accounts/self/sshkeys/' + id, - anonymizedUrl: '/accounts/self/sshkeys/*', - }); - } - - getAccountGPGKeys() { - return this._restApiHelper.fetchJSON({ - url: '/accounts/self/gpgkeys', - reportUrlAsIs: true, - }); - } - - addAccountGPGKey(key) { - const req = { - method: 'POST', - url: '/accounts/self/gpgkeys', - body: key, - reportUrlAsIs: true, - }; - return this._restApiHelper.send(req) - .then(response => { - if (response.status < 200 && response.status >= 300) { - return Promise.reject(new Error('error')); - } - return this.getResponseObject(response); - }) - .then(obj => { - if (!obj) { return Promise.reject(new Error('error')); } - return obj; - }); - } - - deleteAccountGPGKey(id) { - return this._restApiHelper.send({ - method: 'DELETE', - url: '/accounts/self/gpgkeys/' + id, - anonymizedUrl: '/accounts/self/gpgkeys/*', - }); - } - - deleteVote(changeNum, account, label) { - return this._getChangeURLAndSend({ - changeNum, - method: 'DELETE', - endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`, - anonymizedEndpoint: '/reviewers/*/votes/*', - }); - } - - setDescription(changeNum, patchNum, desc) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', patchNum, - endpoint: '/description', - body: {description: desc}, - reportUrlAsIs: true, - }); - } - - confirmEmail(token) { - const req = { - method: 'PUT', - url: '/config/server/email.confirm', - body: {token}, - reportUrlAsIs: true, - }; - return this._restApiHelper.send(req).then(response => { - if (response.status === 204) { - return 'Email confirmed successfully.'; - } - return null; - }); - } - - getCapabilities(opt_errFn) { - return this._restApiHelper.fetchJSON({ - url: '/config/server/capabilities', - errFn: opt_errFn, - reportUrlAsIs: true, - }); - } - - getTopMenus(opt_errFn) { - return this._fetchSharedCacheURL({ - url: '/config/server/top-menus', - errFn: opt_errFn, - reportUrlAsIs: true, - }); - } - - setAssignee(changeNum, assignee) { - return this._getChangeURLAndSend({ - changeNum, - method: 'PUT', - endpoint: '/assignee', - body: {assignee}, - reportUrlAsIs: true, - }); - } - - deleteAssignee(changeNum) { - return this._getChangeURLAndSend({ - changeNum, - method: 'DELETE', - endpoint: '/assignee', - reportUrlAsIs: true, - }); - } - - probePath(path) { - return fetch(new Request(path, {method: 'HEAD'})) - .then(response => response.ok); - } - - /** - * @param {number|string} changeNum - * @param {number|string=} opt_message - */ - startWorkInProgress(changeNum, opt_message) { - const body = {}; - if (opt_message) { - body.message = opt_message; - } - const req = { - changeNum, - method: 'POST', - endpoint: '/wip', - body, - reportUrlAsIs: true, - }; - return this._getChangeURLAndSend(req).then(response => { - if (response.status === 204) { - return 'Change marked as Work In Progress.'; - } - }); - } - - /** - * @param {number|string} changeNum - * @param {number|string=} opt_body - * @param {function(?Response, string=)=} opt_errFn - */ - startReview(changeNum, opt_body, opt_errFn) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - endpoint: '/ready', - body: opt_body, - errFn: opt_errFn, - reportUrlAsIs: true, - }); - } - - /** - * @suppress {checkTypes} - * Resulted in error: Promise.prototype.then does not match formal - * parameter. - */ - deleteComment(changeNum, patchNum, commentID, reason) { - return this._getChangeURLAndSend({ - changeNum, - method: 'POST', - patchNum, - endpoint: `/comments/${commentID}/delete`, - body: {reason}, - parseResponse: true, - anonymizedEndpoint: '/comments/*/delete', - }); - } - - /** - * Given a changeNum, gets the change. - * - * @param {number|string} changeNum - * @param {function(?Response, string=)=} opt_errFn - * @return {!Promise<?Object>} The change - */ - getChange(changeNum, opt_errFn) { - // Cannot use _changeBaseURL, as this function is used by _projectLookup. - return this._restApiHelper.fetchJSON({ - url: `/changes/?q=change:${changeNum}`, - errFn: opt_errFn, - anonymizedUrl: '/changes/?q=change:*', - }).then(res => { - if (!res || !res.length) { return null; } - return res[0]; - }); - } - - /** - * @param {string|number} changeNum - * @param {string=} project - */ - setInProjectLookup(changeNum, project) { - if (this._projectLookup[changeNum] && - this._projectLookup[changeNum] !== project) { - console.warn('Change set with multiple project nums.' + - 'One of them must be invalid.'); - } - this._projectLookup[changeNum] = project; - } - - /** - * Checks in _projectLookup for the changeNum. If it exists, returns the - * project. If not, calls the restAPI to get the change, populates - * _projectLookup with the project for that change, and returns the project. - * - * @param {string|number} changeNum - * @return {!Promise<string|undefined>} - */ - getFromProjectLookup(changeNum) { - const project = this._projectLookup[changeNum]; - if (project) { return Promise.resolve(project); } - - const onError = response => { - // Fire a page error so that the visual 404 is displayed. - this.fire('page-error', {response}); - }; - - return this.getChange(changeNum, onError).then(change => { - if (!change || !change.project) { return; } - this.setInProjectLookup(changeNum, change.project); - return change.project; - }); - } - - /** - * Alias for _changeBaseURL.then(send). - * - * @todo(beckysiegel) clean up comments - * @param {Gerrit.ChangeSendRequest} req - * @return {!Promise<!Object>} - */ - _getChangeURLAndSend(req) { - const anonymizedBaseUrl = req.patchNum ? - ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; - const anonymizedEndpoint = req.reportEndpointAsIs ? - req.endpoint : req.anonymizedEndpoint; - - return this._changeBaseURL(req.changeNum, req.patchNum) - .then(url => this._restApiHelper.send({ - method: req.method, - url: url + req.endpoint, - body: req.body, - errFn: req.errFn, - contentType: req.contentType, - headers: req.headers, - parseResponse: req.parseResponse, - anonymizedUrl: anonymizedEndpoint ? - (anonymizedBaseUrl + anonymizedEndpoint) : undefined, - })); - } - - /** - * Alias for _changeBaseURL.then(_fetchJSON). - * - * @param {Gerrit.ChangeFetchRequest} req - * @return {!Promise<!Object>} - */ - _getChangeURLAndFetch(req, noAcceptHeader) { - const anonymizedEndpoint = req.reportEndpointAsIs ? - req.endpoint : req.anonymizedEndpoint; - const anonymizedBaseUrl = req.patchNum ? - ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; - return this._changeBaseURL(req.changeNum, req.patchNum) - .then(url => this._restApiHelper.fetchJSON({ - url: url + req.endpoint, - errFn: req.errFn, - params: req.params, - fetchOptions: req.fetchOptions, - anonymizedUrl: anonymizedEndpoint ? - (anonymizedBaseUrl + anonymizedEndpoint) : undefined, - }, noAcceptHeader)); - } - - /** - * Execute a change action or revision action on a change. - * - * @param {number} changeNum - * @param {string} method - * @param {string} endpoint - * @param {string|number|undefined} opt_patchNum - * @param {Object=} opt_payload - * @param {?function(?Response, string=)=} opt_errFn - * @return {Promise} - */ - executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload, - opt_errFn) { - return this._getChangeURLAndSend({ - changeNum, - method, - patchNum: opt_patchNum, - endpoint, - body: opt_payload, - errFn: opt_errFn, - }); - } - - /** - * Get blame information for the given diff. - * - * @param {string|number} changeNum - * @param {string|number} patchNum - * @param {string} path - * @param {boolean=} opt_base If true, requests blame for the base of the - * diff, rather than the revision. - * @return {!Promise<!Object>} - */ - getBlame(changeNum, patchNum, path, opt_base) { - const encodedPath = encodeURIComponent(path); - return this._getChangeURLAndFetch({ - changeNum, - endpoint: `/files/${encodedPath}/blame`, - patchNum, - params: opt_base ? {base: 't'} : undefined, - anonymizedEndpoint: '/files/*/blame', - }); - } - - /** - * Modify the given create draft request promise so that it fails and throws - * an error if the response bears HTTP status 200 instead of HTTP 201. - * - * @see Issue 7763 - * @param {Promise} promise The original promise. - * @return {Promise} The modified promise. - */ - _failForCreate200(promise) { - return promise.then(result => { - if (result.status === 200) { - // Read the response headers into an object representation. - const headers = Array.from(result.headers.entries()) - .reduce((obj, [key, val]) => { - if (!HEADER_REPORTING_BLACKLIST.test(key)) { - obj[key] = val; - } - return obj; - }, {}); - const err = new Error([ - CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE, - JSON.stringify(headers), - ].join('\n')); - // Throw the error so that it is caught by gr-reporting. - throw err; - } - return result; - }); - } - - /** - * Fetch a project dashboard definition. - * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard - * - * @param {string} project - * @param {string} dashboard - * @param {function(?Response, string=)=} opt_errFn - * passed as null sometimes. - * @return {!Promise<!Object>} - */ - getDashboard(project, dashboard, opt_errFn) { - const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' + - encodeURIComponent(dashboard); - return this._fetchSharedCacheURL({ - url, - errFn: opt_errFn, - anonymizedUrl: '/projects/*/dashboards/*', - }); - } - - /** - * @param {string} filter - * @return {!Promise<?Object>} - */ - getDocumentationSearches(filter) { - filter = filter.trim(); - const encodedFilter = encodeURIComponent(filter); - - // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend - // supports it. - return this._fetchSharedCacheURL({ - url: `/Documentation/?q=${encodedFilter}`, - anonymizedUrl: '/Documentation/?*', - }); - } - - getMergeable(changeNum) { - return this._getChangeURLAndFetch({ - changeNum, - endpoint: '/revisions/current/mergeable', - parseResponse: true, - reportEndpointAsIs: true, - }); - } - - deleteDraftComments(query) { - return this._restApiHelper.send({ - method: 'POST', - url: '/accounts/self/drafts:delete', - body: {query}, - }); + /** + * @param {?Object} obj + */ + _updateCachedAccount(obj) { + // If result of getAccount is in cache, update it in the cache + // so we don't have to invalidate it. + const cachedAccount = this._cache.get('/accounts/self/detail'); + if (cachedAccount) { + // Replace object in cache with new object to force UI updates. + this._cache.set('/accounts/self/detail', + Object.assign({}, cachedAccount, obj)); } } - customElements.define(GrRestApiInterface.is, GrRestApiInterface); -})(); + /** + * @param {string} name + * @param {function(?Response, string=)=} opt_errFn + */ + setAccountName(name, opt_errFn) { + const req = { + method: 'PUT', + url: '/accounts/self/name', + body: {name}, + errFn: opt_errFn, + parseResponse: true, + reportUrlAsIs: true, + }; + return this._restApiHelper.send(req) + .then(newName => this._updateCachedAccount({name: newName})); + } + + /** + * @param {string} username + * @param {function(?Response, string=)=} opt_errFn + */ + setAccountUsername(username, opt_errFn) { + const req = { + method: 'PUT', + url: '/accounts/self/username', + body: {username}, + errFn: opt_errFn, + parseResponse: true, + reportUrlAsIs: true, + }; + return this._restApiHelper.send(req) + .then(newName => this._updateCachedAccount({username: newName})); + } + + /** + * @param {string} status + * @param {function(?Response, string=)=} opt_errFn + */ + setAccountStatus(status, opt_errFn) { + const req = { + method: 'PUT', + url: '/accounts/self/status', + body: {status}, + errFn: opt_errFn, + parseResponse: true, + reportUrlAsIs: true, + }; + return this._restApiHelper.send(req) + .then(newStatus => this._updateCachedAccount({status: newStatus})); + } + + getAccountStatus(userId) { + return this._restApiHelper.fetchJSON({ + url: `/accounts/${encodeURIComponent(userId)}/status`, + anonymizedUrl: '/accounts/*/status', + }); + } + + getAccountGroups() { + return this._restApiHelper.fetchJSON({ + url: '/accounts/self/groups', + reportUrlAsIs: true, + }); + } + + getAccountAgreements() { + return this._restApiHelper.fetchJSON({ + url: '/accounts/self/agreements', + reportUrlAsIs: true, + }); + } + + saveAccountAgreement(name) { + return this._restApiHelper.send({ + method: 'PUT', + url: '/accounts/self/agreements', + body: name, + reportUrlAsIs: true, + }); + } + + /** + * @param {string=} opt_params + */ + getAccountCapabilities(opt_params) { + let queryString = ''; + if (opt_params) { + queryString = '?q=' + opt_params + .map(param => encodeURIComponent(param)) + .join('&q='); + } + return this._fetchSharedCacheURL({ + url: '/accounts/self/capabilities' + queryString, + anonymizedUrl: '/accounts/self/capabilities?q=*', + }); + } + + getLoggedIn() { + return this._auth.authCheck(); + } + + getIsAdmin() { + return this.getLoggedIn() + .then(isLoggedIn => { + if (isLoggedIn) { + return this.getAccountCapabilities(); + } else { + return Promise.resolve(); + } + }) + .then( + capabilities => capabilities && capabilities.administrateServer + ); + } + + getDefaultPreferences() { + return this._fetchSharedCacheURL({ + url: '/config/server/preferences', + reportUrlAsIs: true, + }); + } + + getPreferences() { + return this.getLoggedIn().then(loggedIn => { + if (loggedIn) { + const req = {url: '/accounts/self/preferences', reportUrlAsIs: true}; + return this._fetchSharedCacheURL(req).then(res => { + if (this._isNarrowScreen()) { + // Note that this can be problematic, because the diff will stay + // unified even after increasing the window width. + res.default_diff_view = DiffViewMode.UNIFIED; + } else { + res.default_diff_view = res.diff_view; + } + return Promise.resolve(res); + }); + } + + return Promise.resolve({ + changes_per_page: 25, + default_diff_view: this._isNarrowScreen() ? + DiffViewMode.UNIFIED : DiffViewMode.SIDE_BY_SIDE, + diff_view: 'SIDE_BY_SIDE', + size_bar_in_change_table: true, + }); + }); + } + + getWatchedProjects() { + return this._fetchSharedCacheURL({ + url: '/accounts/self/watched.projects', + reportUrlAsIs: true, + }); + } + + /** + * @param {string} projects + * @param {function(?Response, string=)=} opt_errFn + */ + saveWatchedProjects(projects, opt_errFn) { + return this._restApiHelper.send({ + method: 'POST', + url: '/accounts/self/watched.projects', + body: projects, + errFn: opt_errFn, + parseResponse: true, + reportUrlAsIs: true, + }); + } + + /** + * @param {string} projects + * @param {function(?Response, string=)=} opt_errFn + */ + deleteWatchedProjects(projects, opt_errFn) { + return this._restApiHelper.send({ + method: 'POST', + url: '/accounts/self/watched.projects:delete', + body: projects, + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } + + _isNarrowScreen() { + return window.innerWidth < MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX; + } + + /** + * @param {number=} opt_changesPerPage + * @param {string|!Array<string>=} opt_query A query or an array of queries. + * @param {number|string=} opt_offset + * @param {!Object=} opt_options + * @return {?Array<!Object>|?Array<!Array<!Object>>} If opt_query is an + * array, _fetchJSON will return an array of arrays of changeInfos. If it + * is unspecified or a string, _fetchJSON will return an array of + * changeInfos. + */ + getChanges(opt_changesPerPage, opt_query, opt_offset, opt_options) { + const options = opt_options || this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.DETAILED_ACCOUNTS + ); + // Issue 4524: respect legacy token with max sortkey. + if (opt_offset === 'n,z') { + opt_offset = 0; + } + const params = { + O: options, + S: opt_offset || 0, + }; + if (opt_changesPerPage) { params.n = opt_changesPerPage; } + if (opt_query && opt_query.length > 0) { + params.q = opt_query; + } + const iterateOverChanges = arr => { + for (const change of (arr || [])) { + this._maybeInsertInLookup(change); + } + }; + const req = { + url: '/changes/', + params, + reportUrlAsIs: true, + }; + return this._restApiHelper.fetchJSON(req).then(response => { + // Response may be an array of changes OR an array of arrays of + // changes. + if (opt_query instanceof Array) { + // Normalize the response to look like a multi-query response + // when there is only one query. + if (opt_query.length === 1) { + response = [response]; + } + for (const arr of response) { + iterateOverChanges(arr); + } + } else { + iterateOverChanges(response); + } + return response; + }); + } + + /** + * Inserts a change into _projectLookup iff it has a valid structure. + * + * @param {?{ _number: (number|string) }} change + */ + _maybeInsertInLookup(change) { + if (change && change.project && change._number) { + this.setInProjectLookup(change._number, change.project); + } + } + + /** + * TODO (beckysiegel) this needs to be rewritten with the optional param + * at the end. + * + * @param {number|string} changeNum + * @param {?number|string=} opt_patchNum passed as null sometimes. + * @param {?=} endpoint + * @return {!Promise<string>} + */ + getChangeActionURL(changeNum, opt_patchNum, endpoint) { + return this._changeBaseURL(changeNum, opt_patchNum) + .then(url => url + endpoint); + } + + /** + * @param {number|string} changeNum + * @param {function(?Response, string=)=} opt_errFn + * @param {function()=} opt_cancelCondition + */ + getChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { + return this.getConfig(false).then(config => { + const optionsHex = this._getChangeOptionsHex(config); + return this._getChangeDetail( + changeNum, optionsHex, opt_errFn, opt_cancelCondition) + .then(GrReviewerUpdatesParser.parse); + }); + } + + _getChangeOptionsHex(config) { + if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.changePage + && !(config.receive && config.receive.enable_signed_push)) { + return window.DEFAULT_DETAIL_HEXES.changePage; + } + + // This list MUST be kept in sync with + // ChangeIT#changeDetailsDoesNotRequireIndex + const options = [ + this.ListChangesOption.ALL_COMMITS, + this.ListChangesOption.ALL_REVISIONS, + this.ListChangesOption.CHANGE_ACTIONS, + this.ListChangesOption.DETAILED_LABELS, + this.ListChangesOption.DOWNLOAD_COMMANDS, + this.ListChangesOption.MESSAGES, + this.ListChangesOption.SUBMITTABLE, + this.ListChangesOption.WEB_LINKS, + this.ListChangesOption.SKIP_DIFFSTAT, + ]; + if (config.receive && config.receive.enable_signed_push) { + options.push(this.ListChangesOption.PUSH_CERTIFICATES); + } + return this.listChangesOptionsToHex(...options); + } + + /** + * @param {number|string} changeNum + * @param {function(?Response, string=)=} opt_errFn + * @param {function()=} opt_cancelCondition + */ + getDiffChangeDetail(changeNum, opt_errFn, opt_cancelCondition) { + let optionsHex = ''; + if (window.DEFAULT_DETAIL_HEXES && window.DEFAULT_DETAIL_HEXES.diffPage) { + optionsHex = window.DEFAULT_DETAIL_HEXES.diffPage; + } else { + optionsHex = this.listChangesOptionsToHex( + this.ListChangesOption.ALL_COMMITS, + this.ListChangesOption.ALL_REVISIONS, + this.ListChangesOption.SKIP_DIFFSTAT + ); + } + return this._getChangeDetail(changeNum, optionsHex, opt_errFn, + opt_cancelCondition); + } + + /** + * @param {number|string} changeNum + * @param {string|undefined} optionsHex list changes options in hex + * @param {function(?Response, string=)=} opt_errFn + * @param {function()=} opt_cancelCondition + */ + _getChangeDetail(changeNum, optionsHex, opt_errFn, opt_cancelCondition) { + return this.getChangeActionURL(changeNum, null, '/detail').then(url => { + const urlWithParams = this._restApiHelper + .urlWithParams(url, optionsHex); + const params = {O: optionsHex}; + const req = { + url, + errFn: opt_errFn, + cancelCondition: opt_cancelCondition, + params, + fetchOptions: this._etags.getOptions(urlWithParams), + anonymizedUrl: '/changes/*~*/detail?O=' + optionsHex, + }; + return this._restApiHelper.fetchRawJSON(req).then(response => { + if (response && response.status === 304) { + return Promise.resolve(this._restApiHelper.parsePrefixedJSON( + this._etags.getCachedPayload(urlWithParams))); + } + + if (response && !response.ok) { + if (opt_errFn) { + opt_errFn.call(null, response); + } else { + this.fire('server-error', {request: req, response}); + } + return; + } + + const payloadPromise = response ? + this._restApiHelper.readResponsePayload(response) : + Promise.resolve(null); + + return payloadPromise.then(payload => { + if (!payload) { return null; } + this._etags.collect(urlWithParams, response, payload.raw); + this._maybeInsertInLookup(payload.parsed); + + return payload.parsed; + }); + }); + }); + } + + /** + * @param {number|string} changeNum + * @param {number|string} patchNum + */ + getChangeCommitInfo(changeNum, patchNum) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/commit?links', + patchNum, + reportEndpointAsIs: true, + }); + } + + /** + * @param {number|string} changeNum + * @param {Gerrit.PatchRange} patchRange + * @param {number=} opt_parentIndex + */ + getChangeFiles(changeNum, patchRange, opt_parentIndex) { + let params = undefined; + if (this.isMergeParent(patchRange.basePatchNum)) { + params = {parent: this.getParentIndex(patchRange.basePatchNum)}; + } else if (!this.patchNumEquals(patchRange.basePatchNum, 'PARENT')) { + params = {base: patchRange.basePatchNum}; + } + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/files', + patchNum: patchRange.patchNum, + params, + reportEndpointAsIs: true, + }); + } + + /** + * @param {number|string} changeNum + * @param {Gerrit.PatchRange} patchRange + */ + getChangeEditFiles(changeNum, patchRange) { + let endpoint = '/edit?list'; + let anonymizedEndpoint = endpoint; + if (patchRange.basePatchNum !== 'PARENT') { + endpoint += '&base=' + encodeURIComponent(patchRange.basePatchNum + ''); + anonymizedEndpoint += '&base=*'; + } + return this._getChangeURLAndFetch({ + changeNum, + endpoint, + anonymizedEndpoint, + }); + } + + /** + * @param {number|string} changeNum + * @param {number|string} patchNum + * @param {string} query + * @return {!Promise<!Object>} + */ + queryChangeFiles(changeNum, patchNum, query) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: `/files?q=${encodeURIComponent(query)}`, + patchNum, + anonymizedEndpoint: '/files?q=*', + }); + } + + /** + * @param {number|string} changeNum + * @param {Gerrit.PatchRange} patchRange + * @return {!Promise<!Array<!Object>>} + */ + getChangeOrEditFiles(changeNum, patchRange) { + if (this.patchNumEquals(patchRange.patchNum, this.EDIT_NAME)) { + return this.getChangeEditFiles(changeNum, patchRange).then(res => + res.files); + } + return this.getChangeFiles(changeNum, patchRange); + } + + getChangeRevisionActions(changeNum, patchNum) { + const req = { + changeNum, + endpoint: '/actions', + patchNum, + reportEndpointAsIs: true, + }; + return this._getChangeURLAndFetch(req).then(revisionActions => { + // The rebase button on change screen is always enabled. + if (revisionActions.rebase) { + revisionActions.rebase.rebaseOnCurrent = + !!revisionActions.rebase.enabled; + revisionActions.rebase.enabled = true; + } + return revisionActions; + }); + } + + /** + * @param {number|string} changeNum + * @param {string} inputVal + * @param {function(?Response, string=)=} opt_errFn + */ + getChangeSuggestedReviewers(changeNum, inputVal, opt_errFn) { + return this._getChangeSuggestedGroup('REVIEWER', changeNum, inputVal, + opt_errFn); + } + + /** + * @param {number|string} changeNum + * @param {string} inputVal + * @param {function(?Response, string=)=} opt_errFn + */ + getChangeSuggestedCCs(changeNum, inputVal, opt_errFn) { + return this._getChangeSuggestedGroup('CC', changeNum, inputVal, + opt_errFn); + } + + _getChangeSuggestedGroup(reviewerState, changeNum, inputVal, opt_errFn) { + // More suggestions may obscure content underneath in the reply dialog, + // see issue 10793. + const params = {'n': 6, 'reviewer-state': reviewerState}; + if (inputVal) { params.q = inputVal; } + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/suggest_reviewers', + errFn: opt_errFn, + params, + reportEndpointAsIs: true, + }); + } + + /** + * @param {number|string} changeNum + */ + getChangeIncludedIn(changeNum) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/in', + reportEndpointAsIs: true, + }); + } + + _computeFilter(filter) { + if (filter && filter.startsWith('^')) { + filter = '&r=' + encodeURIComponent(filter); + } else if (filter) { + filter = '&m=' + encodeURIComponent(filter); + } else { + filter = ''; + } + return filter; + } + + /** + * @param {string} filter + * @param {number} groupsPerPage + * @param {number=} opt_offset + */ + _getGroupsUrl(filter, groupsPerPage, opt_offset) { + const offset = opt_offset || 0; + + return `/groups/?n=${groupsPerPage + 1}&S=${offset}` + + this._computeFilter(filter); + } + + /** + * @param {string} filter + * @param {number} reposPerPage + * @param {number=} opt_offset + */ + _getReposUrl(filter, reposPerPage, opt_offset) { + const defaultFilter = 'state:active OR state:read-only'; + const namePartDelimiters = /[@.\-\s\/_]/g; + const offset = opt_offset || 0; + + if (filter && !filter.includes(':') && filter.match(namePartDelimiters)) { + // The query language specifies hyphens as operators. Split the string + // by hyphens and 'AND' the parts together as 'inname:' queries. + // If the filter includes a semicolon, the user is using a more complex + // query so we trust them and don't do any magic under the hood. + const originalFilter = filter; + filter = ''; + originalFilter.split(namePartDelimiters).forEach(part => { + if (part) { + filter += (filter === '' ? 'inname:' : ' AND inname:') + part; + } + }); + } + // Check if filter is now empty which could be either because the user did + // not provide it or because the user provided only a split character. + if (!filter) { + filter = defaultFilter; + } + + filter = filter.trim(); + const encodedFilter = encodeURIComponent(filter); + + return `/projects/?n=${reposPerPage + 1}&S=${offset}` + + `&query=${encodedFilter}`; + } + + invalidateGroupsCache() { + this._restApiHelper.invalidateFetchPromisesPrefix('/groups/?'); + } + + invalidateReposCache() { + this._restApiHelper.invalidateFetchPromisesPrefix('/projects/?'); + } + + invalidateAccountsCache() { + this._restApiHelper.invalidateFetchPromisesPrefix('/accounts/'); + } + + /** + * @param {string} filter + * @param {number} groupsPerPage + * @param {number=} opt_offset + * @return {!Promise<?Object>} + */ + getGroups(filter, groupsPerPage, opt_offset) { + const url = this._getGroupsUrl(filter, groupsPerPage, opt_offset); + + return this._fetchSharedCacheURL({ + url, + anonymizedUrl: '/groups/?*', + }); + } + + /** + * @param {string} filter + * @param {number} reposPerPage + * @param {number=} opt_offset + * @return {!Promise<?Object>} + */ + getRepos(filter, reposPerPage, opt_offset) { + const url = this._getReposUrl(filter, reposPerPage, opt_offset); + + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._fetchSharedCacheURL({ + url, + anonymizedUrl: '/projects/?*', + }); + } + + setRepoHead(repo, ref) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._restApiHelper.send({ + method: 'PUT', + url: `/projects/${encodeURIComponent(repo)}/HEAD`, + body: {ref}, + anonymizedUrl: '/projects/*/HEAD', + }); + } + + /** + * @param {string} filter + * @param {string} repo + * @param {number} reposBranchesPerPage + * @param {number=} opt_offset + * @param {?function(?Response, string=)=} opt_errFn + * @return {!Promise<?Object>} + */ + getRepoBranches(filter, repo, reposBranchesPerPage, opt_offset, opt_errFn) { + const offset = opt_offset || 0; + const count = reposBranchesPerPage + 1; + filter = this._computeFilter(filter); + repo = encodeURIComponent(repo); + const url = `/projects/${repo}/branches?n=${count}&S=${offset}${filter}`; + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._restApiHelper.fetchJSON({ + url, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/branches?*', + }); + } + + /** + * @param {string} filter + * @param {string} repo + * @param {number} reposTagsPerPage + * @param {number=} opt_offset + * @param {?function(?Response, string=)=} opt_errFn + * @return {!Promise<?Object>} + */ + getRepoTags(filter, repo, reposTagsPerPage, opt_offset, opt_errFn) { + const offset = opt_offset || 0; + const encodedRepo = encodeURIComponent(repo); + const n = reposTagsPerPage + 1; + const encodedFilter = this._computeFilter(filter); + const url = `/projects/${encodedRepo}/tags` + `?n=${n}&S=${offset}` + + encodedFilter; + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._restApiHelper.fetchJSON({ + url, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/tags', + }); + } + + /** + * @param {string} filter + * @param {number} pluginsPerPage + * @param {number=} opt_offset + * @param {?function(?Response, string=)=} opt_errFn + * @return {!Promise<?Object>} + */ + getPlugins(filter, pluginsPerPage, opt_offset, opt_errFn) { + const offset = opt_offset || 0; + const encodedFilter = this._computeFilter(filter); + const n = pluginsPerPage + 1; + const url = `/plugins/?all&n=${n}&S=${offset}${encodedFilter}`; + return this._restApiHelper.fetchJSON({ + url, + errFn: opt_errFn, + anonymizedUrl: '/plugins/?all', + }); + } + + getRepoAccessRights(repoName, opt_errFn) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._restApiHelper.fetchJSON({ + url: `/projects/${encodeURIComponent(repoName)}/access`, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/access', + }); + } + + setRepoAccessRights(repoName, repoInfo) { + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._restApiHelper.send({ + method: 'POST', + url: `/projects/${encodeURIComponent(repoName)}/access`, + body: repoInfo, + anonymizedUrl: '/projects/*/access', + }); + } + + setRepoAccessRightsForReview(projectName, projectInfo) { + return this._restApiHelper.send({ + method: 'PUT', + url: `/projects/${encodeURIComponent(projectName)}/access:review`, + body: projectInfo, + parseResponse: true, + anonymizedUrl: '/projects/*/access:review', + }); + } + + /** + * @param {string} inputVal + * @param {number} opt_n + * @param {function(?Response, string=)=} opt_errFn + */ + getSuggestedGroups(inputVal, opt_n, opt_errFn) { + const params = {s: inputVal}; + if (opt_n) { params.n = opt_n; } + return this._restApiHelper.fetchJSON({ + url: '/groups/', + errFn: opt_errFn, + params, + reportUrlAsIs: true, + }); + } + + /** + * @param {string} inputVal + * @param {number} opt_n + * @param {function(?Response, string=)=} opt_errFn + */ + getSuggestedProjects(inputVal, opt_n, opt_errFn) { + const params = { + m: inputVal, + n: MAX_PROJECT_RESULTS, + type: 'ALL', + }; + if (opt_n) { params.n = opt_n; } + return this._restApiHelper.fetchJSON({ + url: '/projects/', + errFn: opt_errFn, + params, + reportUrlAsIs: true, + }); + } + + /** + * @param {string} inputVal + * @param {number} opt_n + * @param {function(?Response, string=)=} opt_errFn + */ + getSuggestedAccounts(inputVal, opt_n, opt_errFn) { + if (!inputVal) { + return Promise.resolve([]); + } + const params = {suggest: null, q: inputVal}; + if (opt_n) { params.n = opt_n; } + return this._restApiHelper.fetchJSON({ + url: '/accounts/', + errFn: opt_errFn, + params, + anonymizedUrl: '/accounts/?n=*', + }); + } + + addChangeReviewer(changeNum, reviewerID) { + return this._sendChangeReviewerRequest('POST', changeNum, reviewerID); + } + + removeChangeReviewer(changeNum, reviewerID) { + return this._sendChangeReviewerRequest('DELETE', changeNum, reviewerID); + } + + _sendChangeReviewerRequest(method, changeNum, reviewerID) { + return this.getChangeActionURL(changeNum, null, '/reviewers') + .then(url => { + let body; + switch (method) { + case 'POST': + body = {reviewer: reviewerID}; + break; + case 'DELETE': + url += '/' + encodeURIComponent(reviewerID); + break; + default: + throw Error('Unsupported HTTP method: ' + method); + } + + return this._restApiHelper.send({method, url, body}); + }); + } + + getRelatedChanges(changeNum, patchNum) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/related', + patchNum, + reportEndpointAsIs: true, + }); + } + + getChangesSubmittedTogether(changeNum) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/submitted_together?o=NON_VISIBLE_CHANGES', + reportEndpointAsIs: true, + }); + } + + getChangeConflicts(changeNum) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT + ); + const params = { + O: options, + q: 'status:open conflicts:' + changeNum, + }; + return this._restApiHelper.fetchJSON({ + url: '/changes/', + params, + anonymizedUrl: '/changes/conflicts:*', + }); + } + + getChangeCherryPicks(project, changeID, changeNum) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT + ); + const query = [ + 'project:' + project, + 'change:' + changeID, + '-change:' + changeNum, + '-is:abandoned', + ].join(' '); + const params = { + O: options, + q: query, + }; + return this._restApiHelper.fetchJSON({ + url: '/changes/', + params, + anonymizedUrl: '/changes/change:*', + }); + } + + getChangesWithSameTopic(topic, changeNum) { + const options = this.listChangesOptionsToHex( + this.ListChangesOption.LABELS, + this.ListChangesOption.CURRENT_REVISION, + this.ListChangesOption.CURRENT_COMMIT, + this.ListChangesOption.DETAILED_LABELS + ); + const query = [ + 'status:open', + '-change:' + changeNum, + `topic:"${topic}"`, + ].join(' '); + const params = { + O: options, + q: query, + }; + return this._restApiHelper.fetchJSON({ + url: '/changes/', + params, + anonymizedUrl: '/changes/topic:*', + }); + } + + getReviewedFiles(changeNum, patchNum) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/files?reviewed', + patchNum, + reportEndpointAsIs: true, + }); + } + + /** + * @param {number|string} changeNum + * @param {number|string} patchNum + * @param {string} path + * @param {boolean} reviewed + * @param {function(?Response, string=)=} opt_errFn + */ + saveFileReviewed(changeNum, patchNum, path, reviewed, opt_errFn) { + return this._getChangeURLAndSend({ + changeNum, + method: reviewed ? 'PUT' : 'DELETE', + patchNum, + endpoint: `/files/${encodeURIComponent(path)}/reviewed`, + errFn: opt_errFn, + anonymizedEndpoint: '/files/*/reviewed', + }); + } + + /** + * @param {number|string} changeNum + * @param {number|string} patchNum + * @param {!Object} review + * @param {function(?Response, string=)=} opt_errFn + */ + saveChangeReview(changeNum, patchNum, review, opt_errFn) { + const promises = [ + this.awaitPendingDiffDrafts(), + this.getChangeActionURL(changeNum, patchNum, '/review'), + ]; + return Promise.all(promises).then(([, url]) => this._restApiHelper.send({ + method: 'POST', + url, + body: review, + errFn: opt_errFn, + })); + } + + getChangeEdit(changeNum, opt_download_commands) { + const params = opt_download_commands ? {'download-commands': true} : null; + return this.getLoggedIn().then(loggedIn => { + if (!loggedIn) { return false; } + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/edit/', + params, + reportEndpointAsIs: true, + }, true); + }); + } + + /** + * @param {string} project + * @param {string} branch + * @param {string} subject + * @param {string=} opt_topic + * @param {boolean=} opt_isPrivate + * @param {boolean=} opt_workInProgress + * @param {string=} opt_baseChange + * @param {string=} opt_baseCommit + */ + createChange(project, branch, subject, opt_topic, opt_isPrivate, + opt_workInProgress, opt_baseChange, opt_baseCommit) { + return this._restApiHelper.send({ + method: 'POST', + url: '/changes/', + body: { + project, + branch, + subject, + topic: opt_topic, + is_private: opt_isPrivate, + work_in_progress: opt_workInProgress, + base_change: opt_baseChange, + base_commit: opt_baseCommit, + }, + parseResponse: true, + reportUrlAsIs: true, + }); + } + + /** + * @param {number|string} changeNum + * @param {string} path + * @param {number|string} patchNum + */ + getFileContent(changeNum, path, patchNum) { + // 404s indicate the file does not exist yet in the revision, so suppress + // them. + const suppress404s = res => { + if (res && res.status !== 404) { this.fire('server-error', {res}); } + return res; + }; + const promise = this.patchNumEquals(patchNum, this.EDIT_NAME) ? + this._getFileInChangeEdit(changeNum, path) : + this._getFileInRevision(changeNum, path, patchNum, suppress404s); + + return promise.then(res => { + if (!res.ok) { return res; } + + // The file type (used for syntax highlighting) is identified in the + // X-FYI-Content-Type header of the response. + const type = res.headers.get('X-FYI-Content-Type'); + return this.getResponseObject(res).then(content => { + return {content, type, ok: true}; + }); + }); + } + + /** + * Gets a file in a specific change and revision. + * + * @param {number|string} changeNum + * @param {string} path + * @param {number|string} patchNum + * @param {?function(?Response, string=)=} opt_errFn + */ + _getFileInRevision(changeNum, path, patchNum, opt_errFn) { + return this._getChangeURLAndSend({ + changeNum, + method: 'GET', + patchNum, + endpoint: `/files/${encodeURIComponent(path)}/content`, + errFn: opt_errFn, + headers: {Accept: 'application/json'}, + anonymizedEndpoint: '/files/*/content', + }); + } + + /** + * Gets a file in a change edit. + * + * @param {number|string} changeNum + * @param {string} path + */ + _getFileInChangeEdit(changeNum, path) { + return this._getChangeURLAndSend({ + changeNum, + method: 'GET', + endpoint: '/edit/' + encodeURIComponent(path), + headers: {Accept: 'application/json'}, + anonymizedEndpoint: '/edit/*', + }); + } + + rebaseChangeEdit(changeNum) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + endpoint: '/edit:rebase', + reportEndpointAsIs: true, + }); + } + + deleteChangeEdit(changeNum) { + return this._getChangeURLAndSend({ + changeNum, + method: 'DELETE', + endpoint: '/edit', + reportEndpointAsIs: true, + }); + } + + restoreFileInChangeEdit(changeNum, restore_path) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + endpoint: '/edit', + body: {restore_path}, + reportEndpointAsIs: true, + }); + } + + renameFileInChangeEdit(changeNum, old_path, new_path) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + endpoint: '/edit', + body: {old_path, new_path}, + reportEndpointAsIs: true, + }); + } + + deleteFileInChangeEdit(changeNum, path) { + return this._getChangeURLAndSend({ + changeNum, + method: 'DELETE', + endpoint: '/edit/' + encodeURIComponent(path), + anonymizedEndpoint: '/edit/*', + }); + } + + saveChangeEdit(changeNum, path, contents) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', + endpoint: '/edit/' + encodeURIComponent(path), + body: contents, + contentType: 'text/plain', + anonymizedEndpoint: '/edit/*', + }); + } + + getRobotCommentFixPreview(changeNum, patchNum, fixId) { + return this._getChangeURLAndFetch({ + changeNum, + patchNum, + endpoint: `/fixes/${encodeURIComponent(fixId)}/preview`, + reportEndpointAsId: true, + }); + } + + applyFixSuggestion(changeNum, patchNum, fixId) { + return this._getChangeURLAndSend({ + method: 'POST', + changeNum, + patchNum, + endpoint: `/fixes/${encodeURIComponent(fixId)}/apply`, + reportEndpointAsId: true, + }); + } + + // Deprecated, prefer to use putChangeCommitMessage instead. + saveChangeCommitMessageEdit(changeNum, message) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', + endpoint: '/edit:message', + body: {message}, + reportEndpointAsIs: true, + }); + } + + publishChangeEdit(changeNum) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + endpoint: '/edit:publish', + reportEndpointAsIs: true, + }); + } + + putChangeCommitMessage(changeNum, message) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', + endpoint: '/message', + body: {message}, + reportEndpointAsIs: true, + }); + } + + deleteChangeCommitMessage(changeNum, messageId) { + return this._getChangeURLAndSend({ + changeNum, + method: 'DELETE', + endpoint: '/messages/' + messageId, + reportEndpointAsIs: true, + }); + } + + saveChangeStarred(changeNum, starred) { + // Some servers may require the project name to be provided + // alongside the change number, so resolve the project name + // first. + return this.getFromProjectLookup(changeNum).then(project => { + const url = '/accounts/self/starred.changes/' + + (project ? encodeURIComponent(project) + '~' : '') + changeNum; + return this._restApiHelper.send({ + method: starred ? 'PUT' : 'DELETE', + url, + anonymizedUrl: '/accounts/self/starred.changes/*', + }); + }); + } + + saveChangeReviewed(changeNum, reviewed) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', + endpoint: reviewed ? '/reviewed' : '/unreviewed', + }); + } + + /** + * Public version of the _restApiHelper.send method preserved for plugins. + * + * @param {string} method + * @param {string} url + * @param {?string|number|Object=} opt_body passed as null sometimes + * and also apparently a number. TODO (beckysiegel) remove need for + * number at least. + * @param {?function(?Response, string=)=} opt_errFn + * passed as null sometimes. + * @param {?string=} opt_contentType + * @param {Object=} opt_headers + */ + send(method, url, opt_body, opt_errFn, opt_contentType, + opt_headers) { + return this._restApiHelper.send({ + method, + url, + body: opt_body, + errFn: opt_errFn, + contentType: opt_contentType, + headers: opt_headers, + }); + } + + /** + * @param {number|string} changeNum + * @param {number|string} basePatchNum Negative values specify merge parent + * index. + * @param {number|string} patchNum + * @param {string} path + * @param {string=} opt_whitespace the ignore-whitespace level for the diff + * algorithm. + * @param {function(?Response, string=)=} opt_errFn + */ + getDiff(changeNum, basePatchNum, patchNum, path, opt_whitespace, + opt_errFn) { + const params = { + context: 'ALL', + intraline: null, + whitespace: opt_whitespace || 'IGNORE_NONE', + }; + if (this.isMergeParent(basePatchNum)) { + params.parent = this.getParentIndex(basePatchNum); + } else if (!this.patchNumEquals(basePatchNum, PARENT_PATCH_NUM)) { + params.base = basePatchNum; + } + const endpoint = `/files/${encodeURIComponent(path)}/diff`; + const req = { + changeNum, + endpoint, + patchNum, + errFn: opt_errFn, + params, + anonymizedEndpoint: '/files/*/diff', + }; + + // Invalidate the cache if its edit patch to make sure we always get latest. + if (patchNum === this.EDIT_NAME) { + if (!req.fetchOptions) req.fetchOptions = {}; + if (!req.fetchOptions.headers) req.fetchOptions.headers = new Headers(); + req.fetchOptions.headers.append('Cache-Control', 'no-cache'); + } + + return this._getChangeURLAndFetch(req); + } + + /** + * @param {number|string} changeNum + * @param {number|string=} opt_basePatchNum + * @param {number|string=} opt_patchNum + * @param {string=} opt_path + * @return {!Promise<!Object>} + */ + getDiffComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { + return this._getDiffComments(changeNum, '/comments', opt_basePatchNum, + opt_patchNum, opt_path); + } + + /** + * @param {number|string} changeNum + * @param {number|string=} opt_basePatchNum + * @param {number|string=} opt_patchNum + * @param {string=} opt_path + * @return {!Promise<!Object>} + */ + getDiffRobotComments(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { + return this._getDiffComments(changeNum, '/robotcomments', + opt_basePatchNum, opt_patchNum, opt_path); + } + + /** + * If the user is logged in, fetch the user's draft diff comments. If there + * is no logged in user, the request is not made and the promise yields an + * empty object. + * + * @param {number|string} changeNum + * @param {number|string=} opt_basePatchNum + * @param {number|string=} opt_patchNum + * @param {string=} opt_path + * @return {!Promise<!Object>} + */ + getDiffDrafts(changeNum, opt_basePatchNum, opt_patchNum, opt_path) { + return this.getLoggedIn().then(loggedIn => { + if (!loggedIn) { return Promise.resolve({}); } + return this._getDiffComments(changeNum, '/drafts', opt_basePatchNum, + opt_patchNum, opt_path); + }); + } + + _setRange(comments, comment) { + if (comment.in_reply_to && !comment.range) { + for (let i = 0; i < comments.length; i++) { + if (comments[i].id === comment.in_reply_to) { + comment.range = comments[i].range; + break; + } + } + } + return comment; + } + + _setRanges(comments) { + comments = comments || []; + comments.sort( + (a, b) => util.parseDate(a.updated) - util.parseDate(b.updated) + ); + for (const comment of comments) { + this._setRange(comments, comment); + } + return comments; + } + + /** + * @param {number|string} changeNum + * @param {string} endpoint + * @param {number|string=} opt_basePatchNum + * @param {number|string=} opt_patchNum + * @param {string=} opt_path + * @return {!Promise<!Object>} + */ + _getDiffComments(changeNum, endpoint, opt_basePatchNum, + opt_patchNum, opt_path) { + /** + * Fetches the comments for a given patchNum. + * Helper function to make promises more legible. + * + * @param {string|number=} opt_patchNum + * @return {!Promise<!Object>} Diff comments response. + */ + // We don't want to add accept header, since preloading of comments is + // working only without accept header. + const noAcceptHeader = true; + const fetchComments = opt_patchNum => this._getChangeURLAndFetch({ + changeNum, + endpoint, + patchNum: opt_patchNum, + reportEndpointAsIs: true, + }, noAcceptHeader); + + if (!opt_basePatchNum && !opt_patchNum && !opt_path) { + return fetchComments(); + } + function onlyParent(c) { return c.side == PARENT_PATCH_NUM; } + function withoutParent(c) { return c.side != PARENT_PATCH_NUM; } + function setPath(c) { c.path = opt_path; } + + const promises = []; + let comments; + let baseComments; + let fetchPromise; + fetchPromise = fetchComments(opt_patchNum).then(response => { + comments = response[opt_path] || []; + // TODO(kaspern): Implement this on in the backend so this can + // be removed. + // Sort comments by date so that parent ranges can be propagated + // in a single pass. + comments = this._setRanges(comments); + + if (opt_basePatchNum == PARENT_PATCH_NUM) { + baseComments = comments.filter(onlyParent); + baseComments.forEach(setPath); + } + comments = comments.filter(withoutParent); + + comments.forEach(setPath); + }); + promises.push(fetchPromise); + + if (opt_basePatchNum != PARENT_PATCH_NUM) { + fetchPromise = fetchComments(opt_basePatchNum).then(response => { + baseComments = (response[opt_path] || []) + .filter(withoutParent); + baseComments = this._setRanges(baseComments); + baseComments.forEach(setPath); + }); + promises.push(fetchPromise); + } + + return Promise.all(promises).then(() => Promise.resolve({ + baseComments, + comments, + })); + } + + /** + * @param {number|string} changeNum + * @param {string} endpoint + * @param {number|string=} opt_patchNum + */ + _getDiffCommentsFetchURL(changeNum, endpoint, opt_patchNum) { + return this._changeBaseURL(changeNum, opt_patchNum) + .then(url => url + endpoint); + } + + saveDiffDraft(changeNum, patchNum, draft) { + return this._sendDiffDraftRequest('PUT', changeNum, patchNum, draft); + } + + deleteDiffDraft(changeNum, patchNum, draft) { + return this._sendDiffDraftRequest('DELETE', changeNum, patchNum, draft); + } + + /** + * @returns {boolean} Whether there are pending diff draft sends. + */ + hasPendingDiffDrafts() { + const promises = this._pendingRequests[Requests.SEND_DIFF_DRAFT]; + return promises && promises.length; + } + + /** + * @returns {!Promise<undefined>} A promise that resolves when all pending + * diff draft sends have resolved. + */ + awaitPendingDiffDrafts() { + return Promise.all(this._pendingRequests[Requests.SEND_DIFF_DRAFT] || []) + .then(() => { + this._pendingRequests[Requests.SEND_DIFF_DRAFT] = []; + }); + } + + _sendDiffDraftRequest(method, changeNum, patchNum, draft) { + const isCreate = !draft.id && method === 'PUT'; + let endpoint = '/drafts'; + let anonymizedEndpoint = endpoint; + if (draft.id) { + endpoint += '/' + draft.id; + anonymizedEndpoint += '/*'; + } + let body; + if (method === 'PUT') { + body = draft; + } + + if (!this._pendingRequests[Requests.SEND_DIFF_DRAFT]) { + this._pendingRequests[Requests.SEND_DIFF_DRAFT] = []; + } + + const req = { + changeNum, + method, + patchNum, + endpoint, + body, + anonymizedEndpoint, + }; + + const promise = this._getChangeURLAndSend(req); + this._pendingRequests[Requests.SEND_DIFF_DRAFT].push(promise); + + if (isCreate) { + return this._failForCreate200(promise); + } + + return promise; + } + + getCommitInfo(project, commit) { + return this._restApiHelper.fetchJSON({ + url: '/projects/' + encodeURIComponent(project) + + '/commits/' + encodeURIComponent(commit), + anonymizedUrl: '/projects/*/comments/*', + }); + } + + _fetchB64File(url) { + return this._restApiHelper.fetch({url: this.getBaseUrl() + url}) + .then(response => { + if (!response.ok) { + return Promise.reject(new Error(response.statusText)); + } + const type = response.headers.get('X-FYI-Content-Type'); + return response.text() + .then(text => { + return {body: text, type}; + }); + }); + } + + /** + * @param {string} changeId + * @param {string|number} patchNum + * @param {string} path + * @param {number=} opt_parentIndex + */ + getB64FileContents(changeId, patchNum, path, opt_parentIndex) { + const parent = typeof opt_parentIndex === 'number' ? + '?parent=' + opt_parentIndex : ''; + return this._changeBaseURL(changeId, patchNum).then(url => { + url = `${url}/files/${encodeURIComponent(path)}/content${parent}`; + return this._fetchB64File(url); + }); + } + + getImagesForDiff(changeNum, diff, patchRange) { + let promiseA; + let promiseB; + + if (diff.meta_a && diff.meta_a.content_type.startsWith('image/')) { + if (patchRange.basePatchNum === 'PARENT') { + // Note: we only attempt to get the image from the first parent. + promiseA = this.getB64FileContents(changeNum, patchRange.patchNum, + diff.meta_a.name, 1); + } else { + promiseA = this.getB64FileContents(changeNum, + patchRange.basePatchNum, diff.meta_a.name); + } + } else { + promiseA = Promise.resolve(null); + } + + if (diff.meta_b && diff.meta_b.content_type.startsWith('image/')) { + promiseB = this.getB64FileContents(changeNum, patchRange.patchNum, + diff.meta_b.name); + } else { + promiseB = Promise.resolve(null); + } + + return Promise.all([promiseA, promiseB]).then(results => { + const baseImage = results[0]; + const revisionImage = results[1]; + + // Sometimes the server doesn't send back the content type. + if (baseImage) { + baseImage._expectedType = diff.meta_a.content_type; + baseImage._name = diff.meta_a.name; + } + if (revisionImage) { + revisionImage._expectedType = diff.meta_b.content_type; + revisionImage._name = diff.meta_b.name; + } + + return {baseImage, revisionImage}; + }); + } + + /** + * @param {number|string} changeNum + * @param {?number|string=} opt_patchNum passed as null sometimes. + * @param {string=} opt_project + * @return {!Promise<string>} + */ + _changeBaseURL(changeNum, opt_patchNum, opt_project) { + // TODO(kaspern): For full slicer migration, app should warn with a call + // stack every time _changeBaseURL is called without a project. + const projectPromise = opt_project ? + Promise.resolve(opt_project) : + this.getFromProjectLookup(changeNum); + return projectPromise.then(project => { + let url = `/changes/${encodeURIComponent(project)}~${changeNum}`; + if (opt_patchNum) { + url += `/revisions/${opt_patchNum}`; + } + return url; + }); + } + + /** + * @suppress {checkTypes} + * Resulted in error: Promise.prototype.then does not match formal + * parameter. + */ + setChangeTopic(changeNum, topic) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', + endpoint: '/topic', + body: {topic}, + parseResponse: true, + reportUrlAsIs: true, + }); + } + + /** + * @suppress {checkTypes} + * Resulted in error: Promise.prototype.then does not match formal + * parameter. + */ + setChangeHashtag(changeNum, hashtag) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + endpoint: '/hashtags', + body: hashtag, + parseResponse: true, + reportUrlAsIs: true, + }); + } + + deleteAccountHttpPassword() { + return this._restApiHelper.send({ + method: 'DELETE', + url: '/accounts/self/password.http', + reportUrlAsIs: true, + }); + } + + /** + * @suppress {checkTypes} + * Resulted in error: Promise.prototype.then does not match formal + * parameter. + */ + generateAccountHttpPassword() { + return this._restApiHelper.send({ + method: 'PUT', + url: '/accounts/self/password.http', + body: {generate: true}, + parseResponse: true, + reportUrlAsIs: true, + }); + } + + getAccountSSHKeys() { + return this._fetchSharedCacheURL({ + url: '/accounts/self/sshkeys', + reportUrlAsIs: true, + }); + } + + addAccountSSHKey(key) { + const req = { + method: 'POST', + url: '/accounts/self/sshkeys', + body: key, + contentType: 'text/plain', + reportUrlAsIs: true, + }; + return this._restApiHelper.send(req) + .then(response => { + if (response.status < 200 && response.status >= 300) { + return Promise.reject(new Error('error')); + } + return this.getResponseObject(response); + }) + .then(obj => { + if (!obj.valid) { return Promise.reject(new Error('error')); } + return obj; + }); + } + + deleteAccountSSHKey(id) { + return this._restApiHelper.send({ + method: 'DELETE', + url: '/accounts/self/sshkeys/' + id, + anonymizedUrl: '/accounts/self/sshkeys/*', + }); + } + + getAccountGPGKeys() { + return this._restApiHelper.fetchJSON({ + url: '/accounts/self/gpgkeys', + reportUrlAsIs: true, + }); + } + + addAccountGPGKey(key) { + const req = { + method: 'POST', + url: '/accounts/self/gpgkeys', + body: key, + reportUrlAsIs: true, + }; + return this._restApiHelper.send(req) + .then(response => { + if (response.status < 200 && response.status >= 300) { + return Promise.reject(new Error('error')); + } + return this.getResponseObject(response); + }) + .then(obj => { + if (!obj) { return Promise.reject(new Error('error')); } + return obj; + }); + } + + deleteAccountGPGKey(id) { + return this._restApiHelper.send({ + method: 'DELETE', + url: '/accounts/self/gpgkeys/' + id, + anonymizedUrl: '/accounts/self/gpgkeys/*', + }); + } + + deleteVote(changeNum, account, label) { + return this._getChangeURLAndSend({ + changeNum, + method: 'DELETE', + endpoint: `/reviewers/${account}/votes/${encodeURIComponent(label)}`, + anonymizedEndpoint: '/reviewers/*/votes/*', + }); + } + + setDescription(changeNum, patchNum, desc) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', patchNum, + endpoint: '/description', + body: {description: desc}, + reportUrlAsIs: true, + }); + } + + confirmEmail(token) { + const req = { + method: 'PUT', + url: '/config/server/email.confirm', + body: {token}, + reportUrlAsIs: true, + }; + return this._restApiHelper.send(req).then(response => { + if (response.status === 204) { + return 'Email confirmed successfully.'; + } + return null; + }); + } + + getCapabilities(opt_errFn) { + return this._restApiHelper.fetchJSON({ + url: '/config/server/capabilities', + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } + + getTopMenus(opt_errFn) { + return this._fetchSharedCacheURL({ + url: '/config/server/top-menus', + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } + + setAssignee(changeNum, assignee) { + return this._getChangeURLAndSend({ + changeNum, + method: 'PUT', + endpoint: '/assignee', + body: {assignee}, + reportUrlAsIs: true, + }); + } + + deleteAssignee(changeNum) { + return this._getChangeURLAndSend({ + changeNum, + method: 'DELETE', + endpoint: '/assignee', + reportUrlAsIs: true, + }); + } + + probePath(path) { + return fetch(new Request(path, {method: 'HEAD'})) + .then(response => response.ok); + } + + /** + * @param {number|string} changeNum + * @param {number|string=} opt_message + */ + startWorkInProgress(changeNum, opt_message) { + const body = {}; + if (opt_message) { + body.message = opt_message; + } + const req = { + changeNum, + method: 'POST', + endpoint: '/wip', + body, + reportUrlAsIs: true, + }; + return this._getChangeURLAndSend(req).then(response => { + if (response.status === 204) { + return 'Change marked as Work In Progress.'; + } + }); + } + + /** + * @param {number|string} changeNum + * @param {number|string=} opt_body + * @param {function(?Response, string=)=} opt_errFn + */ + startReview(changeNum, opt_body, opt_errFn) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + endpoint: '/ready', + body: opt_body, + errFn: opt_errFn, + reportUrlAsIs: true, + }); + } + + /** + * @suppress {checkTypes} + * Resulted in error: Promise.prototype.then does not match formal + * parameter. + */ + deleteComment(changeNum, patchNum, commentID, reason) { + return this._getChangeURLAndSend({ + changeNum, + method: 'POST', + patchNum, + endpoint: `/comments/${commentID}/delete`, + body: {reason}, + parseResponse: true, + anonymizedEndpoint: '/comments/*/delete', + }); + } + + /** + * Given a changeNum, gets the change. + * + * @param {number|string} changeNum + * @param {function(?Response, string=)=} opt_errFn + * @return {!Promise<?Object>} The change + */ + getChange(changeNum, opt_errFn) { + // Cannot use _changeBaseURL, as this function is used by _projectLookup. + return this._restApiHelper.fetchJSON({ + url: `/changes/?q=change:${changeNum}`, + errFn: opt_errFn, + anonymizedUrl: '/changes/?q=change:*', + }).then(res => { + if (!res || !res.length) { return null; } + return res[0]; + }); + } + + /** + * @param {string|number} changeNum + * @param {string=} project + */ + setInProjectLookup(changeNum, project) { + if (this._projectLookup[changeNum] && + this._projectLookup[changeNum] !== project) { + console.warn('Change set with multiple project nums.' + + 'One of them must be invalid.'); + } + this._projectLookup[changeNum] = project; + } + + /** + * Checks in _projectLookup for the changeNum. If it exists, returns the + * project. If not, calls the restAPI to get the change, populates + * _projectLookup with the project for that change, and returns the project. + * + * @param {string|number} changeNum + * @return {!Promise<string|undefined>} + */ + getFromProjectLookup(changeNum) { + const project = this._projectLookup[changeNum]; + if (project) { return Promise.resolve(project); } + + const onError = response => { + // Fire a page error so that the visual 404 is displayed. + this.fire('page-error', {response}); + }; + + return this.getChange(changeNum, onError).then(change => { + if (!change || !change.project) { return; } + this.setInProjectLookup(changeNum, change.project); + return change.project; + }); + } + + /** + * Alias for _changeBaseURL.then(send). + * + * @todo(beckysiegel) clean up comments + * @param {Gerrit.ChangeSendRequest} req + * @return {!Promise<!Object>} + */ + _getChangeURLAndSend(req) { + const anonymizedBaseUrl = req.patchNum ? + ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; + const anonymizedEndpoint = req.reportEndpointAsIs ? + req.endpoint : req.anonymizedEndpoint; + + return this._changeBaseURL(req.changeNum, req.patchNum) + .then(url => this._restApiHelper.send({ + method: req.method, + url: url + req.endpoint, + body: req.body, + errFn: req.errFn, + contentType: req.contentType, + headers: req.headers, + parseResponse: req.parseResponse, + anonymizedUrl: anonymizedEndpoint ? + (anonymizedBaseUrl + anonymizedEndpoint) : undefined, + })); + } + + /** + * Alias for _changeBaseURL.then(_fetchJSON). + * + * @param {Gerrit.ChangeFetchRequest} req + * @return {!Promise<!Object>} + */ + _getChangeURLAndFetch(req, noAcceptHeader) { + const anonymizedEndpoint = req.reportEndpointAsIs ? + req.endpoint : req.anonymizedEndpoint; + const anonymizedBaseUrl = req.patchNum ? + ANONYMIZED_REVISION_BASE_URL : ANONYMIZED_CHANGE_BASE_URL; + return this._changeBaseURL(req.changeNum, req.patchNum) + .then(url => this._restApiHelper.fetchJSON({ + url: url + req.endpoint, + errFn: req.errFn, + params: req.params, + fetchOptions: req.fetchOptions, + anonymizedUrl: anonymizedEndpoint ? + (anonymizedBaseUrl + anonymizedEndpoint) : undefined, + }, noAcceptHeader)); + } + + /** + * Execute a change action or revision action on a change. + * + * @param {number} changeNum + * @param {string} method + * @param {string} endpoint + * @param {string|number|undefined} opt_patchNum + * @param {Object=} opt_payload + * @param {?function(?Response, string=)=} opt_errFn + * @return {Promise} + */ + executeChangeAction(changeNum, method, endpoint, opt_patchNum, opt_payload, + opt_errFn) { + return this._getChangeURLAndSend({ + changeNum, + method, + patchNum: opt_patchNum, + endpoint, + body: opt_payload, + errFn: opt_errFn, + }); + } + + /** + * Get blame information for the given diff. + * + * @param {string|number} changeNum + * @param {string|number} patchNum + * @param {string} path + * @param {boolean=} opt_base If true, requests blame for the base of the + * diff, rather than the revision. + * @return {!Promise<!Object>} + */ + getBlame(changeNum, patchNum, path, opt_base) { + const encodedPath = encodeURIComponent(path); + return this._getChangeURLAndFetch({ + changeNum, + endpoint: `/files/${encodedPath}/blame`, + patchNum, + params: opt_base ? {base: 't'} : undefined, + anonymizedEndpoint: '/files/*/blame', + }); + } + + /** + * Modify the given create draft request promise so that it fails and throws + * an error if the response bears HTTP status 200 instead of HTTP 201. + * + * @see Issue 7763 + * @param {Promise} promise The original promise. + * @return {Promise} The modified promise. + */ + _failForCreate200(promise) { + return promise.then(result => { + if (result.status === 200) { + // Read the response headers into an object representation. + const headers = Array.from(result.headers.entries()) + .reduce((obj, [key, val]) => { + if (!HEADER_REPORTING_BLACKLIST.test(key)) { + obj[key] = val; + } + return obj; + }, {}); + const err = new Error([ + CREATE_DRAFT_UNEXPECTED_STATUS_MESSAGE, + JSON.stringify(headers), + ].join('\n')); + // Throw the error so that it is caught by gr-reporting. + throw err; + } + return result; + }); + } + + /** + * Fetch a project dashboard definition. + * https://gerrit-review.googlesource.com/Documentation/rest-api-projects.html#get-dashboard + * + * @param {string} project + * @param {string} dashboard + * @param {function(?Response, string=)=} opt_errFn + * passed as null sometimes. + * @return {!Promise<!Object>} + */ + getDashboard(project, dashboard, opt_errFn) { + const url = '/projects/' + encodeURIComponent(project) + '/dashboards/' + + encodeURIComponent(dashboard); + return this._fetchSharedCacheURL({ + url, + errFn: opt_errFn, + anonymizedUrl: '/projects/*/dashboards/*', + }); + } + + /** + * @param {string} filter + * @return {!Promise<?Object>} + */ + getDocumentationSearches(filter) { + filter = filter.trim(); + const encodedFilter = encodeURIComponent(filter); + + // TODO(kaspern): Rename rest api from /projects/ to /repos/ once backend + // supports it. + return this._fetchSharedCacheURL({ + url: `/Documentation/?q=${encodedFilter}`, + anonymizedUrl: '/Documentation/?*', + }); + } + + getMergeable(changeNum) { + return this._getChangeURLAndFetch({ + changeNum, + endpoint: '/revisions/current/mergeable', + parseResponse: true, + reportEndpointAsIs: true, + }); + } + + deleteDraftComments(query) { + return this._restApiHelper.send({ + method: 'POST', + url: '/accounts/self/drafts:delete', + body: {query}, + }); + } +} + +customElements.define(GrRestApiInterface.is, GrRestApiInterface);
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html index 1088f7e..13ed562 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -19,17 +19,23 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rest-api-interface</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> -<link rel="import" href="gr-rest-api-interface.html"> +<script type="module" src="./gr-rest-api-interface.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-rest-api-interface.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -37,92 +43,151 @@ </template> </test-fixture> -<script> - suite('gr-rest-api-interface tests', async () => { - await readyToTest(); - let element; - let sandbox; - let ctr = 0; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-rest-api-interface.js'; +suite('gr-rest-api-interface tests', () => { + let element; + let sandbox; + let ctr = 0; - setup(() => { - // Modify CANONICAL_PATH to effectively reset cache. - ctr += 1; - window.CANONICAL_PATH = `test${ctr}`; + setup(() => { + // Modify CANONICAL_PATH to effectively reset cache. + ctr += 1; + window.CANONICAL_PATH = `test${ctr}`; - sandbox = sinon.sandbox.create(); - const testJSON = ')]}\'\n{"hello": "bonjour"}'; - sandbox.stub(window, 'fetch').returns(Promise.resolve({ - ok: true, - text() { - return Promise.resolve(testJSON); - }, - })); - // fake auth - sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true)); - element = fixture('basic'); - element._projectLookup = {}; - }); + sandbox = sinon.sandbox.create(); + const testJSON = ')]}\'\n{"hello": "bonjour"}'; + sandbox.stub(window, 'fetch').returns(Promise.resolve({ + ok: true, + text() { + return Promise.resolve(testJSON); + }, + })); + // fake auth + sandbox.stub(Gerrit.Auth, 'authCheck').returns(Promise.resolve(true)); + element = fixture('basic'); + element._projectLookup = {}; + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('parent diff comments are properly grouped', done => { - sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({ - '/COMMIT_MSG': [], - 'sieve.go': [ - { - updated: '2017-02-03 22:32:28.000000000', - message: 'this isn’t quite right', - }, - { - side: 'PARENT', - message: 'how did this work in the first place?', - updated: '2017-02-03 22:33:28.000000000', - }, - ], - })); - element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then( - obj => { - assert.equal(obj.baseComments.length, 1); - assert.deepEqual(obj.baseComments[0], { - side: 'PARENT', - message: 'how did this work in the first place?', - path: 'sieve.go', - updated: '2017-02-03 22:33:28.000000000', - }); - assert.equal(obj.comments.length, 1); - assert.deepEqual(obj.comments[0], { - message: 'this isn’t quite right', - path: 'sieve.go', - updated: '2017-02-03 22:32:28.000000000', - }); - done(); - }); - }); - - test('_setRange', () => { - const comments = [ + test('parent diff comments are properly grouped', done => { + sandbox.stub(element._restApiHelper, 'fetchJSON', () => Promise.resolve({ + '/COMMIT_MSG': [], + 'sieve.go': [ { - id: 1, + updated: '2017-02-03 22:32:28.000000000', + message: 'this isn’t quite right', + }, + { side: 'PARENT', message: 'how did this work in the first place?', - updated: '2017-02-03 22:32:28.000000000', - range: { - start_line: 1, - start_character: 1, - end_line: 2, - end_character: 1, - }, - }, - { - id: 2, - in_reply_to: 1, - message: 'this isn’t quite right', updated: '2017-02-03 22:33:28.000000000', }, - ]; - const expectedResult = { + ], + })); + element._getDiffComments('42', '', 'PARENT', 1, 'sieve.go').then( + obj => { + assert.equal(obj.baseComments.length, 1); + assert.deepEqual(obj.baseComments[0], { + side: 'PARENT', + message: 'how did this work in the first place?', + path: 'sieve.go', + updated: '2017-02-03 22:33:28.000000000', + }); + assert.equal(obj.comments.length, 1); + assert.deepEqual(obj.comments[0], { + message: 'this isn’t quite right', + path: 'sieve.go', + updated: '2017-02-03 22:32:28.000000000', + }); + done(); + }); + }); + + test('_setRange', () => { + const comments = [ + { + id: 1, + side: 'PARENT', + message: 'how did this work in the first place?', + updated: '2017-02-03 22:32:28.000000000', + range: { + start_line: 1, + start_character: 1, + end_line: 2, + end_character: 1, + }, + }, + { + id: 2, + in_reply_to: 1, + message: 'this isn’t quite right', + updated: '2017-02-03 22:33:28.000000000', + }, + ]; + const expectedResult = { + id: 2, + in_reply_to: 1, + message: 'this isn’t quite right', + updated: '2017-02-03 22:33:28.000000000', + range: { + start_line: 1, + start_character: 1, + end_line: 2, + end_character: 1, + }, + }; + const comment = comments[1]; + assert.deepEqual(element._setRange(comments, comment), expectedResult); + }); + + test('_setRanges', () => { + const comments = [ + { + id: 3, + in_reply_to: 2, + message: 'this isn’t quite right either', + updated: '2017-02-03 22:34:28.000000000', + }, + { + id: 2, + in_reply_to: 1, + message: 'this isn’t quite right', + updated: '2017-02-03 22:33:28.000000000', + }, + { + id: 1, + side: 'PARENT', + message: 'how did this work in the first place?', + updated: '2017-02-03 22:32:28.000000000', + range: { + start_line: 1, + start_character: 1, + end_line: 2, + end_character: 1, + }, + }, + ]; + const expectedResult = [ + { + id: 1, + side: 'PARENT', + message: 'how did this work in the first place?', + updated: '2017-02-03 22:32:28.000000000', + range: { + start_line: 1, + start_character: 1, + end_line: 2, + end_character: 1, + }, + }, + { id: 2, in_reply_to: 1, message: 'this isn’t quite right', @@ -133,1347 +198,1291 @@ end_line: 2, end_character: 1, }, - }; - const comment = comments[1]; - assert.deepEqual(element._setRange(comments, comment), expectedResult); - }); + }, + { + id: 3, + in_reply_to: 2, + message: 'this isn’t quite right either', + updated: '2017-02-03 22:34:28.000000000', + range: { + start_line: 1, + start_character: 1, + end_line: 2, + end_character: 1, + }, + }, + ]; + assert.deepEqual(element._setRanges(comments), expectedResult); + }); - test('_setRanges', () => { - const comments = [ - { - id: 3, - in_reply_to: 2, - message: 'this isn’t quite right either', - updated: '2017-02-03 22:34:28.000000000', - }, - { - id: 2, - in_reply_to: 1, - message: 'this isn’t quite right', - updated: '2017-02-03 22:33:28.000000000', - }, - { - id: 1, - side: 'PARENT', - message: 'how did this work in the first place?', - updated: '2017-02-03 22:32:28.000000000', - range: { - start_line: 1, - start_character: 1, - end_line: 2, - end_character: 1, - }, - }, - ]; - const expectedResult = [ - { - id: 1, - side: 'PARENT', - message: 'how did this work in the first place?', - updated: '2017-02-03 22:32:28.000000000', - range: { - start_line: 1, - start_character: 1, - end_line: 2, - end_character: 1, - }, - }, - { - id: 2, - in_reply_to: 1, - message: 'this isn’t quite right', - updated: '2017-02-03 22:33:28.000000000', - range: { - start_line: 1, - start_character: 1, - end_line: 2, - end_character: 1, - }, - }, - { - id: 3, - in_reply_to: 2, - message: 'this isn’t quite right either', - updated: '2017-02-03 22:34:28.000000000', - range: { - start_line: 1, - start_character: 1, - end_line: 2, - end_character: 1, - }, - }, - ]; - assert.deepEqual(element._setRanges(comments), expectedResult); - }); - - test('differing patch diff comments are properly grouped', done => { - sandbox.stub(element, 'getFromProjectLookup') - .returns(Promise.resolve('test')); - sandbox.stub(element._restApiHelper, 'fetchJSON', request => { - const url = request.url; - if (url === '/changes/test~42/revisions/1') { - return Promise.resolve({ - '/COMMIT_MSG': [], - 'sieve.go': [ - { - message: 'this isn’t quite right', - updated: '2017-02-03 22:32:28.000000000', - }, - { - side: 'PARENT', - message: 'how did this work in the first place?', - updated: '2017-02-03 22:33:28.000000000', - }, - ], - }); - } else if (url === '/changes/test~42/revisions/2') { - return Promise.resolve({ - '/COMMIT_MSG': [], - 'sieve.go': [ - { - message: 'What on earth are you thinking, here?', - updated: '2017-02-03 22:32:28.000000000', - }, - { - side: 'PARENT', - message: 'Yeah not sure how this worked either?', - updated: '2017-02-03 22:33:28.000000000', - }, - { - message: '¯\\_(ツ)_/¯', - updated: '2017-02-04 22:33:28.000000000', - }, - ], - }); - } - }); - element._getDiffComments('42', '', 1, 2, 'sieve.go').then( - obj => { - assert.equal(obj.baseComments.length, 1); - assert.deepEqual(obj.baseComments[0], { + test('differing patch diff comments are properly grouped', done => { + sandbox.stub(element, 'getFromProjectLookup') + .returns(Promise.resolve('test')); + sandbox.stub(element._restApiHelper, 'fetchJSON', request => { + const url = request.url; + if (url === '/changes/test~42/revisions/1') { + return Promise.resolve({ + '/COMMIT_MSG': [], + 'sieve.go': [ + { message: 'this isn’t quite right', - path: 'sieve.go', updated: '2017-02-03 22:32:28.000000000', - }); - assert.equal(obj.comments.length, 2); - assert.deepEqual(obj.comments[0], { + }, + { + side: 'PARENT', + message: 'how did this work in the first place?', + updated: '2017-02-03 22:33:28.000000000', + }, + ], + }); + } else if (url === '/changes/test~42/revisions/2') { + return Promise.resolve({ + '/COMMIT_MSG': [], + 'sieve.go': [ + { message: 'What on earth are you thinking, here?', - path: 'sieve.go', updated: '2017-02-03 22:32:28.000000000', - }); - assert.deepEqual(obj.comments[1], { + }, + { + side: 'PARENT', + message: 'Yeah not sure how this worked either?', + updated: '2017-02-03 22:33:28.000000000', + }, + { message: '¯\\_(ツ)_/¯', - path: 'sieve.go', updated: '2017-02-04 22:33:28.000000000', - }); + }, + ], + }); + } + }); + element._getDiffComments('42', '', 1, 2, 'sieve.go').then( + obj => { + assert.equal(obj.baseComments.length, 1); + assert.deepEqual(obj.baseComments[0], { + message: 'this isn’t quite right', + path: 'sieve.go', + updated: '2017-02-03 22:32:28.000000000', + }); + assert.equal(obj.comments.length, 2); + assert.deepEqual(obj.comments[0], { + message: 'What on earth are you thinking, here?', + path: 'sieve.go', + updated: '2017-02-03 22:32:28.000000000', + }); + assert.deepEqual(obj.comments[1], { + message: '¯\\_(ツ)_/¯', + path: 'sieve.go', + updated: '2017-02-04 22:33:28.000000000', + }); + done(); + }); + }); + + test('special file path sorting', () => { + assert.deepEqual( + ['.b', '/COMMIT_MSG', '.a', 'file'].sort( + element.specialFilePathCompare), + ['/COMMIT_MSG', '.a', '.b', 'file']); + + assert.deepEqual( + ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort( + element.specialFilePathCompare), + ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']); + + assert.deepEqual( + ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort( + element.specialFilePathCompare), + ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']); + + assert.deepEqual( + ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort( + element.specialFilePathCompare), + ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']); + + assert.deepEqual( + ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort( + element.specialFilePathCompare), + ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']); + + // Regression test for Issue 4448. + assert.deepEqual( + [ + 'minidump/minidump_memory_writer.cc', + 'minidump/minidump_memory_writer.h', + 'minidump/minidump_thread_writer.cc', + 'minidump/minidump_thread_writer.h', + ].sort(element.specialFilePathCompare), + [ + 'minidump/minidump_memory_writer.h', + 'minidump/minidump_memory_writer.cc', + 'minidump/minidump_thread_writer.h', + 'minidump/minidump_thread_writer.cc', + ]); + + // Regression test for Issue 4545. + assert.deepEqual( + [ + 'task_test.go', + 'task.go', + ].sort(element.specialFilePathCompare), + [ + 'task.go', + 'task_test.go', + ]); + }); + + suite('rebase action', () => { + let resolve_fetchJSON; + setup(() => { + sandbox.stub(element._restApiHelper, 'fetchJSON').returns( + new Promise(resolve => { + resolve_fetchJSON = resolve; + })); + }); + + test('no rebase on current', done => { + element.getChangeRevisionActions('42', '1337').then( + response => { + assert.isTrue(response.rebase.enabled); + assert.isFalse(response.rebase.rebaseOnCurrent); done(); }); + resolve_fetchJSON({rebase: {}}); }); - test('special file path sorting', () => { - assert.deepEqual( - ['.b', '/COMMIT_MSG', '.a', 'file'].sort( - element.specialFilePathCompare), - ['/COMMIT_MSG', '.a', '.b', 'file']); - - assert.deepEqual( - ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.h'].sort( - element.specialFilePathCompare), - ['/COMMIT_MSG', '.b', 'foo/bar/baz.h', 'foo/bar/baz.cc']); - - assert.deepEqual( - ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hpp'].sort( - element.specialFilePathCompare), - ['/COMMIT_MSG', '.b', 'foo/bar/baz.hpp', 'foo/bar/baz.cc']); - - assert.deepEqual( - ['.b', '/COMMIT_MSG', 'foo/bar/baz.cc', 'foo/bar/baz.hxx'].sort( - element.specialFilePathCompare), - ['/COMMIT_MSG', '.b', 'foo/bar/baz.hxx', 'foo/bar/baz.cc']); - - assert.deepEqual( - ['foo/bar.h', 'foo/bar.hxx', 'foo/bar.hpp'].sort( - element.specialFilePathCompare), - ['foo/bar.h', 'foo/bar.hpp', 'foo/bar.hxx']); - - // Regression test for Issue 4448. - assert.deepEqual( - [ - 'minidump/minidump_memory_writer.cc', - 'minidump/minidump_memory_writer.h', - 'minidump/minidump_thread_writer.cc', - 'minidump/minidump_thread_writer.h', - ].sort(element.specialFilePathCompare), - [ - 'minidump/minidump_memory_writer.h', - 'minidump/minidump_memory_writer.cc', - 'minidump/minidump_thread_writer.h', - 'minidump/minidump_thread_writer.cc', - ]); - - // Regression test for Issue 4545. - assert.deepEqual( - [ - 'task_test.go', - 'task.go', - ].sort(element.specialFilePathCompare), - [ - 'task.go', - 'task_test.go', - ]); - }); - - suite('rebase action', () => { - let resolve_fetchJSON; - setup(() => { - sandbox.stub(element._restApiHelper, 'fetchJSON').returns( - new Promise(resolve => { - resolve_fetchJSON = resolve; - })); - }); - - test('no rebase on current', done => { - element.getChangeRevisionActions('42', '1337').then( - response => { - assert.isTrue(response.rebase.enabled); - assert.isFalse(response.rebase.rebaseOnCurrent); - done(); - }); - resolve_fetchJSON({rebase: {}}); - }); - - test('rebase on current', done => { - element.getChangeRevisionActions('42', '1337').then( - response => { - assert.isTrue(response.rebase.enabled); - assert.isTrue(response.rebase.rebaseOnCurrent); - done(); - }); - resolve_fetchJSON({rebase: {enabled: true}}); - }); - }); - - test('server error', done => { - const getResponseObjectStub = sandbox.stub(element, 'getResponseObject'); - window.fetch.returns(Promise.resolve({ok: false})); - const serverErrorEventPromise = new Promise(resolve => { - element.addEventListener('server-error', resolve); - }); - - element._restApiHelper.fetchJSON({}).then(response => { - assert.isUndefined(response); - assert.isTrue(getResponseObjectStub.notCalled); - serverErrorEventPromise.then(() => done()); - }); - }); - - test('legacy n,z key in change url is replaced', () => { - const stub = sandbox.stub(element._restApiHelper, 'fetchJSON') - .returns(Promise.resolve([])); - element.getChanges(1, null, 'n,z'); - assert.equal(stub.lastCall.args[0].params.S, 0); - }); - - test('saveDiffPreferences invalidates cache line', () => { - const cacheKey = '/accounts/self/preferences.diff'; - const sendStub = sandbox.stub(element._restApiHelper, 'send'); - element._cache.set(cacheKey, {tab_size: 4}); - element.saveDiffPreferences({tab_size: 8}); - assert.isTrue(sendStub.called); - assert.isFalse(element._restApiHelper._cache.has(cacheKey)); - }); - - test('getAccount when resp is null does not add anything to the cache', - done => { - const cacheKey = '/accounts/self/detail'; - const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', - () => Promise.resolve()); - - element.getAccount().then(() => { - assert.isTrue(stub.called); - assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + test('rebase on current', done => { + element.getChangeRevisionActions('42', '1337').then( + response => { + assert.isTrue(response.rebase.enabled); + assert.isTrue(response.rebase.rebaseOnCurrent); done(); }); + resolve_fetchJSON({rebase: {enabled: true}}); + }); + }); - element._restApiHelper._cache.set(cacheKey, 'fake cache'); - stub.lastCall.args[0].errFn(); + test('server error', done => { + const getResponseObjectStub = sandbox.stub(element, 'getResponseObject'); + window.fetch.returns(Promise.resolve({ok: false})); + const serverErrorEventPromise = new Promise(resolve => { + element.addEventListener('server-error', resolve); + }); + + element._restApiHelper.fetchJSON({}).then(response => { + assert.isUndefined(response); + assert.isTrue(getResponseObjectStub.notCalled); + serverErrorEventPromise.then(() => done()); + }); + }); + + test('legacy n,z key in change url is replaced', () => { + const stub = sandbox.stub(element._restApiHelper, 'fetchJSON') + .returns(Promise.resolve([])); + element.getChanges(1, null, 'n,z'); + assert.equal(stub.lastCall.args[0].params.S, 0); + }); + + test('saveDiffPreferences invalidates cache line', () => { + const cacheKey = '/accounts/self/preferences.diff'; + const sendStub = sandbox.stub(element._restApiHelper, 'send'); + element._cache.set(cacheKey, {tab_size: 4}); + element.saveDiffPreferences({tab_size: 8}); + assert.isTrue(sendStub.called); + assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + }); + + test('getAccount when resp is null does not add anything to the cache', + done => { + const cacheKey = '/accounts/self/detail'; + const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', + () => Promise.resolve()); + + element.getAccount().then(() => { + assert.isTrue(stub.called); + assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + done(); }); - test('getAccount does not add to the cache when resp.status is 403', - done => { - const cacheKey = '/accounts/self/detail'; - const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', - () => Promise.resolve()); - - element.getAccount().then(() => { - assert.isTrue(stub.called); - assert.isFalse(element._restApiHelper._cache.has(cacheKey)); - done(); - }); - element._cache.set(cacheKey, 'fake cache'); - stub.lastCall.args[0].errFn({status: 403}); - }); - - test('getAccount when resp is successful', done => { - const cacheKey = '/accounts/self/detail'; - const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', - () => Promise.resolve()); - - element.getAccount().then(response => { - assert.isTrue(stub.called); - assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache'); - done(); + element._restApiHelper._cache.set(cacheKey, 'fake cache'); + stub.lastCall.args[0].errFn(); }); - element._restApiHelper._cache.set(cacheKey, 'fake cache'); - stub.lastCall.args[0].errFn({}); - }); + test('getAccount does not add to the cache when resp.status is 403', + done => { + const cacheKey = '/accounts/self/detail'; + const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', + () => Promise.resolve()); - const preferenceSetup = function(testJSON, loggedIn, smallScreen) { - sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn)); - sandbox.stub(element, '_isNarrowScreen', () => smallScreen); - sandbox.stub( - element._restApiHelper, - 'fetchCacheURL', - () => Promise.resolve(testJSON)); - }; - - test('getPreferences returns correctly on small screens logged in', - done => { - const testJSON = {diff_view: 'SIDE_BY_SIDE'}; - const loggedIn = true; - const smallScreen = true; - - preferenceSetup(testJSON, loggedIn, smallScreen); - - element.getPreferences().then(obj => { - assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); - assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); - done(); - }); + element.getAccount().then(() => { + assert.isTrue(stub.called); + assert.isFalse(element._restApiHelper._cache.has(cacheKey)); + done(); }); - - test('getPreferences returns correctly on small screens not logged in', - done => { - const testJSON = {diff_view: 'SIDE_BY_SIDE'}; - const loggedIn = false; - const smallScreen = true; - - preferenceSetup(testJSON, loggedIn, smallScreen); - element.getPreferences().then(obj => { - assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); - assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); - done(); - }); - }); - - test('getPreferences returns correctly on larger screens logged in', - done => { - const testJSON = {diff_view: 'UNIFIED_DIFF'}; - const loggedIn = true; - const smallScreen = false; - - preferenceSetup(testJSON, loggedIn, smallScreen); - - element.getPreferences().then(obj => { - assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); - assert.equal(obj.diff_view, 'UNIFIED_DIFF'); - done(); - }); - }); - - test('getPreferences returns correctly on larger screens not logged in', - done => { - const testJSON = {diff_view: 'UNIFIED_DIFF'}; - const loggedIn = false; - const smallScreen = false; - - preferenceSetup(testJSON, loggedIn, smallScreen); - - element.getPreferences().then(obj => { - assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE'); - assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); - done(); - }); - }); - - test('savPreferences normalizes download scheme', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send'); - element.savePreferences({download_scheme: 'HTTP'}); - assert.isTrue(sendStub.called); - assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http'); - }); - - test('getDiffPreferences returns correct defaults', done => { - sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false)); - - element.getDiffPreferences().then(obj => { - assert.equal(obj.auto_hide_diff_table_header, true); - assert.equal(obj.context, 10); - assert.equal(obj.cursor_blink_rate, 0); - assert.equal(obj.font_size, 12); - assert.equal(obj.ignore_whitespace, 'IGNORE_NONE'); - assert.equal(obj.intraline_difference, true); - assert.equal(obj.line_length, 100); - assert.equal(obj.line_wrapping, false); - assert.equal(obj.show_line_endings, true); - assert.equal(obj.show_tabs, true); - assert.equal(obj.show_whitespace_errors, true); - assert.equal(obj.syntax_highlighting, true); - assert.equal(obj.tab_size, 8); - assert.equal(obj.theme, 'DEFAULT'); - done(); + element._cache.set(cacheKey, 'fake cache'); + stub.lastCall.args[0].errFn({status: 403}); }); + + test('getAccount when resp is successful', done => { + const cacheKey = '/accounts/self/detail'; + const stub = sandbox.stub(element._restApiHelper, 'fetchCacheURL', + () => Promise.resolve()); + + element.getAccount().then(response => { + assert.isTrue(stub.called); + assert.equal(element._restApiHelper._cache.get(cacheKey), 'fake cache'); + done(); }); + element._restApiHelper._cache.set(cacheKey, 'fake cache'); - test('saveDiffPreferences set show_tabs to false', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send'); - element.saveDiffPreferences({show_tabs: false}); - assert.isTrue(sendStub.called); - assert.equal(sendStub.lastCall.args[0].body.show_tabs, false); - }); + stub.lastCall.args[0].errFn({}); + }); - test('getEditPreferences returns correct defaults', done => { - sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false)); + const preferenceSetup = function(testJSON, loggedIn, smallScreen) { + sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(loggedIn)); + sandbox.stub(element, '_isNarrowScreen', () => smallScreen); + sandbox.stub( + element._restApiHelper, + 'fetchCacheURL', + () => Promise.resolve(testJSON)); + }; - element.getEditPreferences().then(obj => { - assert.equal(obj.auto_close_brackets, false); - assert.equal(obj.cursor_blink_rate, 0); - assert.equal(obj.hide_line_numbers, false); - assert.equal(obj.hide_top_menu, false); - assert.equal(obj.indent_unit, 2); - assert.equal(obj.indent_with_tabs, false); - assert.equal(obj.key_map_type, 'DEFAULT'); - assert.equal(obj.line_length, 100); - assert.equal(obj.line_wrapping, false); - assert.equal(obj.match_brackets, true); - assert.equal(obj.show_base, false); - assert.equal(obj.show_tabs, true); - assert.equal(obj.show_whitespace_errors, true); - assert.equal(obj.syntax_highlighting, true); - assert.equal(obj.tab_size, 8); - assert.equal(obj.theme, 'DEFAULT'); - done(); + test('getPreferences returns correctly on small screens logged in', + done => { + const testJSON = {diff_view: 'SIDE_BY_SIDE'}; + const loggedIn = true; + const smallScreen = true; + + preferenceSetup(testJSON, loggedIn, smallScreen); + + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); }); - }); - test('saveEditPreferences set show_tabs to false', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send'); - element.saveEditPreferences({show_tabs: false}); - assert.isTrue(sendStub.called); - assert.equal(sendStub.lastCall.args[0].body.show_tabs, false); - }); + test('getPreferences returns correctly on small screens not logged in', + done => { + const testJSON = {diff_view: 'SIDE_BY_SIDE'}; + const loggedIn = false; + const smallScreen = true; - test('confirmEmail', () => { - const sendStub = sandbox.spy(element._restApiHelper, 'send'); - element.confirmEmail('foo'); + preferenceSetup(testJSON, loggedIn, smallScreen); + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); + + test('getPreferences returns correctly on larger screens logged in', + done => { + const testJSON = {diff_view: 'UNIFIED_DIFF'}; + const loggedIn = true; + const smallScreen = false; + + preferenceSetup(testJSON, loggedIn, smallScreen); + + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'UNIFIED_DIFF'); + assert.equal(obj.diff_view, 'UNIFIED_DIFF'); + done(); + }); + }); + + test('getPreferences returns correctly on larger screens not logged in', + done => { + const testJSON = {diff_view: 'UNIFIED_DIFF'}; + const loggedIn = false; + const smallScreen = false; + + preferenceSetup(testJSON, loggedIn, smallScreen); + + element.getPreferences().then(obj => { + assert.equal(obj.default_diff_view, 'SIDE_BY_SIDE'); + assert.equal(obj.diff_view, 'SIDE_BY_SIDE'); + done(); + }); + }); + + test('savPreferences normalizes download scheme', () => { + const sendStub = sandbox.stub(element._restApiHelper, 'send'); + element.savePreferences({download_scheme: 'HTTP'}); + assert.isTrue(sendStub.called); + assert.equal(sendStub.lastCall.args[0].body.download_scheme, 'http'); + }); + + test('getDiffPreferences returns correct defaults', done => { + sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false)); + + element.getDiffPreferences().then(obj => { + assert.equal(obj.auto_hide_diff_table_header, true); + assert.equal(obj.context, 10); + assert.equal(obj.cursor_blink_rate, 0); + assert.equal(obj.font_size, 12); + assert.equal(obj.ignore_whitespace, 'IGNORE_NONE'); + assert.equal(obj.intraline_difference, true); + assert.equal(obj.line_length, 100); + assert.equal(obj.line_wrapping, false); + assert.equal(obj.show_line_endings, true); + assert.equal(obj.show_tabs, true); + assert.equal(obj.show_whitespace_errors, true); + assert.equal(obj.syntax_highlighting, true); + assert.equal(obj.tab_size, 8); + assert.equal(obj.theme, 'DEFAULT'); + done(); + }); + }); + + test('saveDiffPreferences set show_tabs to false', () => { + const sendStub = sandbox.stub(element._restApiHelper, 'send'); + element.saveDiffPreferences({show_tabs: false}); + assert.isTrue(sendStub.called); + assert.equal(sendStub.lastCall.args[0].body.show_tabs, false); + }); + + test('getEditPreferences returns correct defaults', done => { + sandbox.stub(element, 'getLoggedIn', () => Promise.resolve(false)); + + element.getEditPreferences().then(obj => { + assert.equal(obj.auto_close_brackets, false); + assert.equal(obj.cursor_blink_rate, 0); + assert.equal(obj.hide_line_numbers, false); + assert.equal(obj.hide_top_menu, false); + assert.equal(obj.indent_unit, 2); + assert.equal(obj.indent_with_tabs, false); + assert.equal(obj.key_map_type, 'DEFAULT'); + assert.equal(obj.line_length, 100); + assert.equal(obj.line_wrapping, false); + assert.equal(obj.match_brackets, true); + assert.equal(obj.show_base, false); + assert.equal(obj.show_tabs, true); + assert.equal(obj.show_whitespace_errors, true); + assert.equal(obj.syntax_highlighting, true); + assert.equal(obj.tab_size, 8); + assert.equal(obj.theme, 'DEFAULT'); + done(); + }); + }); + + test('saveEditPreferences set show_tabs to false', () => { + const sendStub = sandbox.stub(element._restApiHelper, 'send'); + element.saveEditPreferences({show_tabs: false}); + assert.isTrue(sendStub.called); + assert.equal(sendStub.lastCall.args[0].body.show_tabs, false); + }); + + test('confirmEmail', () => { + const sendStub = sandbox.spy(element._restApiHelper, 'send'); + element.confirmEmail('foo'); + assert.isTrue(sendStub.calledOnce); + assert.equal(sendStub.lastCall.args[0].method, 'PUT'); + assert.equal(sendStub.lastCall.args[0].url, + '/config/server/email.confirm'); + assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'}); + }); + + test('setAccountStatus', () => { + const sendStub = sandbox.stub(element._restApiHelper, 'send') + .returns(Promise.resolve('OOO')); + element._cache.set('/accounts/self/detail', {}); + return element.setAccountStatus('OOO').then(() => { assert.isTrue(sendStub.calledOnce); assert.equal(sendStub.lastCall.args[0].method, 'PUT'); assert.equal(sendStub.lastCall.args[0].url, - '/config/server/email.confirm'); - assert.deepEqual(sendStub.lastCall.args[0].body, {token: 'foo'}); - }); - - test('setAccountStatus', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send') - .returns(Promise.resolve('OOO')); - element._cache.set('/accounts/self/detail', {}); - return element.setAccountStatus('OOO').then(() => { - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].method, 'PUT'); - assert.equal(sendStub.lastCall.args[0].url, - '/accounts/self/status'); - assert.deepEqual(sendStub.lastCall.args[0].body, - {status: 'OOO'}); - assert.deepEqual(element._restApiHelper - ._cache.get('/accounts/self/detail'), - {status: 'OOO'}); - }); - }); - - suite('draft comments', () => { - test('_sendDiffDraftRequest pending requests tracked', () => { - const obj = element._pendingRequests; - sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise()); - assert.notOk(element.hasPendingDiffDrafts()); - - element._sendDiffDraftRequest(null, null, null, {}); - assert.equal(obj.sendDiffDraft.length, 1); - assert.isTrue(!!element.hasPendingDiffDrafts()); - - element._sendDiffDraftRequest(null, null, null, {}); - assert.equal(obj.sendDiffDraft.length, 2); - assert.isTrue(!!element.hasPendingDiffDrafts()); - - for (const promise of obj.sendDiffDraft) { promise.resolve(); } - - return element.awaitPendingDiffDrafts().then(() => { - assert.equal(obj.sendDiffDraft.length, 0); - assert.isFalse(!!element.hasPendingDiffDrafts()); - }); - }); - - suite('_failForCreate200', () => { - test('_sendDiffDraftRequest checks for 200 on create', () => { - const sendPromise = Promise.resolve(); - sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise); - const failStub = sandbox.stub(element, '_failForCreate200') - .returns(Promise.resolve()); - return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => { - assert.isTrue(failStub.calledOnce); - assert.isTrue(failStub.calledWithExactly(sendPromise)); - }); - }); - - test('_sendDiffDraftRequest no checks for 200 on non create', () => { - sandbox.stub(element, '_getChangeURLAndSend') - .returns(Promise.resolve()); - const failStub = sandbox.stub(element, '_failForCreate200') - .returns(Promise.resolve()); - return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'}) - .then(() => { - assert.isFalse(failStub.called); - }); - }); - - test('_failForCreate200 fails on 200', done => { - const result = { - ok: true, - status: 200, - headers: {entries: () => [ - ['Set-CoOkiE', 'secret'], - ['Innocuous', 'hello'], - ]}, - }; - element._failForCreate200(Promise.resolve(result)) - .then(() => { - assert.isTrue(false, 'Promise should not resolve'); - }) - .catch(e => { - assert.isOk(e); - assert.include(e.message, 'Saving draft resulted in HTTP 200'); - assert.include(e.message, 'hello'); - assert.notInclude(e.message, 'secret'); - done(); - }); - }); - - test('_failForCreate200 does not fail on 201', done => { - const result = { - ok: true, - status: 201, - headers: {entries: () => []}, - }; - element._failForCreate200(Promise.resolve(result)) - .then(() => { - done(); - }) - .catch(e => { - assert.isTrue(false, 'Promise should not fail'); - }); - }); - }); - }); - - test('saveChangeEdit', () => { - element._projectLookup = {1: 'test'}; - const change_num = '1'; - const file_name = 'index.php'; - const file_contents = '<?php'; - sandbox.stub(element._restApiHelper, 'send').returns( - Promise.resolve([change_num, file_name, file_contents])); - sandbox.stub(element, 'getResponseObject') - .returns(Promise.resolve([change_num, file_name, file_contents])); - element._cache.set('/changes/' + change_num + '/edit/' + file_name, {}); - return element.saveChangeEdit(change_num, file_name, file_contents) - .then(() => { - assert.isTrue(element._restApiHelper.send.calledOnce); - assert.equal(element._restApiHelper.send.lastCall.args[0].method, - 'PUT'); - assert.equal(element._restApiHelper.send.lastCall.args[0].url, - '/changes/test~1/edit/' + file_name); - assert.equal(element._restApiHelper.send.lastCall.args[0].body, - file_contents); - }); - }); - - test('putChangeCommitMessage', () => { - element._projectLookup = {1: 'test'}; - const change_num = '1'; - const message = 'this is a commit message'; - sandbox.stub(element._restApiHelper, 'send').returns( - Promise.resolve([change_num, message])); - sandbox.stub(element, 'getResponseObject') - .returns(Promise.resolve([change_num, message])); - element._cache.set('/changes/' + change_num + '/message', {}); - return element.putChangeCommitMessage(change_num, message).then(() => { - assert.isTrue(element._restApiHelper.send.calledOnce); - assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT'); - assert.equal(element._restApiHelper.send.lastCall.args[0].url, - '/changes/test~1/message'); - assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body, - {message}); - }); - }); - - test('deleteChangeCommitMessage', () => { - element._projectLookup = {1: 'test'}; - const change_num = '1'; - const messageId = 'abc'; - sandbox.stub(element._restApiHelper, 'send').returns( - Promise.resolve([change_num, messageId])); - sandbox.stub(element, 'getResponseObject') - .returns(Promise.resolve([change_num, messageId])); - return element.deleteChangeCommitMessage(change_num, messageId).then(() => { - assert.isTrue(element._restApiHelper.send.calledOnce); - assert.equal( - element._restApiHelper.send.lastCall.args[0].method, - 'DELETE' - ); - assert.equal(element._restApiHelper.send.lastCall.args[0].url, - '/changes/test~1/messages/abc'); - }); - }); - - test('startWorkInProgress', () => { - const sendStub = sandbox.stub(element, '_getChangeURLAndSend') - .returns(Promise.resolve('ok')); - element.startWorkInProgress('42'); - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].changeNum, '42'); - assert.equal(sendStub.lastCall.args[0].method, 'POST'); - assert.isNotOk(sendStub.lastCall.args[0].patchNum); - assert.equal(sendStub.lastCall.args[0].endpoint, '/wip'); - assert.deepEqual(sendStub.lastCall.args[0].body, {}); - - element.startWorkInProgress('42', 'revising...'); - assert.isTrue(sendStub.calledTwice); - assert.equal(sendStub.lastCall.args[0].changeNum, '42'); - assert.equal(sendStub.lastCall.args[0].method, 'POST'); - assert.isNotOk(sendStub.lastCall.args[0].patchNum); - assert.equal(sendStub.lastCall.args[0].endpoint, '/wip'); + '/accounts/self/status'); assert.deepEqual(sendStub.lastCall.args[0].body, - {message: 'revising...'}); + {status: 'OOO'}); + assert.deepEqual(element._restApiHelper + ._cache.get('/accounts/self/detail'), + {status: 'OOO'}); }); + }); - test('startReview', () => { - const sendStub = sandbox.stub(element, '_getChangeURLAndSend') - .returns(Promise.resolve({})); - element.startReview('42', {message: 'Please review.'}); - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].changeNum, '42'); - assert.equal(sendStub.lastCall.args[0].method, 'POST'); - assert.isNotOk(sendStub.lastCall.args[0].patchNum); - assert.equal(sendStub.lastCall.args[0].endpoint, '/ready'); - assert.deepEqual(sendStub.lastCall.args[0].body, - {message: 'Please review.'}); - }); + suite('draft comments', () => { + test('_sendDiffDraftRequest pending requests tracked', () => { + const obj = element._pendingRequests; + sandbox.stub(element, '_getChangeURLAndSend', () => mockPromise()); + assert.notOk(element.hasPendingDiffDrafts()); - test('deleteComment', () => { - const sendStub = sandbox.stub(element, '_getChangeURLAndSend') - .returns(Promise.resolve('some response')); - return element.deleteComment('foo', 'bar', '01234', 'removal reason') - .then(response => { - assert.equal(response, 'some response'); - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].changeNum, 'foo'); - assert.equal(sendStub.lastCall.args[0].method, 'POST'); - assert.equal(sendStub.lastCall.args[0].patchNum, 'bar'); - assert.equal(sendStub.lastCall.args[0].endpoint, - '/comments/01234/delete'); - assert.deepEqual(sendStub.lastCall.args[0].body, - {reason: 'removal reason'}); - }); - }); + element._sendDiffDraftRequest(null, null, null, {}); + assert.equal(obj.sendDiffDraft.length, 1); + assert.isTrue(!!element.hasPendingDiffDrafts()); - test('createRepo encodes name', () => { - const sendStub = sandbox.stub(element._restApiHelper, 'send') - .returns(Promise.resolve()); - return element.createRepo({name: 'x/y'}).then(() => { - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy'); + element._sendDiffDraftRequest(null, null, null, {}); + assert.equal(obj.sendDiffDraft.length, 2); + assert.isTrue(!!element.hasPendingDiffDrafts()); + + for (const promise of obj.sendDiffDraft) { promise.resolve(); } + + return element.awaitPendingDiffDrafts().then(() => { + assert.equal(obj.sendDiffDraft.length, 0); + assert.isFalse(!!element.hasPendingDiffDrafts()); }); }); - test('queryChangeFiles', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => { - assert.equal(fetchStub.lastCall.args[0].changeNum, '42'); - assert.equal(fetchStub.lastCall.args[0].endpoint, - '/files?q=test%2Fpath.js'); - assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit'); - }); - }); - - test('normal use', () => { - const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only'; - - assert.equal(element._getReposUrl('test', 25), - '/projects/?n=26&S=0&query=test'); - - assert.equal(element._getReposUrl(null, 25), - `/projects/?n=26&S=0&query=${defaultQuery}`); - - assert.equal(element._getReposUrl('test', 25, 25), - '/projects/?n=26&S=25&query=test'); - }); - - test('invalidateReposCache', () => { - const url = '/projects/?n=26&S=0&query=test'; - - element._cache.set(url, {}); - - element.invalidateReposCache(); - - assert.isUndefined(element._sharedFetchPromises[url]); - - assert.isFalse(element._cache.has(url)); - }); - - test('invalidateAccountsCache', () => { - const url = '/accounts/self/detail'; - - element._cache.set(url, {}); - - element.invalidateAccountsCache(); - - assert.isUndefined(element._sharedFetchPromises[url]); - - assert.isFalse(element._cache.has(url)); - }); - - suite('getRepos', () => { - const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only'; - let fetchCacheURLStub; - setup(() => { - fetchCacheURLStub = - sandbox.stub(element._restApiHelper, 'fetchCacheURL'); - }); - - test('normal use', () => { - element.getRepos('test', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=test'); - - element.getRepos(null, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - `/projects/?n=26&S=0&query=${defaultQuery}`); - - element.getRepos('test', 25, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=25&query=test'); - }); - - test('with blank', () => { - element.getRepos('test/test', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest'); - }); - - test('with hyphen', () => { - element.getRepos('foo-bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); - }); - - test('with leading hyphen', () => { - element.getRepos('-bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=inname%3Abar'); - }); - - test('with trailing hyphen', () => { - element.getRepos('foo-bar-', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); - }); - - test('with underscore', () => { - element.getRepos('foo_bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); - }); - - test('with underscore', () => { - element.getRepos('foo_bar', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); - }); - - test('hyphen only', () => { - element.getRepos('-', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - `/projects/?n=26&S=0&query=${defaultQuery}`); - }); - }); - - test('_getGroupsUrl normal use', () => { - assert.equal(element._getGroupsUrl('test', 25), - '/groups/?n=26&S=0&m=test'); - - assert.equal(element._getGroupsUrl(null, 25), - '/groups/?n=26&S=0'); - - assert.equal(element._getGroupsUrl('test', 25, 25), - '/groups/?n=26&S=25&m=test'); - }); - - test('invalidateGroupsCache', () => { - const url = '/groups/?n=26&S=0&m=test'; - - element._cache.set(url, {}); - - element.invalidateGroupsCache(); - - assert.isUndefined(element._sharedFetchPromises[url]); - - assert.isFalse(element._cache.has(url)); - }); - - suite('getGroups', () => { - let fetchCacheURLStub; - setup(() => { - fetchCacheURLStub = - sandbox.stub(element._restApiHelper, 'fetchCacheURL'); - }); - - test('normal use', () => { - element.getGroups('test', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/groups/?n=26&S=0&m=test'); - - element.getGroups(null, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/groups/?n=26&S=0'); - - element.getGroups('test', 25, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/groups/?n=26&S=25&m=test'); - }); - - test('regex', () => { - element.getGroups('^test.*', 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/groups/?n=26&S=0&r=%5Etest.*'); - - element.getGroups('^test.*', 25, 25); - assert.equal(fetchCacheURLStub.lastCall.args[0].url, - '/groups/?n=26&S=25&r=%5Etest.*'); - }); - }); - - test('gerrit auth is used', () => { - sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve()); - element._restApiHelper.fetchJSON({url: 'foo'}); - assert(Gerrit.Auth.fetch.called); - }); - - test('getSuggestedAccounts does not return _fetchJSON', () => { - const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON'); - return element.getSuggestedAccounts().then(accts => { - assert.isFalse(_fetchJSONSpy.called); - assert.equal(accts.length, 0); - }); - }); - - test('_fetchJSON gets called by getSuggestedAccounts', () => { - const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON', - () => Promise.resolve()); - return element.getSuggestedAccounts('own').then(() => { - assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, { - q: 'own', - suggest: null, - }); - }); - }); - - suite('getChangeDetail', () => { - suite('change detail options', () => { - let toHexStub; - - setup(() => { - toHexStub = sandbox.stub(element, 'listChangesOptionsToHex', - options => 'deadbeef'); - sandbox.stub(element, '_getChangeDetail', - async (changeNum, options) => { return {changeNum, options}; }); - }); - - test('signed pushes disabled', async () => { - const {PUSH_CERTIFICATES} = element.ListChangesOption; - sandbox.stub(element, 'getConfig', async () => { return {}; }); - const {changeNum, options} = await element.getChangeDetail(123); - assert.strictEqual(123, changeNum); - assert.strictEqual('deadbeef', options); - assert.isTrue(toHexStub.calledOnce); - assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES)); - }); - - test('signed pushes enabled', async () => { - const {PUSH_CERTIFICATES} = element.ListChangesOption; - sandbox.stub(element, 'getConfig', async () => { - return {receive: {enable_signed_push: true}}; - }); - const {changeNum, options} = await element.getChangeDetail(123); - assert.strictEqual(123, changeNum); - assert.strictEqual('deadbeef', options); - assert.isTrue(toHexStub.calledOnce); - assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES)); + suite('_failForCreate200', () => { + test('_sendDiffDraftRequest checks for 200 on create', () => { + const sendPromise = Promise.resolve(); + sandbox.stub(element, '_getChangeURLAndSend').returns(sendPromise); + const failStub = sandbox.stub(element, '_failForCreate200') + .returns(Promise.resolve()); + return element._sendDiffDraftRequest('PUT', 123, 4, {}).then(() => { + assert.isTrue(failStub.calledOnce); + assert.isTrue(failStub.calledWithExactly(sendPromise)); }); }); - test('GrReviewerUpdatesParser.parse is used', () => { - sandbox.stub(GrReviewerUpdatesParser, 'parse').returns( - Promise.resolve('foo')); - return element.getChangeDetail(42).then(result => { - assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce); - assert.equal(result, 'foo'); - }); - }); - - test('_getChangeDetail passes params to ETags decorator', () => { - const changeNum = 4321; - element._projectLookup[changeNum] = 'test'; - const expectedUrl = - window.CANONICAL_PATH + '/changes/test~4321/detail?'+ - '0=5&1=1&2=6&3=7&4=1&5=4'; - sandbox.stub(element._etags, 'getOptions'); - sandbox.stub(element._etags, 'collect'); - return element._getChangeDetail(changeNum, '516714').then(() => { - assert.isTrue(element._etags.getOptions.calledWithExactly( - expectedUrl)); - assert.equal(element._etags.collect.lastCall.args[0], expectedUrl); - }); - }); - - test('_getChangeDetail calls errFn on 500', () => { - const errFn = sinon.stub(); - sandbox.stub(element, 'getChangeActionURL') - .returns(Promise.resolve('')); - sandbox.stub(element._restApiHelper, 'fetchRawJSON') - .returns(Promise.resolve({ok: false, status: 500})); - return element._getChangeDetail(123, '516714', errFn).then(() => { - assert.isTrue(errFn.called); - }); - }); - - test('_getChangeDetail populates _projectLookup', () => { - sandbox.stub(element, 'getChangeActionURL') - .returns(Promise.resolve('')); - sandbox.stub(element._restApiHelper, 'fetchRawJSON') - .returns(Promise.resolve({ok: true})); - - const mockResponse = {_number: 1, project: 'test'}; - sandbox.stub(element._restApiHelper, 'readResponsePayload') - .returns(Promise.resolve({ - parsed: mockResponse, - raw: JSON.stringify(mockResponse), - })); - return element._getChangeDetail(1, '516714').then(() => { - assert.equal(Object.keys(element._projectLookup).length, 1); - assert.equal(element._projectLookup[1], 'test'); - }); - }); - - suite('_getChangeDetail ETag cache', () => { - let requestUrl; - let mockResponseSerial; - let collectSpy; - let getPayloadSpy; - - setup(() => { - requestUrl = '/foo/bar'; - const mockResponse = {foo: 'bar', baz: 42}; - mockResponseSerial = element.JSON_PREFIX + - JSON.stringify(mockResponse); - sandbox.stub(element._restApiHelper, 'urlWithParams') - .returns(requestUrl); - sandbox.stub(element, 'getChangeActionURL') - .returns(Promise.resolve(requestUrl)); - collectSpy = sandbox.spy(element._etags, 'collect'); - getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload'); - }); - - test('contributes to cache', () => { - sandbox.stub(element._restApiHelper, 'fetchRawJSON') - .returns(Promise.resolve({ - text: () => Promise.resolve(mockResponseSerial), - status: 200, - ok: true, - })); - - return element._getChangeDetail(123, '516714').then(detail => { - assert.isFalse(getPayloadSpy.called); - assert.isTrue(collectSpy.calledOnce); - const cachedResponse = element._etags.getCachedPayload(requestUrl); - assert.equal(cachedResponse, mockResponseSerial); - }); - }); - - test('uses cache on HTTP 304', () => { - sandbox.stub(element._restApiHelper, 'fetchRawJSON') - .returns(Promise.resolve({ - text: () => Promise.resolve(mockResponseSerial), - status: 304, - ok: true, - })); - - return element._getChangeDetail(123, {}).then(detail => { - assert.isFalse(collectSpy.called); - assert.isTrue(getPayloadSpy.calledOnce); - }); - }); - }); - }); - - test('setInProjectLookup', () => { - element.setInProjectLookup('test', 'project'); - assert.deepEqual(element._projectLookup, {test: 'project'}); - }); - - suite('getFromProjectLookup', () => { - test('getChange fails', () => { - sandbox.stub(element, 'getChange') - .returns(Promise.resolve(null)); - return element.getFromProjectLookup().then(val => { - assert.strictEqual(val, undefined); - assert.deepEqual(element._projectLookup, {}); - }); - }); - - test('getChange succeeds, no project', () => { - sandbox.stub(element, 'getChange').returns(Promise.resolve(null)); - return element.getFromProjectLookup().then(val => { - assert.strictEqual(val, undefined); - assert.deepEqual(element._projectLookup, {}); - }); - }); - - test('getChange succeeds with project', () => { - sandbox.stub(element, 'getChange') - .returns(Promise.resolve({project: 'project'})); - return element.getFromProjectLookup('test').then(val => { - assert.equal(val, 'project'); - assert.deepEqual(element._projectLookup, {test: 'project'}); - }); - }); - }); - - suite('getChanges populates _projectLookup', () => { - test('multiple queries', () => { - sandbox.stub(element._restApiHelper, 'fetchJSON') - .returns(Promise.resolve([ - [ - {_number: 1, project: 'test'}, - {_number: 2, project: 'test'}, - ], [ - {_number: 3, project: 'test/test'}, - ], - ])); - // When opt_query instanceof Array, _fetchJSON returns - // Array<Array<Object>>. - return element.getChanges(null, []).then(() => { - assert.equal(Object.keys(element._projectLookup).length, 3); - assert.equal(element._projectLookup[1], 'test'); - assert.equal(element._projectLookup[2], 'test'); - assert.equal(element._projectLookup[3], 'test/test'); - }); - }); - - test('no query', () => { - sandbox.stub(element._restApiHelper, 'fetchJSON') - .returns(Promise.resolve([ - {_number: 1, project: 'test'}, - {_number: 2, project: 'test'}, - {_number: 3, project: 'test/test'}, - ])); - - // When opt_query !instanceof Array, _fetchJSON returns - // Array<Object>. - return element.getChanges().then(() => { - assert.equal(Object.keys(element._projectLookup).length, 3); - assert.equal(element._projectLookup[1], 'test'); - assert.equal(element._projectLookup[2], 'test'); - assert.equal(element._projectLookup[3], 'test/test'); - }); - }); - }); - - test('_getChangeURLAndFetch', () => { - element._projectLookup = {1: 'test'}; - const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON') - .returns(Promise.resolve()); - const req = {changeNum: 1, endpoint: '/test', patchNum: 1}; - return element._getChangeURLAndFetch(req).then(() => { - assert.equal(fetchStub.lastCall.args[0].url, - '/changes/test~1/revisions/1/test'); - }); - }); - - test('_getChangeURLAndSend', () => { - element._projectLookup = {1: 'test'}; - const sendStub = sandbox.stub(element._restApiHelper, 'send') - .returns(Promise.resolve()); - - const req = { - changeNum: 1, - method: 'POST', - patchNum: 1, - endpoint: '/test', - }; - return element._getChangeURLAndSend(req).then(() => { - assert.isTrue(sendStub.calledOnce); - assert.equal(sendStub.lastCall.args[0].method, 'POST'); - assert.equal(sendStub.lastCall.args[0].url, - '/changes/test~1/revisions/1/test'); - }); - }); - - suite('reading responses', () => { - test('_readResponsePayload', () => { - const mockObject = {foo: 'bar', baz: 'foo'}; - const serial = element.JSON_PREFIX + JSON.stringify(mockObject); - const mockResponse = {text: () => Promise.resolve(serial)}; - return element._restApiHelper.readResponsePayload(mockResponse) - .then(payload => { - assert.deepEqual(payload.parsed, mockObject); - assert.equal(payload.raw, serial); + test('_sendDiffDraftRequest no checks for 200 on non create', () => { + sandbox.stub(element, '_getChangeURLAndSend') + .returns(Promise.resolve()); + const failStub = sandbox.stub(element, '_failForCreate200') + .returns(Promise.resolve()); + return element._sendDiffDraftRequest('PUT', 123, 4, {id: '123'}) + .then(() => { + assert.isFalse(failStub.called); }); }); - test('_parsePrefixedJSON', () => { - const obj = {x: 3, y: {z: 4}, w: 23}; - const serial = element.JSON_PREFIX + JSON.stringify(obj); - const result = element._restApiHelper.parsePrefixedJSON(serial); - assert.deepEqual(result, obj); - }); - }); - - test('setChangeTopic', () => { - const sendSpy = sandbox.spy(element, '_getChangeURLAndSend'); - return element.setChangeTopic(123, 'foo-bar').then(() => { - assert.isTrue(sendSpy.calledOnce); - assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'}); - }); - }); - - test('setChangeHashtag', () => { - const sendSpy = sandbox.spy(element, '_getChangeURLAndSend'); - return element.setChangeHashtag(123, 'foo-bar').then(() => { - assert.isTrue(sendSpy.calledOnce); - assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar'); - }); - }); - - test('generateAccountHttpPassword', () => { - const sendSpy = sandbox.spy(element._restApiHelper, 'send'); - return element.generateAccountHttpPassword().then(() => { - assert.isTrue(sendSpy.calledOnce); - assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true}); - }); - }); - - suite('getChangeFiles', () => { - test('patch only', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - const range = {basePatchNum: 'PARENT', patchNum: 2}; - return element.getChangeFiles(123, range).then(() => { - assert.isTrue(fetchStub.calledOnce); - assert.equal(fetchStub.lastCall.args[0].patchNum, 2); - assert.isNotOk(fetchStub.lastCall.args[0].params); - }); + test('_failForCreate200 fails on 200', done => { + const result = { + ok: true, + status: 200, + headers: {entries: () => [ + ['Set-CoOkiE', 'secret'], + ['Innocuous', 'hello'], + ]}, + }; + element._failForCreate200(Promise.resolve(result)) + .then(() => { + assert.isTrue(false, 'Promise should not resolve'); + }) + .catch(e => { + assert.isOk(e); + assert.include(e.message, 'Saving draft resulted in HTTP 200'); + assert.include(e.message, 'hello'); + assert.notInclude(e.message, 'secret'); + done(); + }); }); - test('simple range', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - const range = {basePatchNum: 4, patchNum: 5}; - return element.getChangeFiles(123, range).then(() => { - assert.isTrue(fetchStub.calledOnce); - assert.equal(fetchStub.lastCall.args[0].patchNum, 5); - assert.isOk(fetchStub.lastCall.args[0].params); - assert.equal(fetchStub.lastCall.args[0].params.base, 4); - assert.isNotOk(fetchStub.lastCall.args[0].params.parent); - }); - }); - - test('parent index', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - const range = {basePatchNum: -3, patchNum: 5}; - return element.getChangeFiles(123, range).then(() => { - assert.isTrue(fetchStub.calledOnce); - assert.equal(fetchStub.lastCall.args[0].patchNum, 5); - assert.isOk(fetchStub.lastCall.args[0].params); - assert.isNotOk(fetchStub.lastCall.args[0].params.base); - assert.equal(fetchStub.lastCall.args[0].params.parent, 3); - }); - }); - }); - - suite('getDiff', () => { - test('patchOnly', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => { - assert.isTrue(fetchStub.calledOnce); - assert.equal(fetchStub.lastCall.args[0].patchNum, 2); - assert.isOk(fetchStub.lastCall.args[0].params); - assert.isNotOk(fetchStub.lastCall.args[0].params.parent); - assert.isNotOk(fetchStub.lastCall.args[0].params.base); - }); - }); - - test('simple range', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => { - assert.isTrue(fetchStub.calledOnce); - assert.equal(fetchStub.lastCall.args[0].patchNum, 5); - assert.isOk(fetchStub.lastCall.args[0].params); - assert.isNotOk(fetchStub.lastCall.args[0].params.parent); - assert.equal(fetchStub.lastCall.args[0].params.base, 4); - }); - }); - - test('parent index', () => { - const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') - .returns(Promise.resolve()); - return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => { - assert.isTrue(fetchStub.calledOnce); - assert.equal(fetchStub.lastCall.args[0].patchNum, 5); - assert.isOk(fetchStub.lastCall.args[0].params); - assert.isNotOk(fetchStub.lastCall.args[0].params.base); - assert.equal(fetchStub.lastCall.args[0].params.parent, 3); - }); - }); - }); - - test('getDashboard', () => { - const fetchCacheURLStub = sandbox.stub(element._restApiHelper, - 'fetchCacheURL'); - element.getDashboard('gerrit/project', 'default:main'); - assert.isTrue(fetchCacheURLStub.calledOnce); - assert.equal( - fetchCacheURLStub.lastCall.args[0].url, - '/projects/gerrit%2Fproject/dashboards/default%3Amain'); - }); - - test('getFileContent', () => { - sandbox.stub(element, '_getChangeURLAndSend') - .returns(Promise.resolve({ - ok: 'true', - headers: { - get(header) { - if (header === 'X-FYI-Content-Type') { - return 'text/java'; - } - }, - }, - })); - - sandbox.stub(element, 'getResponseObject') - .returns(Promise.resolve('new content')); - - const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => { - assert.deepEqual(res, - {content: 'new content', type: 'text/java', ok: true}); - }); - - const normal = element.getFileContent('1', 'tst/path', '3').then(res => { - assert.deepEqual(res, - {content: 'new content', type: 'text/java', ok: true}); - }); - - return Promise.all([edit, normal]); - }); - - test('getFileContent suppresses 404s', done => { - const res = {status: 404}; - const handler = e => { - assert.isFalse(e.detail.res.status === 404); - done(); - }; - element.addEventListener('server-error', handler); - sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res)); - sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve('')); - element.getFileContent('1', 'tst/path', '1').then(() => { - flushAsynchronousOperations(); - - res.status = 500; - element.getFileContent('1', 'tst/path', '1'); - }); - }); - - test('getChangeFilesOrEditFiles is edit-sensitive', () => { - const fn = element.getChangeOrEditFiles.bind(element); - const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles') - .returns(Promise.resolve({})); - const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles') - .returns(Promise.resolve({})); - - return fn('1', {patchNum: 'edit'}).then(() => { - assert.isTrue(getChangeEditFilesStub.calledOnce); - assert.isFalse(getChangeFilesStub.called); - return fn('1', {patchNum: '1'}).then(() => { - assert.isTrue(getChangeEditFilesStub.calledOnce); - assert.isTrue(getChangeFilesStub.calledOnce); - }); - }); - }); - - test('_fetch forwards request and logs', () => { - const logStub = sandbox.stub(element._restApiHelper, '_logCall'); - const response = {status: 404, text: sinon.stub()}; - const url = 'my url'; - const fetchOptions = {method: 'DELETE'}; - sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response)); - const startTime = 123; - sandbox.stub(Date, 'now').returns(startTime); - const req = {url, fetchOptions}; - return element._restApiHelper.fetch(req).then(() => { - assert.isTrue(logStub.calledOnce); - assert.isTrue(logStub.calledWith(req, startTime, response.status)); - assert.isFalse(response.text.called); - }); - }); - - test('_logCall only reports requests with anonymized URLss', () => { - sandbox.stub(Date, 'now').returns(200); - const handler = sinon.stub(); - element.addEventListener('rpc-log', handler); - - element._restApiHelper._logCall({url: 'url'}, 100, 200); - assert.isFalse(handler.called); - - element._restApiHelper - ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200); - flushAsynchronousOperations(); - assert.isTrue(handler.calledOnce); - }); - - test('saveChangeStarred', async () => { - sandbox.stub(element, 'getFromProjectLookup') - .returns(Promise.resolve('test')); - const sendStub = - sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve()); - - await element.saveChangeStarred(123, true); - assert.isTrue(sendStub.calledOnce); - assert.deepEqual(sendStub.lastCall.args[0], { - method: 'PUT', - url: '/accounts/self/starred.changes/test~123', - anonymizedUrl: '/accounts/self/starred.changes/*', - }); - - await element.saveChangeStarred(456, false); - assert.isTrue(sendStub.calledTwice); - assert.deepEqual(sendStub.lastCall.args[0], { - method: 'DELETE', - url: '/accounts/self/starred.changes/test~456', - anonymizedUrl: '/accounts/self/starred.changes/*', + test('_failForCreate200 does not fail on 201', done => { + const result = { + ok: true, + status: 201, + headers: {entries: () => []}, + }; + element._failForCreate200(Promise.resolve(result)) + .then(() => { + done(); + }) + .catch(e => { + assert.isTrue(false, 'Promise should not fail'); + }); }); }); }); + + test('saveChangeEdit', () => { + element._projectLookup = {1: 'test'}; + const change_num = '1'; + const file_name = 'index.php'; + const file_contents = '<?php'; + sandbox.stub(element._restApiHelper, 'send').returns( + Promise.resolve([change_num, file_name, file_contents])); + sandbox.stub(element, 'getResponseObject') + .returns(Promise.resolve([change_num, file_name, file_contents])); + element._cache.set('/changes/' + change_num + '/edit/' + file_name, {}); + return element.saveChangeEdit(change_num, file_name, file_contents) + .then(() => { + assert.isTrue(element._restApiHelper.send.calledOnce); + assert.equal(element._restApiHelper.send.lastCall.args[0].method, + 'PUT'); + assert.equal(element._restApiHelper.send.lastCall.args[0].url, + '/changes/test~1/edit/' + file_name); + assert.equal(element._restApiHelper.send.lastCall.args[0].body, + file_contents); + }); + }); + + test('putChangeCommitMessage', () => { + element._projectLookup = {1: 'test'}; + const change_num = '1'; + const message = 'this is a commit message'; + sandbox.stub(element._restApiHelper, 'send').returns( + Promise.resolve([change_num, message])); + sandbox.stub(element, 'getResponseObject') + .returns(Promise.resolve([change_num, message])); + element._cache.set('/changes/' + change_num + '/message', {}); + return element.putChangeCommitMessage(change_num, message).then(() => { + assert.isTrue(element._restApiHelper.send.calledOnce); + assert.equal(element._restApiHelper.send.lastCall.args[0].method, 'PUT'); + assert.equal(element._restApiHelper.send.lastCall.args[0].url, + '/changes/test~1/message'); + assert.deepEqual(element._restApiHelper.send.lastCall.args[0].body, + {message}); + }); + }); + + test('deleteChangeCommitMessage', () => { + element._projectLookup = {1: 'test'}; + const change_num = '1'; + const messageId = 'abc'; + sandbox.stub(element._restApiHelper, 'send').returns( + Promise.resolve([change_num, messageId])); + sandbox.stub(element, 'getResponseObject') + .returns(Promise.resolve([change_num, messageId])); + return element.deleteChangeCommitMessage(change_num, messageId).then(() => { + assert.isTrue(element._restApiHelper.send.calledOnce); + assert.equal( + element._restApiHelper.send.lastCall.args[0].method, + 'DELETE' + ); + assert.equal(element._restApiHelper.send.lastCall.args[0].url, + '/changes/test~1/messages/abc'); + }); + }); + + test('startWorkInProgress', () => { + const sendStub = sandbox.stub(element, '_getChangeURLAndSend') + .returns(Promise.resolve('ok')); + element.startWorkInProgress('42'); + assert.isTrue(sendStub.calledOnce); + assert.equal(sendStub.lastCall.args[0].changeNum, '42'); + assert.equal(sendStub.lastCall.args[0].method, 'POST'); + assert.isNotOk(sendStub.lastCall.args[0].patchNum); + assert.equal(sendStub.lastCall.args[0].endpoint, '/wip'); + assert.deepEqual(sendStub.lastCall.args[0].body, {}); + + element.startWorkInProgress('42', 'revising...'); + assert.isTrue(sendStub.calledTwice); + assert.equal(sendStub.lastCall.args[0].changeNum, '42'); + assert.equal(sendStub.lastCall.args[0].method, 'POST'); + assert.isNotOk(sendStub.lastCall.args[0].patchNum); + assert.equal(sendStub.lastCall.args[0].endpoint, '/wip'); + assert.deepEqual(sendStub.lastCall.args[0].body, + {message: 'revising...'}); + }); + + test('startReview', () => { + const sendStub = sandbox.stub(element, '_getChangeURLAndSend') + .returns(Promise.resolve({})); + element.startReview('42', {message: 'Please review.'}); + assert.isTrue(sendStub.calledOnce); + assert.equal(sendStub.lastCall.args[0].changeNum, '42'); + assert.equal(sendStub.lastCall.args[0].method, 'POST'); + assert.isNotOk(sendStub.lastCall.args[0].patchNum); + assert.equal(sendStub.lastCall.args[0].endpoint, '/ready'); + assert.deepEqual(sendStub.lastCall.args[0].body, + {message: 'Please review.'}); + }); + + test('deleteComment', () => { + const sendStub = sandbox.stub(element, '_getChangeURLAndSend') + .returns(Promise.resolve('some response')); + return element.deleteComment('foo', 'bar', '01234', 'removal reason') + .then(response => { + assert.equal(response, 'some response'); + assert.isTrue(sendStub.calledOnce); + assert.equal(sendStub.lastCall.args[0].changeNum, 'foo'); + assert.equal(sendStub.lastCall.args[0].method, 'POST'); + assert.equal(sendStub.lastCall.args[0].patchNum, 'bar'); + assert.equal(sendStub.lastCall.args[0].endpoint, + '/comments/01234/delete'); + assert.deepEqual(sendStub.lastCall.args[0].body, + {reason: 'removal reason'}); + }); + }); + + test('createRepo encodes name', () => { + const sendStub = sandbox.stub(element._restApiHelper, 'send') + .returns(Promise.resolve()); + return element.createRepo({name: 'x/y'}).then(() => { + assert.isTrue(sendStub.calledOnce); + assert.equal(sendStub.lastCall.args[0].url, '/projects/x%2Fy'); + }); + }); + + test('queryChangeFiles', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + return element.queryChangeFiles('42', 'edit', 'test/path.js').then(() => { + assert.equal(fetchStub.lastCall.args[0].changeNum, '42'); + assert.equal(fetchStub.lastCall.args[0].endpoint, + '/files?q=test%2Fpath.js'); + assert.equal(fetchStub.lastCall.args[0].patchNum, 'edit'); + }); + }); + + test('normal use', () => { + const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only'; + + assert.equal(element._getReposUrl('test', 25), + '/projects/?n=26&S=0&query=test'); + + assert.equal(element._getReposUrl(null, 25), + `/projects/?n=26&S=0&query=${defaultQuery}`); + + assert.equal(element._getReposUrl('test', 25, 25), + '/projects/?n=26&S=25&query=test'); + }); + + test('invalidateReposCache', () => { + const url = '/projects/?n=26&S=0&query=test'; + + element._cache.set(url, {}); + + element.invalidateReposCache(); + + assert.isUndefined(element._sharedFetchPromises[url]); + + assert.isFalse(element._cache.has(url)); + }); + + test('invalidateAccountsCache', () => { + const url = '/accounts/self/detail'; + + element._cache.set(url, {}); + + element.invalidateAccountsCache(); + + assert.isUndefined(element._sharedFetchPromises[url]); + + assert.isFalse(element._cache.has(url)); + }); + + suite('getRepos', () => { + const defaultQuery = 'state%3Aactive%20OR%20state%3Aread-only'; + let fetchCacheURLStub; + setup(() => { + fetchCacheURLStub = + sandbox.stub(element._restApiHelper, 'fetchCacheURL'); + }); + + test('normal use', () => { + element.getRepos('test', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=test'); + + element.getRepos(null, 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + `/projects/?n=26&S=0&query=${defaultQuery}`); + + element.getRepos('test', 25, 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=25&query=test'); + }); + + test('with blank', () => { + element.getRepos('test/test', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=inname%3Atest%20AND%20inname%3Atest'); + }); + + test('with hyphen', () => { + element.getRepos('foo-bar', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); + }); + + test('with leading hyphen', () => { + element.getRepos('-bar', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=inname%3Abar'); + }); + + test('with trailing hyphen', () => { + element.getRepos('foo-bar-', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); + }); + + test('with underscore', () => { + element.getRepos('foo_bar', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); + }); + + test('with underscore', () => { + element.getRepos('foo_bar', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/projects/?n=26&S=0&query=inname%3Afoo%20AND%20inname%3Abar'); + }); + + test('hyphen only', () => { + element.getRepos('-', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + `/projects/?n=26&S=0&query=${defaultQuery}`); + }); + }); + + test('_getGroupsUrl normal use', () => { + assert.equal(element._getGroupsUrl('test', 25), + '/groups/?n=26&S=0&m=test'); + + assert.equal(element._getGroupsUrl(null, 25), + '/groups/?n=26&S=0'); + + assert.equal(element._getGroupsUrl('test', 25, 25), + '/groups/?n=26&S=25&m=test'); + }); + + test('invalidateGroupsCache', () => { + const url = '/groups/?n=26&S=0&m=test'; + + element._cache.set(url, {}); + + element.invalidateGroupsCache(); + + assert.isUndefined(element._sharedFetchPromises[url]); + + assert.isFalse(element._cache.has(url)); + }); + + suite('getGroups', () => { + let fetchCacheURLStub; + setup(() => { + fetchCacheURLStub = + sandbox.stub(element._restApiHelper, 'fetchCacheURL'); + }); + + test('normal use', () => { + element.getGroups('test', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/groups/?n=26&S=0&m=test'); + + element.getGroups(null, 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/groups/?n=26&S=0'); + + element.getGroups('test', 25, 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/groups/?n=26&S=25&m=test'); + }); + + test('regex', () => { + element.getGroups('^test.*', 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/groups/?n=26&S=0&r=%5Etest.*'); + + element.getGroups('^test.*', 25, 25); + assert.equal(fetchCacheURLStub.lastCall.args[0].url, + '/groups/?n=26&S=25&r=%5Etest.*'); + }); + }); + + test('gerrit auth is used', () => { + sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve()); + element._restApiHelper.fetchJSON({url: 'foo'}); + assert(Gerrit.Auth.fetch.called); + }); + + test('getSuggestedAccounts does not return _fetchJSON', () => { + const _fetchJSONSpy = sandbox.spy(element._restApiHelper, 'fetchJSON'); + return element.getSuggestedAccounts().then(accts => { + assert.isFalse(_fetchJSONSpy.called); + assert.equal(accts.length, 0); + }); + }); + + test('_fetchJSON gets called by getSuggestedAccounts', () => { + const _fetchJSONStub = sandbox.stub(element._restApiHelper, 'fetchJSON', + () => Promise.resolve()); + return element.getSuggestedAccounts('own').then(() => { + assert.deepEqual(_fetchJSONStub.lastCall.args[0].params, { + q: 'own', + suggest: null, + }); + }); + }); + + suite('getChangeDetail', () => { + suite('change detail options', () => { + let toHexStub; + + setup(() => { + toHexStub = sandbox.stub(element, 'listChangesOptionsToHex', + options => 'deadbeef'); + sandbox.stub(element, '_getChangeDetail', + async (changeNum, options) => { return {changeNum, options}; }); + }); + + test('signed pushes disabled', async () => { + const {PUSH_CERTIFICATES} = element.ListChangesOption; + sandbox.stub(element, 'getConfig', async () => { return {}; }); + const {changeNum, options} = await element.getChangeDetail(123); + assert.strictEqual(123, changeNum); + assert.strictEqual('deadbeef', options); + assert.isTrue(toHexStub.calledOnce); + assert.isFalse(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES)); + }); + + test('signed pushes enabled', async () => { + const {PUSH_CERTIFICATES} = element.ListChangesOption; + sandbox.stub(element, 'getConfig', async () => { + return {receive: {enable_signed_push: true}}; + }); + const {changeNum, options} = await element.getChangeDetail(123); + assert.strictEqual(123, changeNum); + assert.strictEqual('deadbeef', options); + assert.isTrue(toHexStub.calledOnce); + assert.isTrue(toHexStub.lastCall.args.includes(PUSH_CERTIFICATES)); + }); + }); + + test('GrReviewerUpdatesParser.parse is used', () => { + sandbox.stub(GrReviewerUpdatesParser, 'parse').returns( + Promise.resolve('foo')); + return element.getChangeDetail(42).then(result => { + assert.isTrue(GrReviewerUpdatesParser.parse.calledOnce); + assert.equal(result, 'foo'); + }); + }); + + test('_getChangeDetail passes params to ETags decorator', () => { + const changeNum = 4321; + element._projectLookup[changeNum] = 'test'; + const expectedUrl = + window.CANONICAL_PATH + '/changes/test~4321/detail?'+ + '0=5&1=1&2=6&3=7&4=1&5=4'; + sandbox.stub(element._etags, 'getOptions'); + sandbox.stub(element._etags, 'collect'); + return element._getChangeDetail(changeNum, '516714').then(() => { + assert.isTrue(element._etags.getOptions.calledWithExactly( + expectedUrl)); + assert.equal(element._etags.collect.lastCall.args[0], expectedUrl); + }); + }); + + test('_getChangeDetail calls errFn on 500', () => { + const errFn = sinon.stub(); + sandbox.stub(element, 'getChangeActionURL') + .returns(Promise.resolve('')); + sandbox.stub(element._restApiHelper, 'fetchRawJSON') + .returns(Promise.resolve({ok: false, status: 500})); + return element._getChangeDetail(123, '516714', errFn).then(() => { + assert.isTrue(errFn.called); + }); + }); + + test('_getChangeDetail populates _projectLookup', () => { + sandbox.stub(element, 'getChangeActionURL') + .returns(Promise.resolve('')); + sandbox.stub(element._restApiHelper, 'fetchRawJSON') + .returns(Promise.resolve({ok: true})); + + const mockResponse = {_number: 1, project: 'test'}; + sandbox.stub(element._restApiHelper, 'readResponsePayload') + .returns(Promise.resolve({ + parsed: mockResponse, + raw: JSON.stringify(mockResponse), + })); + return element._getChangeDetail(1, '516714').then(() => { + assert.equal(Object.keys(element._projectLookup).length, 1); + assert.equal(element._projectLookup[1], 'test'); + }); + }); + + suite('_getChangeDetail ETag cache', () => { + let requestUrl; + let mockResponseSerial; + let collectSpy; + let getPayloadSpy; + + setup(() => { + requestUrl = '/foo/bar'; + const mockResponse = {foo: 'bar', baz: 42}; + mockResponseSerial = element.JSON_PREFIX + + JSON.stringify(mockResponse); + sandbox.stub(element._restApiHelper, 'urlWithParams') + .returns(requestUrl); + sandbox.stub(element, 'getChangeActionURL') + .returns(Promise.resolve(requestUrl)); + collectSpy = sandbox.spy(element._etags, 'collect'); + getPayloadSpy = sandbox.spy(element._etags, 'getCachedPayload'); + }); + + test('contributes to cache', () => { + sandbox.stub(element._restApiHelper, 'fetchRawJSON') + .returns(Promise.resolve({ + text: () => Promise.resolve(mockResponseSerial), + status: 200, + ok: true, + })); + + return element._getChangeDetail(123, '516714').then(detail => { + assert.isFalse(getPayloadSpy.called); + assert.isTrue(collectSpy.calledOnce); + const cachedResponse = element._etags.getCachedPayload(requestUrl); + assert.equal(cachedResponse, mockResponseSerial); + }); + }); + + test('uses cache on HTTP 304', () => { + sandbox.stub(element._restApiHelper, 'fetchRawJSON') + .returns(Promise.resolve({ + text: () => Promise.resolve(mockResponseSerial), + status: 304, + ok: true, + })); + + return element._getChangeDetail(123, {}).then(detail => { + assert.isFalse(collectSpy.called); + assert.isTrue(getPayloadSpy.calledOnce); + }); + }); + }); + }); + + test('setInProjectLookup', () => { + element.setInProjectLookup('test', 'project'); + assert.deepEqual(element._projectLookup, {test: 'project'}); + }); + + suite('getFromProjectLookup', () => { + test('getChange fails', () => { + sandbox.stub(element, 'getChange') + .returns(Promise.resolve(null)); + return element.getFromProjectLookup().then(val => { + assert.strictEqual(val, undefined); + assert.deepEqual(element._projectLookup, {}); + }); + }); + + test('getChange succeeds, no project', () => { + sandbox.stub(element, 'getChange').returns(Promise.resolve(null)); + return element.getFromProjectLookup().then(val => { + assert.strictEqual(val, undefined); + assert.deepEqual(element._projectLookup, {}); + }); + }); + + test('getChange succeeds with project', () => { + sandbox.stub(element, 'getChange') + .returns(Promise.resolve({project: 'project'})); + return element.getFromProjectLookup('test').then(val => { + assert.equal(val, 'project'); + assert.deepEqual(element._projectLookup, {test: 'project'}); + }); + }); + }); + + suite('getChanges populates _projectLookup', () => { + test('multiple queries', () => { + sandbox.stub(element._restApiHelper, 'fetchJSON') + .returns(Promise.resolve([ + [ + {_number: 1, project: 'test'}, + {_number: 2, project: 'test'}, + ], [ + {_number: 3, project: 'test/test'}, + ], + ])); + // When opt_query instanceof Array, _fetchJSON returns + // Array<Array<Object>>. + return element.getChanges(null, []).then(() => { + assert.equal(Object.keys(element._projectLookup).length, 3); + assert.equal(element._projectLookup[1], 'test'); + assert.equal(element._projectLookup[2], 'test'); + assert.equal(element._projectLookup[3], 'test/test'); + }); + }); + + test('no query', () => { + sandbox.stub(element._restApiHelper, 'fetchJSON') + .returns(Promise.resolve([ + {_number: 1, project: 'test'}, + {_number: 2, project: 'test'}, + {_number: 3, project: 'test/test'}, + ])); + + // When opt_query !instanceof Array, _fetchJSON returns + // Array<Object>. + return element.getChanges().then(() => { + assert.equal(Object.keys(element._projectLookup).length, 3); + assert.equal(element._projectLookup[1], 'test'); + assert.equal(element._projectLookup[2], 'test'); + assert.equal(element._projectLookup[3], 'test/test'); + }); + }); + }); + + test('_getChangeURLAndFetch', () => { + element._projectLookup = {1: 'test'}; + const fetchStub = sandbox.stub(element._restApiHelper, 'fetchJSON') + .returns(Promise.resolve()); + const req = {changeNum: 1, endpoint: '/test', patchNum: 1}; + return element._getChangeURLAndFetch(req).then(() => { + assert.equal(fetchStub.lastCall.args[0].url, + '/changes/test~1/revisions/1/test'); + }); + }); + + test('_getChangeURLAndSend', () => { + element._projectLookup = {1: 'test'}; + const sendStub = sandbox.stub(element._restApiHelper, 'send') + .returns(Promise.resolve()); + + const req = { + changeNum: 1, + method: 'POST', + patchNum: 1, + endpoint: '/test', + }; + return element._getChangeURLAndSend(req).then(() => { + assert.isTrue(sendStub.calledOnce); + assert.equal(sendStub.lastCall.args[0].method, 'POST'); + assert.equal(sendStub.lastCall.args[0].url, + '/changes/test~1/revisions/1/test'); + }); + }); + + suite('reading responses', () => { + test('_readResponsePayload', () => { + const mockObject = {foo: 'bar', baz: 'foo'}; + const serial = element.JSON_PREFIX + JSON.stringify(mockObject); + const mockResponse = {text: () => Promise.resolve(serial)}; + return element._restApiHelper.readResponsePayload(mockResponse) + .then(payload => { + assert.deepEqual(payload.parsed, mockObject); + assert.equal(payload.raw, serial); + }); + }); + + test('_parsePrefixedJSON', () => { + const obj = {x: 3, y: {z: 4}, w: 23}; + const serial = element.JSON_PREFIX + JSON.stringify(obj); + const result = element._restApiHelper.parsePrefixedJSON(serial); + assert.deepEqual(result, obj); + }); + }); + + test('setChangeTopic', () => { + const sendSpy = sandbox.spy(element, '_getChangeURLAndSend'); + return element.setChangeTopic(123, 'foo-bar').then(() => { + assert.isTrue(sendSpy.calledOnce); + assert.deepEqual(sendSpy.lastCall.args[0].body, {topic: 'foo-bar'}); + }); + }); + + test('setChangeHashtag', () => { + const sendSpy = sandbox.spy(element, '_getChangeURLAndSend'); + return element.setChangeHashtag(123, 'foo-bar').then(() => { + assert.isTrue(sendSpy.calledOnce); + assert.equal(sendSpy.lastCall.args[0].body, 'foo-bar'); + }); + }); + + test('generateAccountHttpPassword', () => { + const sendSpy = sandbox.spy(element._restApiHelper, 'send'); + return element.generateAccountHttpPassword().then(() => { + assert.isTrue(sendSpy.calledOnce); + assert.deepEqual(sendSpy.lastCall.args[0].body, {generate: true}); + }); + }); + + suite('getChangeFiles', () => { + test('patch only', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + const range = {basePatchNum: 'PARENT', patchNum: 2}; + return element.getChangeFiles(123, range).then(() => { + assert.isTrue(fetchStub.calledOnce); + assert.equal(fetchStub.lastCall.args[0].patchNum, 2); + assert.isNotOk(fetchStub.lastCall.args[0].params); + }); + }); + + test('simple range', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + const range = {basePatchNum: 4, patchNum: 5}; + return element.getChangeFiles(123, range).then(() => { + assert.isTrue(fetchStub.calledOnce); + assert.equal(fetchStub.lastCall.args[0].patchNum, 5); + assert.isOk(fetchStub.lastCall.args[0].params); + assert.equal(fetchStub.lastCall.args[0].params.base, 4); + assert.isNotOk(fetchStub.lastCall.args[0].params.parent); + }); + }); + + test('parent index', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + const range = {basePatchNum: -3, patchNum: 5}; + return element.getChangeFiles(123, range).then(() => { + assert.isTrue(fetchStub.calledOnce); + assert.equal(fetchStub.lastCall.args[0].patchNum, 5); + assert.isOk(fetchStub.lastCall.args[0].params); + assert.isNotOk(fetchStub.lastCall.args[0].params.base); + assert.equal(fetchStub.lastCall.args[0].params.parent, 3); + }); + }); + }); + + suite('getDiff', () => { + test('patchOnly', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + return element.getDiff(123, 'PARENT', 2, 'foo/bar.baz').then(() => { + assert.isTrue(fetchStub.calledOnce); + assert.equal(fetchStub.lastCall.args[0].patchNum, 2); + assert.isOk(fetchStub.lastCall.args[0].params); + assert.isNotOk(fetchStub.lastCall.args[0].params.parent); + assert.isNotOk(fetchStub.lastCall.args[0].params.base); + }); + }); + + test('simple range', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + return element.getDiff(123, 4, 5, 'foo/bar.baz').then(() => { + assert.isTrue(fetchStub.calledOnce); + assert.equal(fetchStub.lastCall.args[0].patchNum, 5); + assert.isOk(fetchStub.lastCall.args[0].params); + assert.isNotOk(fetchStub.lastCall.args[0].params.parent); + assert.equal(fetchStub.lastCall.args[0].params.base, 4); + }); + }); + + test('parent index', () => { + const fetchStub = sandbox.stub(element, '_getChangeURLAndFetch') + .returns(Promise.resolve()); + return element.getDiff(123, -3, 5, 'foo/bar.baz').then(() => { + assert.isTrue(fetchStub.calledOnce); + assert.equal(fetchStub.lastCall.args[0].patchNum, 5); + assert.isOk(fetchStub.lastCall.args[0].params); + assert.isNotOk(fetchStub.lastCall.args[0].params.base); + assert.equal(fetchStub.lastCall.args[0].params.parent, 3); + }); + }); + }); + + test('getDashboard', () => { + const fetchCacheURLStub = sandbox.stub(element._restApiHelper, + 'fetchCacheURL'); + element.getDashboard('gerrit/project', 'default:main'); + assert.isTrue(fetchCacheURLStub.calledOnce); + assert.equal( + fetchCacheURLStub.lastCall.args[0].url, + '/projects/gerrit%2Fproject/dashboards/default%3Amain'); + }); + + test('getFileContent', () => { + sandbox.stub(element, '_getChangeURLAndSend') + .returns(Promise.resolve({ + ok: 'true', + headers: { + get(header) { + if (header === 'X-FYI-Content-Type') { + return 'text/java'; + } + }, + }, + })); + + sandbox.stub(element, 'getResponseObject') + .returns(Promise.resolve('new content')); + + const edit = element.getFileContent('1', 'tst/path', 'EDIT').then(res => { + assert.deepEqual(res, + {content: 'new content', type: 'text/java', ok: true}); + }); + + const normal = element.getFileContent('1', 'tst/path', '3').then(res => { + assert.deepEqual(res, + {content: 'new content', type: 'text/java', ok: true}); + }); + + return Promise.all([edit, normal]); + }); + + test('getFileContent suppresses 404s', done => { + const res = {status: 404}; + const handler = e => { + assert.isFalse(e.detail.res.status === 404); + done(); + }; + element.addEventListener('server-error', handler); + sandbox.stub(Gerrit.Auth, 'fetch').returns(Promise.resolve(res)); + sandbox.stub(element, '_changeBaseURL').returns(Promise.resolve('')); + element.getFileContent('1', 'tst/path', '1').then(() => { + flushAsynchronousOperations(); + + res.status = 500; + element.getFileContent('1', 'tst/path', '1'); + }); + }); + + test('getChangeFilesOrEditFiles is edit-sensitive', () => { + const fn = element.getChangeOrEditFiles.bind(element); + const getChangeFilesStub = sandbox.stub(element, 'getChangeFiles') + .returns(Promise.resolve({})); + const getChangeEditFilesStub = sandbox.stub(element, 'getChangeEditFiles') + .returns(Promise.resolve({})); + + return fn('1', {patchNum: 'edit'}).then(() => { + assert.isTrue(getChangeEditFilesStub.calledOnce); + assert.isFalse(getChangeFilesStub.called); + return fn('1', {patchNum: '1'}).then(() => { + assert.isTrue(getChangeEditFilesStub.calledOnce); + assert.isTrue(getChangeFilesStub.calledOnce); + }); + }); + }); + + test('_fetch forwards request and logs', () => { + const logStub = sandbox.stub(element._restApiHelper, '_logCall'); + const response = {status: 404, text: sinon.stub()}; + const url = 'my url'; + const fetchOptions = {method: 'DELETE'}; + sandbox.stub(element._auth, 'fetch').returns(Promise.resolve(response)); + const startTime = 123; + sandbox.stub(Date, 'now').returns(startTime); + const req = {url, fetchOptions}; + return element._restApiHelper.fetch(req).then(() => { + assert.isTrue(logStub.calledOnce); + assert.isTrue(logStub.calledWith(req, startTime, response.status)); + assert.isFalse(response.text.called); + }); + }); + + test('_logCall only reports requests with anonymized URLss', () => { + sandbox.stub(Date, 'now').returns(200); + const handler = sinon.stub(); + element.addEventListener('rpc-log', handler); + + element._restApiHelper._logCall({url: 'url'}, 100, 200); + assert.isFalse(handler.called); + + element._restApiHelper + ._logCall({url: 'url', anonymizedUrl: 'not url'}, 100, 200); + flushAsynchronousOperations(); + assert.isTrue(handler.calledOnce); + }); + + test('saveChangeStarred', async () => { + sandbox.stub(element, 'getFromProjectLookup') + .returns(Promise.resolve('test')); + const sendStub = + sandbox.stub(element._restApiHelper, 'send').returns(Promise.resolve()); + + await element.saveChangeStarred(123, true); + assert.isTrue(sendStub.calledOnce); + assert.deepEqual(sendStub.lastCall.args[0], { + method: 'PUT', + url: '/accounts/self/starred.changes/test~123', + anonymizedUrl: '/accounts/self/starred.changes/*', + }); + + await element.saveChangeStarred(456, false); + assert.isTrue(sendStub.calledTwice); + assert.deepEqual(sendStub.lastCall.args[0], { + method: 'DELETE', + url: '/accounts/self/starred.changes/test~456', + anonymizedUrl: '/accounts/self/starred.changes/*', + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html index 310063c..7f86953 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper_test.html
@@ -19,158 +19,169 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-rest-api-helper</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../../test/common-test-setup.html"/> -<script src="../../../../scripts/util.js"></script> -<script src="../gr-auth.js"></script> -<script src="gr-rest-api-helper.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../../test/common-test-setup.js"></script> +<script type="module" src="../../../../scripts/util.js"></script> +<script type="module" src="../gr-auth.js"></script> +<script type="module" src="./gr-rest-api-helper.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../../test/test-pre-setup.js'; +import '../../../../test/common-test-setup.js'; +import '../../../../scripts/util.js'; +import '../gr-auth.js'; +import './gr-rest-api-helper.js'; +void(0); +</script> -<script> - suite('gr-rest-api-helper tests', async () => { - await readyToTest(); - let helper; - let sandbox; - let cache; - let fetchPromisesCache; +<script type="module"> +import '../../../../test/test-pre-setup.js'; +import '../../../../test/common-test-setup.js'; +import '../../../../scripts/util.js'; +import '../gr-auth.js'; +import './gr-rest-api-helper.js'; +suite('gr-rest-api-helper tests', () => { + let helper; + let sandbox; + let cache; + let fetchPromisesCache; - setup(() => { - sandbox = sinon.sandbox.create(); - cache = new SiteBasedCache(); - fetchPromisesCache = new FetchPromisesCache(); + setup(() => { + sandbox = sinon.sandbox.create(); + cache = new SiteBasedCache(); + fetchPromisesCache = new FetchPromisesCache(); - window.CANONICAL_PATH = 'testhelper'; + window.CANONICAL_PATH = 'testhelper'; - const mockRestApiInterface = { - getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH), - fire: sinon.stub(), - }; + const mockRestApiInterface = { + getBaseUrl: sinon.stub().returns(window.CANONICAL_PATH), + fire: sinon.stub(), + }; - const testJSON = ')]}\'\n{"hello": "bonjour"}'; - sandbox.stub(window, 'fetch').returns(Promise.resolve({ - ok: true, - text() { - return Promise.resolve(testJSON); - }, - })); + const testJSON = ')]}\'\n{"hello": "bonjour"}'; + sandbox.stub(window, 'fetch').returns(Promise.resolve({ + ok: true, + text() { + return Promise.resolve(testJSON); + }, + })); - helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache, - mockRestApiInterface); + helper = new GrRestApiHelper(cache, Gerrit.Auth, fetchPromisesCache, + mockRestApiInterface); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('fetchJSON()', () => { + test('Sets header to accept application/json', () => { + const authFetchStub = sandbox.stub(helper._auth, 'fetch') + .returns(Promise.resolve()); + helper.fetchJSON({url: '/dummy/url'}); + assert.isTrue(authFetchStub.called); + assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), + 'application/json'); }); - teardown(() => { - sandbox.restore(); - }); - - suite('fetchJSON()', () => { - test('Sets header to accept application/json', () => { - const authFetchStub = sandbox.stub(helper._auth, 'fetch') - .returns(Promise.resolve()); - helper.fetchJSON({url: '/dummy/url'}); - assert.isTrue(authFetchStub.called); - assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), - 'application/json'); - }); - - test('Use header option accept when provided', () => { - const authFetchStub = sandbox.stub(helper._auth, 'fetch') - .returns(Promise.resolve()); - const headers = new Headers(); - headers.append('Accept', '*/*'); - const fetchOptions = {headers}; - helper.fetchJSON({url: '/dummy/url', fetchOptions}); - assert.isTrue(authFetchStub.called); - assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), - '*/*'); - }); - }); - - test('JSON prefix is properly removed', done => { - helper.fetchJSON({url: '/dummy/url'}).then(obj => { - assert.deepEqual(obj, {hello: 'bonjour'}); - done(); - }); - }); - - test('cached results', done => { - let n = 0; - sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n)); - const promises = []; - promises.push(helper.fetchCacheURL('/foo')); - promises.push(helper.fetchCacheURL('/foo')); - promises.push(helper.fetchCacheURL('/foo')); - - Promise.all(promises).then(results => { - assert.deepEqual(results, [1, 1, 1]); - helper.fetchCacheURL('/foo').then(foo => { - assert.equal(foo, 1); - done(); - }); - }); - }); - - test('cached promise', done => { - const promise = Promise.reject(new Error('foo')); - cache.set('/foo', promise); - helper.fetchCacheURL({url: '/foo'}).catch(p => { - assert.equal(p.message, 'foo'); - done(); - }); - }); - - test('cache invalidation', () => { - cache.set('/foo/bar', 1); - cache.set('/bar', 2); - fetchPromisesCache.set('/foo/bar', 3); - fetchPromisesCache.set('/bar', 4); - helper.invalidateFetchPromisesPrefix('/foo/'); - assert.isFalse(cache.has('/foo/bar')); - assert.isTrue(cache.has('/bar')); - assert.isUndefined(fetchPromisesCache.get('/foo/bar')); - assert.strictEqual(4, fetchPromisesCache.get('/bar')); - }); - - test('params are properly encoded', () => { - let url = helper.urlWithParams('/path/', { - sp: 'hola', - gr: 'guten tag', - noval: null, - }); - assert.equal(url, - window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval'); - - url = helper.urlWithParams('/path/', { - sp: 'hola', - en: ['hey', 'hi'], - }); - assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi'); - - // Order must be maintained with array params. - url = helper.urlWithParams('/path/', { - l: ['c', 'b', 'a'], - }); - assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a'); - }); - - test('request callbacks can be canceled', done => { - let cancelCalled = false; - window.fetch.returns(Promise.resolve({ - body: { - cancel() { cancelCalled = true; }, - }, - })); - const cancelCondition = () => true; - helper.fetchJSON({url: '/dummy/url', cancelCondition}).then( - obj => { - assert.isUndefined(obj); - assert.isTrue(cancelCalled); - done(); - }); + test('Use header option accept when provided', () => { + const authFetchStub = sandbox.stub(helper._auth, 'fetch') + .returns(Promise.resolve()); + const headers = new Headers(); + headers.append('Accept', '*/*'); + const fetchOptions = {headers}; + helper.fetchJSON({url: '/dummy/url', fetchOptions}); + assert.isTrue(authFetchStub.called); + assert.equal(authFetchStub.lastCall.args[1].headers.get('Accept'), + '*/*'); }); }); + + test('JSON prefix is properly removed', done => { + helper.fetchJSON({url: '/dummy/url'}).then(obj => { + assert.deepEqual(obj, {hello: 'bonjour'}); + done(); + }); + }); + + test('cached results', done => { + let n = 0; + sandbox.stub(helper, 'fetchJSON', () => Promise.resolve(++n)); + const promises = []; + promises.push(helper.fetchCacheURL('/foo')); + promises.push(helper.fetchCacheURL('/foo')); + promises.push(helper.fetchCacheURL('/foo')); + + Promise.all(promises).then(results => { + assert.deepEqual(results, [1, 1, 1]); + helper.fetchCacheURL('/foo').then(foo => { + assert.equal(foo, 1); + done(); + }); + }); + }); + + test('cached promise', done => { + const promise = Promise.reject(new Error('foo')); + cache.set('/foo', promise); + helper.fetchCacheURL({url: '/foo'}).catch(p => { + assert.equal(p.message, 'foo'); + done(); + }); + }); + + test('cache invalidation', () => { + cache.set('/foo/bar', 1); + cache.set('/bar', 2); + fetchPromisesCache.set('/foo/bar', 3); + fetchPromisesCache.set('/bar', 4); + helper.invalidateFetchPromisesPrefix('/foo/'); + assert.isFalse(cache.has('/foo/bar')); + assert.isTrue(cache.has('/bar')); + assert.isUndefined(fetchPromisesCache.get('/foo/bar')); + assert.strictEqual(4, fetchPromisesCache.get('/bar')); + }); + + test('params are properly encoded', () => { + let url = helper.urlWithParams('/path/', { + sp: 'hola', + gr: 'guten tag', + noval: null, + }); + assert.equal(url, + window.CANONICAL_PATH + '/path/?sp=hola&gr=guten%20tag&noval'); + + url = helper.urlWithParams('/path/', { + sp: 'hola', + en: ['hey', 'hi'], + }); + assert.equal(url, window.CANONICAL_PATH + '/path/?sp=hola&en=hey&en=hi'); + + // Order must be maintained with array params. + url = helper.urlWithParams('/path/', { + l: ['c', 'b', 'a'], + }); + assert.equal(url, window.CANONICAL_PATH + '/path/?l=c&l=b&l=a'); + }); + + test('request callbacks can be canceled', done => { + let cancelCalled = false; + window.fetch.returns(Promise.resolve({ + body: { + cancel() { cancelCalled = true; }, + }, + })); + const cancelCondition = () => true; + helper.fetchJSON({url: '/dummy/url', cancelCondition}).then( + obj => { + assert.isUndefined(obj); + assert.isTrue(cancelCalled); + done(); + }); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html index 678e02a..6dcdc48 100644 --- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser_test.html
@@ -19,289 +19,292 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-reviewer-updates-parser</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<script src="../../../scripts/util.js"></script> -<script src="gr-reviewer-updates-parser.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="../../../scripts/util.js"></script> +<script type="module" src="./gr-reviewer-updates-parser.js"></script> -<script> - suite('gr-reviewer-updates-parser tests', async () => { - await readyToTest(); - let sandbox; - let instance; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import '../../../scripts/util.js'; +import './gr-reviewer-updates-parser.js'; +suite('gr-reviewer-updates-parser tests', () => { + let sandbox; + let instance; - setup(() => { - sandbox = sinon.sandbox.create(); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + }); - teardown(() => { - sandbox.restore(); - }); + teardown(() => { + sandbox.restore(); + }); - test('ignores changes without messages', () => { - const change = {}; - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_groupUpdates'); - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_formatUpdates'); - assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); - assert.isFalse( - GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); - assert.isFalse( - GrReviewerUpdatesParser.prototype._groupUpdates.called); - assert.isFalse( - GrReviewerUpdatesParser.prototype._formatUpdates.called); - }); + test('ignores changes without messages', () => { + const change = {}; + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_groupUpdates'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_formatUpdates'); + assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); + assert.isFalse( + GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._groupUpdates.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._formatUpdates.called); + }); - test('ignores changes without reviewer updates', () => { - const change = { - messages: [], - }; - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_groupUpdates'); - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_formatUpdates'); - assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); - assert.isFalse( - GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); - assert.isFalse( - GrReviewerUpdatesParser.prototype._groupUpdates.called); - assert.isFalse( - GrReviewerUpdatesParser.prototype._formatUpdates.called); - }); + test('ignores changes without reviewer updates', () => { + const change = { + messages: [], + }; + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_groupUpdates'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_formatUpdates'); + assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); + assert.isFalse( + GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._groupUpdates.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._formatUpdates.called); + }); - test('ignores changes with empty reviewer updates', () => { - const change = { - messages: [], - reviewer_updates: [], - }; - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_groupUpdates'); - sandbox.stub( - GrReviewerUpdatesParser.prototype, '_formatUpdates'); - assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); - assert.isFalse( - GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); - assert.isFalse( - GrReviewerUpdatesParser.prototype._groupUpdates.called); - assert.isFalse( - GrReviewerUpdatesParser.prototype._formatUpdates.called); - }); + test('ignores changes with empty reviewer updates', () => { + const change = { + messages: [], + reviewer_updates: [], + }; + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_filterRemovedMessages'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_groupUpdates'); + sandbox.stub( + GrReviewerUpdatesParser.prototype, '_formatUpdates'); + assert.strictEqual(GrReviewerUpdatesParser.parse(change), change); + assert.isFalse( + GrReviewerUpdatesParser.prototype._filterRemovedMessages.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._groupUpdates.called); + assert.isFalse( + GrReviewerUpdatesParser.prototype._formatUpdates.called); + }); - test('filter removed messages', () => { - const change = { - messages: [ - { - message: 'msg1', - tag: 'autogenerated:gerrit:deleteReviewer', - }, - { - message: 'msg2', - tag: 'foo', - }, - ], - }; - instance = new GrReviewerUpdatesParser(change); - instance._filterRemovedMessages(); - assert.deepEqual(instance.result, { - messages: [{ + test('filter removed messages', () => { + const change = { + messages: [ + { + message: 'msg1', + tag: 'autogenerated:gerrit:deleteReviewer', + }, + { message: 'msg2', tag: 'foo', - }], - }); - }); - - test('group reviewer updates', () => { - const reviewer1 = {_account_id: 1}; - const reviewer2 = {_account_id: 2}; - const date1 = '2017-01-26 12:11:50.000000000'; - const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold. - const date3 = '2017-01-26 12:33:50.000000000'; - const date4 = '2017-01-26 12:44:50.000000000'; - const makeItem = function(state, reviewer, opt_date, opt_author) { - return { - reviewer, - updated: opt_date || date1, - updated_by: opt_author || reviewer1, - state, - }; - }; - let change = { - reviewer_updates: [ - makeItem('REVIEWER', reviewer1), // New group. - makeItem('CC', reviewer2), // Appended. - makeItem('REVIEWER', reviewer2, date2), // Overrides previous one. - - makeItem('CC', reviewer1, date2, reviewer2), // New group. - - makeItem('REMOVED', reviewer2, date3), // Group has no state change. - makeItem('REVIEWER', reviewer2, date3), - - makeItem('CC', reviewer1, date4), // No change, removed. - makeItem('REVIEWER', reviewer1, date4), // Forms new group - makeItem('REMOVED', reviewer2, date4), // Should be grouped. - ], - }; - - instance = new GrReviewerUpdatesParser(change); - instance._groupUpdates(); - change = instance.result; - - assert.equal(change.reviewer_updates.length, 3); - assert.equal(change.reviewer_updates[0].updates.length, 2); - assert.equal(change.reviewer_updates[1].updates.length, 1); - assert.equal(change.reviewer_updates[2].updates.length, 2); - - assert.equal(change.reviewer_updates[0].date, date1); - assert.deepEqual(change.reviewer_updates[0].author, reviewer1); - assert.deepEqual(change.reviewer_updates[0].updates, [ - { - reviewer: reviewer1, - state: 'REVIEWER', }, - { - reviewer: reviewer2, - state: 'REVIEWER', - }, - ]); - - assert.equal(change.reviewer_updates[1].date, date2); - assert.deepEqual(change.reviewer_updates[1].author, reviewer2); - assert.deepEqual(change.reviewer_updates[1].updates, [ - { - reviewer: reviewer1, - state: 'CC', - prev_state: 'REVIEWER', - }, - ]); - - assert.equal(change.reviewer_updates[2].date, date4); - assert.deepEqual(change.reviewer_updates[2].author, reviewer1); - assert.deepEqual(change.reviewer_updates[2].updates, [ - { - reviewer: reviewer1, - prev_state: 'CC', - state: 'REVIEWER', - }, - { - reviewer: reviewer2, - prev_state: 'REVIEWER', - state: 'REMOVED', - }, - ]); - }); - - test('format reviewer updates', () => { - const reviewer1 = {_account_id: 1}; - const reviewer2 = {_account_id: 2}; - const makeItem = function(prev, state, opt_reviewer) { - return { - reviewer: opt_reviewer || reviewer1, - prev_state: prev, - state, - }; - }; - const makeUpdate = function(items) { - return { - author: reviewer1, - updated: '', - updates: items, - }; - }; - const change = { - reviewer_updates: [ - makeUpdate([ - makeItem(undefined, 'CC'), - makeItem(undefined, 'CC', reviewer2), - ]), - makeUpdate([ - makeItem('CC', 'REVIEWER'), - makeItem('REVIEWER', 'REMOVED'), - makeItem('REMOVED', 'REVIEWER'), - makeItem(undefined, 'REVIEWER', reviewer2), - ]), - ], - }; - - instance = new GrReviewerUpdatesParser(change); - instance._formatUpdates(); - - assert.equal(change.reviewer_updates.length, 2); - assert.equal(change.reviewer_updates[0].updates.length, 1); - assert.equal(change.reviewer_updates[1].updates.length, 3); - - let items = change.reviewer_updates[0].updates; - assert.equal(items[0].message, 'Added to cc: '); - assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]); - - items = change.reviewer_updates[1].updates; - assert.equal(items[0].message, 'Moved from cc to reviewer: '); - assert.deepEqual(items[0].reviewers, [reviewer1]); - assert.equal(items[1].message, 'Removed from reviewer: '); - assert.deepEqual(items[1].reviewers, [reviewer1]); - assert.equal(items[2].message, 'Added to reviewer: '); - assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]); - }); - - test('_advanceUpdates', () => { - const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime(); - const tplus = delta => new Date(T0 + delta) - .toISOString() - .replace('T', ' ') - .replace('Z', '000000'); - const change = { - reviewer_updates: [{ - date: tplus(0), - type: 'REVIEWER_UPDATE', - updates: [{ - message: 'same time update', - }], - }, { - date: tplus(200), - type: 'REVIEWER_UPDATE', - updates: [{ - message: 'update within threshold', - }], - }, { - date: tplus(600), - type: 'REVIEWER_UPDATE', - updates: [{ - message: 'update between messages', - }], - }, { - date: tplus(1000), - type: 'REVIEWER_UPDATE', - updates: [{ - message: 'late update', - }], - }], - messages: [{ - id: '6734489eb9d642de28dbf2bcf9bda875923800d8', - date: tplus(0), - message: 'Uploaded patch set 1.', - }, { - id: '6734489eb9d642de28dbf2bcf9bda875923800d8', - date: tplus(800), - message: 'Uploaded patch set 2.', - }], - }; - instance = new GrReviewerUpdatesParser(change); - instance._advanceUpdates(); - const updates = instance.result.reviewer_updates; - assert.isBelow(util.parseDate(updates[0].date).getTime(), T0); - assert.isBelow(util.parseDate(updates[1].date).getTime(), T0); - assert.equal(updates[2].date, tplus(100)); - assert.equal(updates[3].date, tplus(500)); + ], + }; + instance = new GrReviewerUpdatesParser(change); + instance._filterRemovedMessages(); + assert.deepEqual(instance.result, { + messages: [{ + message: 'msg2', + tag: 'foo', + }], }); }); + + test('group reviewer updates', () => { + const reviewer1 = {_account_id: 1}; + const reviewer2 = {_account_id: 2}; + const date1 = '2017-01-26 12:11:50.000000000'; + const date2 = '2017-01-26 12:11:55.000000000'; // Within threshold. + const date3 = '2017-01-26 12:33:50.000000000'; + const date4 = '2017-01-26 12:44:50.000000000'; + const makeItem = function(state, reviewer, opt_date, opt_author) { + return { + reviewer, + updated: opt_date || date1, + updated_by: opt_author || reviewer1, + state, + }; + }; + let change = { + reviewer_updates: [ + makeItem('REVIEWER', reviewer1), // New group. + makeItem('CC', reviewer2), // Appended. + makeItem('REVIEWER', reviewer2, date2), // Overrides previous one. + + makeItem('CC', reviewer1, date2, reviewer2), // New group. + + makeItem('REMOVED', reviewer2, date3), // Group has no state change. + makeItem('REVIEWER', reviewer2, date3), + + makeItem('CC', reviewer1, date4), // No change, removed. + makeItem('REVIEWER', reviewer1, date4), // Forms new group + makeItem('REMOVED', reviewer2, date4), // Should be grouped. + ], + }; + + instance = new GrReviewerUpdatesParser(change); + instance._groupUpdates(); + change = instance.result; + + assert.equal(change.reviewer_updates.length, 3); + assert.equal(change.reviewer_updates[0].updates.length, 2); + assert.equal(change.reviewer_updates[1].updates.length, 1); + assert.equal(change.reviewer_updates[2].updates.length, 2); + + assert.equal(change.reviewer_updates[0].date, date1); + assert.deepEqual(change.reviewer_updates[0].author, reviewer1); + assert.deepEqual(change.reviewer_updates[0].updates, [ + { + reviewer: reviewer1, + state: 'REVIEWER', + }, + { + reviewer: reviewer2, + state: 'REVIEWER', + }, + ]); + + assert.equal(change.reviewer_updates[1].date, date2); + assert.deepEqual(change.reviewer_updates[1].author, reviewer2); + assert.deepEqual(change.reviewer_updates[1].updates, [ + { + reviewer: reviewer1, + state: 'CC', + prev_state: 'REVIEWER', + }, + ]); + + assert.equal(change.reviewer_updates[2].date, date4); + assert.deepEqual(change.reviewer_updates[2].author, reviewer1); + assert.deepEqual(change.reviewer_updates[2].updates, [ + { + reviewer: reviewer1, + prev_state: 'CC', + state: 'REVIEWER', + }, + { + reviewer: reviewer2, + prev_state: 'REVIEWER', + state: 'REMOVED', + }, + ]); + }); + + test('format reviewer updates', () => { + const reviewer1 = {_account_id: 1}; + const reviewer2 = {_account_id: 2}; + const makeItem = function(prev, state, opt_reviewer) { + return { + reviewer: opt_reviewer || reviewer1, + prev_state: prev, + state, + }; + }; + const makeUpdate = function(items) { + return { + author: reviewer1, + updated: '', + updates: items, + }; + }; + const change = { + reviewer_updates: [ + makeUpdate([ + makeItem(undefined, 'CC'), + makeItem(undefined, 'CC', reviewer2), + ]), + makeUpdate([ + makeItem('CC', 'REVIEWER'), + makeItem('REVIEWER', 'REMOVED'), + makeItem('REMOVED', 'REVIEWER'), + makeItem(undefined, 'REVIEWER', reviewer2), + ]), + ], + }; + + instance = new GrReviewerUpdatesParser(change); + instance._formatUpdates(); + + assert.equal(change.reviewer_updates.length, 2); + assert.equal(change.reviewer_updates[0].updates.length, 1); + assert.equal(change.reviewer_updates[1].updates.length, 3); + + let items = change.reviewer_updates[0].updates; + assert.equal(items[0].message, 'Added to cc: '); + assert.deepEqual(items[0].reviewers, [reviewer1, reviewer2]); + + items = change.reviewer_updates[1].updates; + assert.equal(items[0].message, 'Moved from cc to reviewer: '); + assert.deepEqual(items[0].reviewers, [reviewer1]); + assert.equal(items[1].message, 'Removed from reviewer: '); + assert.deepEqual(items[1].reviewers, [reviewer1]); + assert.equal(items[2].message, 'Added to reviewer: '); + assert.deepEqual(items[2].reviewers, [reviewer1, reviewer2]); + }); + + test('_advanceUpdates', () => { + const T0 = util.parseDate('2017-02-17 19:04:18.000000000').getTime(); + const tplus = delta => new Date(T0 + delta) + .toISOString() + .replace('T', ' ') + .replace('Z', '000000'); + const change = { + reviewer_updates: [{ + date: tplus(0), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'same time update', + }], + }, { + date: tplus(200), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'update within threshold', + }], + }, { + date: tplus(600), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'update between messages', + }], + }, { + date: tplus(1000), + type: 'REVIEWER_UPDATE', + updates: [{ + message: 'late update', + }], + }], + messages: [{ + id: '6734489eb9d642de28dbf2bcf9bda875923800d8', + date: tplus(0), + message: 'Uploaded patch set 1.', + }, { + id: '6734489eb9d642de28dbf2bcf9bda875923800d8', + date: tplus(800), + message: 'Uploaded patch set 2.', + }], + }; + instance = new GrReviewerUpdatesParser(change); + instance._advanceUpdates(); + const updates = instance.result.reviewer_updates; + assert.isBelow(util.parseDate(updates[0].date).getTime(), T0); + assert.isBelow(util.parseDate(updates[1].date).getTime(), T0); + assert.equal(updates[2].date, tplus(100)); + assert.equal(updates[3].date, tplus(500)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js new file mode 100644 index 0000000..e29d300 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/mock-diff-response_test.js
@@ -0,0 +1,167 @@ +/** + * @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 '../../../scripts/bundled-polymer.js'; + +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js'; +import {html} from '@polymer/polymer/lib/utils/html-tag.js'; + +const RESPONSE = { + 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.', + ], + }, + ], +}; + +Polymer({ + _template: html` + +`, + + is: 'mock-diff-response', + + properties: { + diffResponse: { + type: Object, + value() { + return RESPONSE; + }, + }, + }, +});
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select.html deleted file mode 100644 index f1ef86a..0000000 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.html +++ /dev/null
@@ -1,24 +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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> - -<dom-module id="gr-select"> - <slot></slot> - <script src="gr-select.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js index 3e59aee..18be73d 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select.js +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select.js
@@ -14,77 +14,89 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.FireMixin - * @extends Polymer.Element - */ - class GrSelect extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-select'; } +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +const $_documentContainer = document.createElement('template'); - static get properties() { - return { - bindValue: { - type: String, - notify: true, - observer: '_updateValue', - }, - }; - } +$_documentContainer.innerHTML = `<dom-module id="gr-select"> + <slot></slot> + +</dom-module>`; - get nativeSelect() { - // gr-select is not a shadow component - // TODO(taoalpha): maybe we should convert - // it into a shadow dom component instead - return this.querySelector('select'); - } +document.head.appendChild($_documentContainer.content); - _updateValue() { - // It's possible to have a value of 0. - if (this.bindValue !== undefined) { - // Set for chrome/safari so it happens instantly +/** + * @appliesMixin Gerrit.FireMixin + * @extends Polymer.Element + */ +class GrSelect extends mixinBehaviors( [ + Gerrit.FireBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get is() { return 'gr-select'; } + + static get properties() { + return { + bindValue: { + type: String, + notify: true, + observer: '_updateValue', + }, + }; + } + + get nativeSelect() { + // gr-select is not a shadow component + // TODO(taoalpha): maybe we should convert + // it into a shadow dom component instead + return this.querySelector('select'); + } + + _updateValue() { + // It's possible to have a value of 0. + if (this.bindValue !== undefined) { + // Set for chrome/safari so it happens instantly + this.nativeSelect.value = this.bindValue; + // Async needed for firefox to populate value. It was trying to do it + // before options from a dom-repeat were rendered previously. + // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735 + this.async(() => { this.nativeSelect.value = this.bindValue; - // Async needed for firefox to populate value. It was trying to do it - // before options from a dom-repeat were rendered previously. - // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7735 - this.async(() => { - this.nativeSelect.value = this.bindValue; - }, 1); - } - } - - _valueChanged() { - this.bindValue = this.nativeSelect.value; - } - - focus() { - this.nativeSelect.focus(); - } - - /** @override */ - created() { - super.created(); - this.addEventListener('change', - () => this._valueChanged()); - this.addEventListener('dom-change', - () => this._updateValue()); - } - - /** @override */ - ready() { - super.ready(); - // If not set via the property, set bind-value to the element value. - if (this.bindValue == undefined && this.nativeSelect.options.length > 0) { - this.bindValue = this.nativeSelect.value; - } + }, 1); } } - customElements.define(GrSelect.is, GrSelect); -})(); + _valueChanged() { + this.bindValue = this.nativeSelect.value; + } + + focus() { + this.nativeSelect.focus(); + } + + /** @override */ + created() { + super.created(); + this.addEventListener('change', + () => this._valueChanged()); + this.addEventListener('dom-change', + () => this._updateValue()); + } + + /** @override */ + ready() { + super.ready(); + // If not set via the property, set bind-value to the element value. + if (this.bindValue == undefined && this.nativeSelect.options.length > 0) { + this.bindValue = this.nativeSelect.value; + } + } +} + +customElements.define(GrSelect.is, GrSelect);
diff --git a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html index 536f4f8..a4d28a3 100644 --- a/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html +++ b/polygerrit-ui/app/elements/shared/gr-select/gr-select_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-select</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-select.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-select.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-select.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -50,71 +55,73 @@ </template> </test-fixture> -<script> - suite('gr-select tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-select.js'; +suite('gr-select tests', () => { + let element; + + setup(() => { + element = fixture('basic'); + }); + + test('bindValue must be set to the first option value', () => { + assert.equal(element.bindValue, '1'); + }); + + test('value of 0 should still trigger value updates', () => { + element.bindValue = 0; + assert.equal(element.nativeSelect.value, 0); + }); + + test('bidirectional binding property-to-attribute', () => { + const changeStub = sinon.stub(); + element.addEventListener('bind-value-changed', changeStub); + + // The selected element should be the first one by default. + assert.equal(element.nativeSelect.value, '1'); + assert.equal(element.bindValue, '1'); + assert.isFalse(changeStub.called); + + // Now change the value. + element.bindValue = '2'; + + // It should be updated. + assert.equal(element.nativeSelect.value, '2'); + assert.equal(element.bindValue, '2'); + assert.isTrue(changeStub.called); + }); + + test('bidirectional binding attribute-to-property', () => { + const changeStub = sinon.stub(); + element.addEventListener('bind-value-changed', changeStub); + + // The selected element should be the first one by default. + assert.equal(element.nativeSelect.value, '1'); + assert.equal(element.bindValue, '1'); + assert.isFalse(changeStub.called); + + // Now change the value. + element.nativeSelect.value = '3'; + element.fire('change'); + + // It should be updated. + assert.equal(element.nativeSelect.value, '3'); + assert.equal(element.bindValue, '3'); + assert.isTrue(changeStub.called); + }); + + suite('gr-select no options tests', () => { let element; setup(() => { - element = fixture('basic'); + element = fixture('noOptions'); }); - test('bindValue must be set to the first option value', () => { - assert.equal(element.bindValue, '1'); - }); - - test('value of 0 should still trigger value updates', () => { - element.bindValue = 0; - assert.equal(element.nativeSelect.value, 0); - }); - - test('bidirectional binding property-to-attribute', () => { - const changeStub = sinon.stub(); - element.addEventListener('bind-value-changed', changeStub); - - // The selected element should be the first one by default. - assert.equal(element.nativeSelect.value, '1'); - assert.equal(element.bindValue, '1'); - assert.isFalse(changeStub.called); - - // Now change the value. - element.bindValue = '2'; - - // It should be updated. - assert.equal(element.nativeSelect.value, '2'); - assert.equal(element.bindValue, '2'); - assert.isTrue(changeStub.called); - }); - - test('bidirectional binding attribute-to-property', () => { - const changeStub = sinon.stub(); - element.addEventListener('bind-value-changed', changeStub); - - // The selected element should be the first one by default. - assert.equal(element.nativeSelect.value, '1'); - assert.equal(element.bindValue, '1'); - assert.isFalse(changeStub.called); - - // Now change the value. - element.nativeSelect.value = '3'; - element.fire('change'); - - // It should be updated. - assert.equal(element.nativeSelect.value, '3'); - assert.equal(element.bindValue, '3'); - assert.isTrue(changeStub.called); - }); - - suite('gr-select no options tests', () => { - let element; - - setup(() => { - element = fixture('noOptions'); - }); - - test('bindValue must not be changed', () => { - assert.isUndefined(element.bindValue); - }); + test('bindValue must not be changed', () => { + assert.isUndefined(element.bindValue); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js index 63dbcbd..151498c 100644 --- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js +++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command.js
@@ -14,26 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrShellCommand extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-shell-command'; } +import '../../../styles/shared-styles.js'; +import '../gr-copy-clipboard/gr-copy-clipboard.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-shell-command_html.js'; - static get properties() { - return { - command: String, - label: String, - }; - } +/** @extends Polymer.Element */ +class GrShellCommand extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - focusOnCopy() { - this.shadowRoot.querySelector('gr-copy-clipboard').focusOnCopy(); - } + static get is() { return 'gr-shell-command'; } + + static get properties() { + return { + command: String, + label: String, + }; } - customElements.define(GrShellCommand.is, GrShellCommand); -})(); + focusOnCopy() { + this.shadowRoot.querySelector('gr-copy-clipboard').focusOnCopy(); + } +} + +customElements.define(GrShellCommand.is, GrShellCommand);
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js index 15e282f..8fbf2b6 100644 --- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js +++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_html.js
@@ -1,26 +1,22 @@ -<!-- -@license -Copyright (C) 2018 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../shared/gr-copy-clipboard/gr-copy-clipboard.html"> - -<dom-module id="gr-shell-command"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> .commandContainer { margin-bottom: var(--spacing-m); @@ -33,7 +29,7 @@ width: 100%; } .commandContainer:before { - content: '$'; + content: '\$'; position: absolute; display: block; box-sizing: border-box; @@ -58,6 +54,4 @@ <div class="commandContainer"> <gr-copy-clipboard text="[[command]]"></gr-copy-clipboard> </div> - </template> - <script src="gr-shell-command.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html index b596a4a..4e5be4d 100644 --- a/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html +++ b/polygerrit-ui/app/elements/shared/gr-shell-command/gr-shell-command_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-shell-command</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-shell-command.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-shell-command.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-shell-command.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,30 +40,32 @@ </template> </test-fixture> -<script> - suite('gr-shell-command tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-shell-command.js'; +suite('gr-shell-command tests', () => { + let element; + let sandbox; - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('basic'); - element.text = `git fetch http://gerrit@localhost:8080/a/test-project - refs/changes/05/5/1 && git checkout FETCH_HEAD`; - flushAsynchronousOperations(); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('focusOnCopy', () => { - const focusStub = sandbox.stub(element.shadowRoot - .querySelector('gr-copy-clipboard'), - 'focusOnCopy'); - element.focusOnCopy(); - assert.isTrue(focusStub.called); - }); + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + element.text = `git fetch http://gerrit@localhost:8080/a/test-project + refs/changes/05/5/1 && git checkout FETCH_HEAD`; + flushAsynchronousOperations(); }); + + teardown(() => { + sandbox.restore(); + }); + + test('focusOnCopy', () => { + const focusStub = sandbox.stub(element.shadowRoot + .querySelector('gr-copy-clipboard'), + 'focusOnCopy'); + element.focusOnCopy(); + assert.isTrue(focusStub.called); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html deleted file mode 100644 index 7215b26..0000000 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.html +++ /dev/null
@@ -1,20 +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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> -<dom-module id="gr-storage"> - <script src="gr-storage.js"></script> -</dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js index 8cc9de9..1597439 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage.js
@@ -14,148 +14,150 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const DURATION_DAY = 24 * 60 * 60 * 1000; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; - // Clean up old entries no more frequently than one day. - const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY; +const DURATION_DAY = 24 * 60 * 60 * 1000; - const CLEANUP_PREFIXES_MAX_AGE_MAP = { - // respectfultip has a 3 day expiration - 'respectfultip:': 3 * DURATION_DAY, - 'draft:': DURATION_DAY, - 'editablecontent:': DURATION_DAY, - }; +// Clean up old entries no more frequently than one day. +const CLEANUP_THROTTLE_INTERVAL = DURATION_DAY; - /** @extends Polymer.Element */ - class GrStorage extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-storage'; } +const CLEANUP_PREFIXES_MAX_AGE_MAP = { + // respectfultip has a 3 day expiration + 'respectfultip:': 3 * DURATION_DAY, + 'draft:': DURATION_DAY, + 'editablecontent:': DURATION_DAY, +}; - static get properties() { - return { - _lastCleanup: Number, - /** @type {?Storage} */ - _storage: { - type: Object, - value() { - return window.localStorage; - }, +/** @extends Polymer.Element */ +class GrStorage extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get is() { return 'gr-storage'; } + + static get properties() { + return { + _lastCleanup: Number, + /** @type {?Storage} */ + _storage: { + type: Object, + value() { + return window.localStorage; }, - _exceededQuota: { - type: Boolean, - value: false, - }, - }; + }, + _exceededQuota: { + type: Boolean, + value: false, + }, + }; + } + + getDraftComment(location) { + this._cleanupItems(); + return this._getObject(this._getDraftKey(location)); + } + + setDraftComment(location, message) { + const key = this._getDraftKey(location); + this._setObject(key, {message, updated: Date.now()}); + } + + eraseDraftComment(location) { + const key = this._getDraftKey(location); + this._storage.removeItem(key); + } + + getEditableContentItem(key) { + this._cleanupItems(); + return this._getObject(this._getEditableContentKey(key)); + } + + setEditableContentItem(key, message) { + this._setObject(this._getEditableContentKey(key), + {message, updated: Date.now()}); + } + + getRespectfulTipVisibility() { + this._cleanupItems(); + return this._getObject('respectfultip:visibility'); + } + + setRespectfulTipVisibility(delayDays = 0) { + this._cleanupItems(); + this._setObject( + 'respectfultip:visibility', + {updated: Date.now() + delayDays * DURATION_DAY} + ); + } + + eraseEditableContentItem(key) { + this._storage.removeItem(this._getEditableContentKey(key)); + } + + _getDraftKey(location) { + const range = location.range ? + `${location.range.start_line}-${location.range.start_character}` + + `-${location.range.end_character}-${location.range.end_line}` : + null; + let key = ['draft', location.changeNum, location.patchNum, location.path, + location.line || ''].join(':'); + if (range) { + key = key + ':' + range; } + return key; + } - getDraftComment(location) { - this._cleanupItems(); - return this._getObject(this._getDraftKey(location)); + _getEditableContentKey(key) { + return `editablecontent:${key}`; + } + + _cleanupItems() { + // Throttle cleanup to the throttle interval. + if (this._lastCleanup && + Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) { + return; } + this._lastCleanup = Date.now(); - setDraftComment(location, message) { - const key = this._getDraftKey(location); - this._setObject(key, {message, updated: Date.now()}); - } - - eraseDraftComment(location) { - const key = this._getDraftKey(location); - this._storage.removeItem(key); - } - - getEditableContentItem(key) { - this._cleanupItems(); - return this._getObject(this._getEditableContentKey(key)); - } - - setEditableContentItem(key, message) { - this._setObject(this._getEditableContentKey(key), - {message, updated: Date.now()}); - } - - getRespectfulTipVisibility() { - this._cleanupItems(); - return this._getObject('respectfultip:visibility'); - } - - setRespectfulTipVisibility(delayDays = 0) { - this._cleanupItems(); - this._setObject( - 'respectfultip:visibility', - {updated: Date.now() + delayDays * DURATION_DAY} - ); - } - - eraseEditableContentItem(key) { - this._storage.removeItem(this._getEditableContentKey(key)); - } - - _getDraftKey(location) { - const range = location.range ? - `${location.range.start_line}-${location.range.start_character}` + - `-${location.range.end_character}-${location.range.end_line}` : - null; - let key = ['draft', location.changeNum, location.patchNum, location.path, - location.line || ''].join(':'); - if (range) { - key = key + ':' + range; - } - return key; - } - - _getEditableContentKey(key) { - return `editablecontent:${key}`; - } - - _cleanupItems() { - // Throttle cleanup to the throttle interval. - if (this._lastCleanup && - Date.now() - this._lastCleanup < CLEANUP_THROTTLE_INTERVAL) { - return; - } - this._lastCleanup = Date.now(); - - let item; - Object.keys(this._storage).forEach(key => { - Object.keys(CLEANUP_PREFIXES_MAX_AGE_MAP).forEach(prefix => { - if (key.startsWith(prefix)) { - item = this._getObject(key); - const expiration = CLEANUP_PREFIXES_MAX_AGE_MAP[prefix]; - if (Date.now() - item.updated > expiration) { - this._storage.removeItem(key); - } + let item; + Object.keys(this._storage).forEach(key => { + Object.keys(CLEANUP_PREFIXES_MAX_AGE_MAP).forEach(prefix => { + if (key.startsWith(prefix)) { + item = this._getObject(key); + const expiration = CLEANUP_PREFIXES_MAX_AGE_MAP[prefix]; + if (Date.now() - item.updated > expiration) { + this._storage.removeItem(key); } - }); - }); - } - - _getObject(key) { - const serial = this._storage.getItem(key); - if (!serial) { return null; } - return JSON.parse(serial); - } - - _setObject(key, obj) { - if (this._exceededQuota) { return; } - try { - this._storage.setItem(key, JSON.stringify(obj)); - } catch (exc) { - // Catch for QuotaExceededError and disable writes on local storage the - // first time that it occurs. - if (exc.code === 22) { - this._exceededQuota = true; - console.warn('Local storage quota exceeded: disabling'); - return; - } else { - throw exc; } + }); + }); + } + + _getObject(key) { + const serial = this._storage.getItem(key); + if (!serial) { return null; } + return JSON.parse(serial); + } + + _setObject(key, obj) { + if (this._exceededQuota) { return; } + try { + this._storage.setItem(key, JSON.stringify(obj)); + } catch (exc) { + // Catch for QuotaExceededError and disable writes on local storage the + // first time that it occurs. + if (exc.code === 22) { + this._exceededQuota = true; + console.warn('Local storage quota exceeded: disabling'); + return; + } else { + throw exc; } } } +} - customElements.define(GrStorage.is, GrStorage); -})(); +customElements.define(GrStorage.is, GrStorage);
diff --git a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html index 66e7f98..06e5915 100644 --- a/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html +++ b/polygerrit-ui/app/elements/shared/gr-storage/gr-storage_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-storage</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-storage.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-storage.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-storage.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -34,165 +39,167 @@ </template> </test-fixture> -<script> - suite('gr-storage tests', async () => { - await readyToTest(); - let element; - let sandbox; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-storage.js'; +suite('gr-storage tests', () => { + let element; + let sandbox; - function mockStorage(opt_quotaExceeded) { - return { - getItem(key) { return this[key]; }, - removeItem(key) { delete this[key]; }, - setItem(key, value) { - // eslint-disable-next-line no-throw-literal - if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ } - this[key] = value; - }, - }; - } + function mockStorage(opt_quotaExceeded) { + return { + getItem(key) { return this[key]; }, + removeItem(key) { delete this[key]; }, + setItem(key, value) { + // eslint-disable-next-line no-throw-literal + if (opt_quotaExceeded) { throw {code: 22}; /* Quota exceeded */ } + this[key] = value; + }, + }; + } - setup(() => { - element = fixture('basic'); - sandbox = sinon.sandbox.create(); - element._storage = mockStorage(); - }); - - teardown(() => sandbox.restore()); - - test('storing, retrieving and erasing drafts', () => { - const changeNum = 1234; - const patchNum = 5; - const path = 'my_source_file.js'; - const line = 123; - const location = { - changeNum, - patchNum, - path, - line, - }; - - // The key is in the expected format. - const key = element._getDraftKey(location); - assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':')); - - // There should be no draft initially. - const draft = element.getDraftComment(location); - assert.isNotOk(draft); - - // Setting the draft stores it under the expected key. - element.setDraftComment(location, 'my comment'); - assert.isOk(element._storage.getItem(key)); - assert.equal(JSON.parse(element._storage.getItem(key)).message, - 'my comment'); - assert.isOk(JSON.parse(element._storage.getItem(key)).updated); - - // Erasing the draft removes the key. - element.eraseDraftComment(location); - assert.isNotOk(element._storage.getItem(key)); - }); - - test('automatically removes old drafts', () => { - const changeNum = 1234; - const patchNum = 5; - const path = 'my_source_file.js'; - const line = 123; - const location = { - changeNum, - patchNum, - path, - line, - }; - - const key = element._getDraftKey(location); - - // Make sure that the call to cleanup doesn't get throttled. - element._lastCleanup = 0; - - const cleanupSpy = sandbox.spy(element, '_cleanupItems'); - - // Create a message with a timestamp that is a second behind the max age. - element._storage.setItem(key, JSON.stringify({ - message: 'old message', - updated: Date.now() - 24 * 60 * 60 * 1000 - 1000, - })); - - // Getting the draft should cause it to be removed. - const draft = element.getDraftComment(location); - - assert.isTrue(cleanupSpy.called); - assert.isNotOk(draft); - assert.isNotOk(element._storage.getItem(key)); - }); - - test('_getDraftKey', () => { - const changeNum = 1234; - const patchNum = 5; - const path = 'my_source_file.js'; - const line = 123; - const location = { - changeNum, - patchNum, - path, - line, - }; - let expectedResult = 'draft:1234:5:my_source_file.js:123'; - assert.equal(element._getDraftKey(location), expectedResult); - location.range = { - start_character: 1, - start_line: 1, - end_character: 1, - end_line: 2, - }; - expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2'; - assert.equal(element._getDraftKey(location), expectedResult); - }); - - test('exceeded quota disables storage', () => { - element._storage = mockStorage(true); - assert.isFalse(element._exceededQuota); - - const changeNum = 1234; - const patchNum = 5; - const path = 'my_source_file.js'; - const line = 123; - const location = { - changeNum, - patchNum, - path, - line, - }; - const key = element._getDraftKey(location); - element.setDraftComment(location, 'my comment'); - assert.isTrue(element._exceededQuota); - assert.isNotOk(element._storage.getItem(key)); - }); - - test('editable content items', () => { - const cleanupStub = sandbox.stub(element, '_cleanupItems'); - const key = 'testKey'; - const computedKey = element._getEditableContentKey(key); - // Key correctly computed. - assert.equal(computedKey, 'editablecontent:testKey'); - - element.setEditableContentItem(key, 'my content'); - - // Setting the draft stores it under the expected key. - let item = element._storage.getItem(computedKey); - assert.isOk(item); - assert.equal(JSON.parse(item).message, 'my content'); - assert.isOk(JSON.parse(item).updated); - - // getEditableContentItem performs as expected. - item = element.getEditableContentItem(key); - assert.isOk(item); - assert.equal(item.message, 'my content'); - assert.isOk(item.updated); - assert.isTrue(cleanupStub.called); - - // eraseEditableContentItem performs as expected. - element.eraseEditableContentItem(key); - assert.isNotOk(element._storage.getItem(computedKey)); - }); + setup(() => { + element = fixture('basic'); + sandbox = sinon.sandbox.create(); + element._storage = mockStorage(); }); + + teardown(() => sandbox.restore()); + + test('storing, retrieving and erasing drafts', () => { + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, + }; + + // The key is in the expected format. + const key = element._getDraftKey(location); + assert.equal(key, ['draft', changeNum, patchNum, path, line].join(':')); + + // There should be no draft initially. + const draft = element.getDraftComment(location); + assert.isNotOk(draft); + + // Setting the draft stores it under the expected key. + element.setDraftComment(location, 'my comment'); + assert.isOk(element._storage.getItem(key)); + assert.equal(JSON.parse(element._storage.getItem(key)).message, + 'my comment'); + assert.isOk(JSON.parse(element._storage.getItem(key)).updated); + + // Erasing the draft removes the key. + element.eraseDraftComment(location); + assert.isNotOk(element._storage.getItem(key)); + }); + + test('automatically removes old drafts', () => { + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, + }; + + const key = element._getDraftKey(location); + + // Make sure that the call to cleanup doesn't get throttled. + element._lastCleanup = 0; + + const cleanupSpy = sandbox.spy(element, '_cleanupItems'); + + // Create a message with a timestamp that is a second behind the max age. + element._storage.setItem(key, JSON.stringify({ + message: 'old message', + updated: Date.now() - 24 * 60 * 60 * 1000 - 1000, + })); + + // Getting the draft should cause it to be removed. + const draft = element.getDraftComment(location); + + assert.isTrue(cleanupSpy.called); + assert.isNotOk(draft); + assert.isNotOk(element._storage.getItem(key)); + }); + + test('_getDraftKey', () => { + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, + }; + let expectedResult = 'draft:1234:5:my_source_file.js:123'; + assert.equal(element._getDraftKey(location), expectedResult); + location.range = { + start_character: 1, + start_line: 1, + end_character: 1, + end_line: 2, + }; + expectedResult = 'draft:1234:5:my_source_file.js:123:1-1-1-2'; + assert.equal(element._getDraftKey(location), expectedResult); + }); + + test('exceeded quota disables storage', () => { + element._storage = mockStorage(true); + assert.isFalse(element._exceededQuota); + + const changeNum = 1234; + const patchNum = 5; + const path = 'my_source_file.js'; + const line = 123; + const location = { + changeNum, + patchNum, + path, + line, + }; + const key = element._getDraftKey(location); + element.setDraftComment(location, 'my comment'); + assert.isTrue(element._exceededQuota); + assert.isNotOk(element._storage.getItem(key)); + }); + + test('editable content items', () => { + const cleanupStub = sandbox.stub(element, '_cleanupItems'); + const key = 'testKey'; + const computedKey = element._getEditableContentKey(key); + // Key correctly computed. + assert.equal(computedKey, 'editablecontent:testKey'); + + element.setEditableContentItem(key, 'my content'); + + // Setting the draft stores it under the expected key. + let item = element._storage.getItem(computedKey); + assert.isOk(item); + assert.equal(JSON.parse(item).message, 'my content'); + assert.isOk(JSON.parse(item).updated); + + // getEditableContentItem performs as expected. + item = element.getEditableContentItem(key); + assert.isOk(item); + assert.equal(item.message, 'my content'); + assert.isOk(item.updated); + assert.isTrue(cleanupStub.called); + + // eraseEditableContentItem performs as expected. + element.eraseEditableContentItem(key); + assert.isNotOk(element._storage.getItem(computedKey)); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js index 07b664b2..6f4c75d 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -14,319 +14,335 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - const MAX_ITEMS_DROPDOWN = 10; +import '../../../behaviors/fire-behavior/fire-behavior.js'; +import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js'; +import '../gr-autocomplete-dropdown/gr-autocomplete-dropdown.js'; +import '../gr-cursor-manager/gr-cursor-manager.js'; +import '../gr-overlay/gr-overlay.js'; +import '@polymer/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js'; +import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js'; +import '../../../styles/shared-styles.js'; +import '../../core/gr-reporting/gr-reporting.js'; +import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-textarea_html.js'; - const ALL_SUGGESTIONS = [ - {value: '😊', match: 'smile :)'}, - {value: '👍', match: 'thumbs up'}, - {value: '😄', match: 'laugh :D'}, - {value: '🎉', match: 'party'}, - {value: '😞', match: 'sad :('}, - {value: '😂', match: 'tears :\')'}, - {value: '🙏', match: 'pray'}, - {value: '😐', match: 'neutral :|'}, - {value: '😮', match: 'shock :O'}, - {value: '👎', match: 'thumbs down'}, - {value: '😎', match: 'cool |;)'}, - {value: '😕', match: 'confused'}, - {value: '👌', match: 'ok'}, - {value: '🔥', match: 'fire'}, - {value: '👊', match: 'fistbump'}, - {value: '💯', match: '100'}, - {value: '💔', match: 'broken heart'}, - {value: '🍺', match: 'beer'}, - {value: '✔', match: 'check'}, - {value: '😋', match: 'tongue'}, - {value: '😭', match: 'crying :\'('}, - {value: '🐨', match: 'koala'}, - {value: '🤓', match: 'glasses'}, - {value: '😆', match: 'grin'}, - {value: '💩', match: 'poop'}, - {value: '😢', match: 'tear'}, - {value: '😒', match: 'unamused'}, - {value: '😉', match: 'wink ;)'}, - {value: '🍷', match: 'wine'}, - {value: '😜', match: 'winking tongue ;)'}, - ]; +const MAX_ITEMS_DROPDOWN = 10; +const ALL_SUGGESTIONS = [ + {value: '😊', match: 'smile :)'}, + {value: '👍', match: 'thumbs up'}, + {value: '😄', match: 'laugh :D'}, + {value: '🎉', match: 'party'}, + {value: '😞', match: 'sad :('}, + {value: '😂', match: 'tears :\')'}, + {value: '🙏', match: 'pray'}, + {value: '😐', match: 'neutral :|'}, + {value: '😮', match: 'shock :O'}, + {value: '👎', match: 'thumbs down'}, + {value: '😎', match: 'cool |;)'}, + {value: '😕', match: 'confused'}, + {value: '👌', match: 'ok'}, + {value: '🔥', match: 'fire'}, + {value: '👊', match: 'fistbump'}, + {value: '💯', match: '100'}, + {value: '💔', match: 'broken heart'}, + {value: '🍺', match: 'beer'}, + {value: '✔', match: 'check'}, + {value: '😋', match: 'tongue'}, + {value: '😭', match: 'crying :\'('}, + {value: '🐨', match: 'koala'}, + {value: '🤓', match: 'glasses'}, + {value: '😆', match: 'grin'}, + {value: '💩', match: 'poop'}, + {value: '😢', match: 'tear'}, + {value: '😒', match: 'unamused'}, + {value: '😉', match: 'wink ;)'}, + {value: '🍷', match: 'wine'}, + {value: '😜', match: 'winking tongue ;)'}, +]; + +/** + * @appliesMixin Gerrit.FireMixin + * @appliesMixin Gerrit.KeyboardShortcutMixin + * @extends Polymer.Element + */ +class GrTextarea extends mixinBehaviors( [ + Gerrit.FireBehavior, + Gerrit.KeyboardShortcutBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-textarea'; } /** - * @appliesMixin Gerrit.FireMixin - * @appliesMixin Gerrit.KeyboardShortcutMixin - * @extends Polymer.Element + * @event bind-value-changed */ - class GrTextarea extends Polymer.mixinBehaviors( [ - Gerrit.FireBehavior, - Gerrit.KeyboardShortcutBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-textarea'; } - /** - * @event bind-value-changed - */ - static get properties() { - return { - autocomplete: Boolean, - disabled: Boolean, - rows: Number, - maxRows: Number, - placeholder: String, - text: { - type: String, - notify: true, - observer: '_handleTextChanged', - }, - hideBorder: { - type: Boolean, - value: false, - }, - /** Text input should be rendered in monspace font. */ - monospace: { - type: Boolean, - value: false, - }, - /** Text input should be rendered in code font, which is smaller than the - standard monospace font. */ - code: { - type: Boolean, - value: false, - }, - /** @type {?number} */ - _colonIndex: Number, - _currentSearchString: { - type: String, - observer: '_determineSuggestions', - }, - _hideAutocomplete: { - type: Boolean, - value: true, - }, - _index: Number, - _suggestions: Array, - // Offset makes dropdown appear below text. - _verticalOffset: { - type: Number, - value: 20, - readOnly: true, - }, - }; + static get properties() { + return { + autocomplete: Boolean, + disabled: Boolean, + rows: Number, + maxRows: Number, + placeholder: String, + text: { + type: String, + notify: true, + observer: '_handleTextChanged', + }, + hideBorder: { + type: Boolean, + value: false, + }, + /** Text input should be rendered in monspace font. */ + monospace: { + type: Boolean, + value: false, + }, + /** Text input should be rendered in code font, which is smaller than the + standard monospace font. */ + code: { + type: Boolean, + value: false, + }, + /** @type {?number} */ + _colonIndex: Number, + _currentSearchString: { + type: String, + observer: '_determineSuggestions', + }, + _hideAutocomplete: { + type: Boolean, + value: true, + }, + _index: Number, + _suggestions: Array, + // Offset makes dropdown appear below text. + _verticalOffset: { + type: Number, + value: 20, + readOnly: true, + }, + }; + } + + get keyBindings() { + return { + esc: '_handleEscKey', + tab: '_handleEnterByKey', + enter: '_handleEnterByKey', + up: '_handleUpKey', + down: '_handleDownKey', + }; + } + + /** @override */ + ready() { + super.ready(); + if (this.monospace) { + this.classList.add('monospace'); } - - get keyBindings() { - return { - esc: '_handleEscKey', - tab: '_handleEnterByKey', - enter: '_handleEnterByKey', - up: '_handleUpKey', - down: '_handleDownKey', - }; + if (this.code) { + this.classList.add('code'); } - - /** @override */ - ready() { - super.ready(); - if (this.monospace) { - this.classList.add('monospace'); - } - if (this.code) { - this.classList.add('code'); - } - if (this.hideBorder) { - this.$.textarea.classList.add('noBorder'); - } - } - - closeDropdown() { - return this.$.emojiSuggestions.close(); - } - - getNativeTextarea() { - return this.$.textarea.textarea; - } - - putCursorAtEnd() { - const textarea = this.getNativeTextarea(); - // Put the cursor at the end always. - textarea.selectionStart = textarea.value.length; - textarea.selectionEnd = textarea.selectionStart; - this.async(() => { - textarea.focus(); - }); - } - - _handleEscKey(e) { - if (this._hideAutocomplete) { return; } - e.preventDefault(); - e.stopPropagation(); - this._resetEmojiDropdown(); - } - - _handleUpKey(e) { - if (this._hideAutocomplete) { return; } - e.preventDefault(); - e.stopPropagation(); - this.$.emojiSuggestions.cursorUp(); - this.$.textarea.textarea.focus(); - this.disableEnterKeyForSelectingEmoji = false; - } - - _handleDownKey(e) { - if (this._hideAutocomplete) { return; } - e.preventDefault(); - e.stopPropagation(); - this.$.emojiSuggestions.cursorDown(); - this.$.textarea.textarea.focus(); - this.disableEnterKeyForSelectingEmoji = false; - } - - _handleEnterByKey(e) { - if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) { - return; - } - e.preventDefault(); - e.stopPropagation(); - this._setEmoji(this.$.emojiSuggestions.getCurrentText()); - } - - _handleEmojiSelect(e) { - this._setEmoji(e.detail.selected.dataset.value); - } - - _setEmoji(text) { - const colonIndex = this._colonIndex; - this.text = this._getText(text); - this.$.textarea.selectionStart = colonIndex + 1; - this.$.textarea.selectionEnd = colonIndex + 1; - this.$.reporting.reportInteraction('select-emoji', {type: text}); - this._resetEmojiDropdown(); - } - - _getText(value) { - return this.text.substr(0, this._colonIndex || 0) + - value + this.text.substr(this.$.textarea.selectionStart); - } - - /** - * Uses a hidden element with the same width and styling of the textarea and - * the text up until the point of interest. Then caratSpan element is added - * to the end and is set to be the positionTarget for the dropdown. Together - * this allows the dropdown to appear near where the user is typing. - */ - _updateCaratPosition() { - this._hideAutocomplete = false; - this.$.hiddenText.textContent = this.$.textarea.value.substr(0, - this.$.textarea.selectionStart); - - const caratSpan = this.$.caratSpan; - this.$.hiddenText.appendChild(caratSpan); - this.$.emojiSuggestions.positionTarget = caratSpan; - this._openEmojiDropdown(); - } - - _getFontSize() { - const fontSizePx = getComputedStyle(this).fontSize || '12px'; - return parseInt(fontSizePx.substr(0, fontSizePx.length - 2), - 10); - } - - _getScrollTop() { - return document.body.scrollTop; - } - - /** - * _handleKeydown used for key handling in the this.$.textarea AND all child - * autocomplete options. - */ - _onValueChanged(e) { - // Relay the event. - this.fire('bind-value-changed', e); - - // If cursor is not in textarea (just opened with colon as last char), - // Don't do anything. - if (!e.currentTarget.focused) { return; } - - const charAtCursor = e.detail && e.detail.value ? - e.detail.value[this.$.textarea.selectionStart - 1] : ''; - if (charAtCursor !== ':' && this._colonIndex == null) { return; } - - // When a colon is detected, set a colon index. We are interested only on - // colons after space or in beginning of textarea - if (charAtCursor === ':') { - if (this.$.textarea.selectionStart < 2 || - e.detail.value[this.$.textarea.selectionStart - 2] === ' ') { - this._colonIndex = this.$.textarea.selectionStart - 1; - } - } - - this._currentSearchString = e.detail.value.substr(this._colonIndex + 1, - this.$.textarea.selectionStart - this._colonIndex - 1); - // Under the following conditions, close and reset the dropdown: - // - The cursor is no longer at the end of the current search string - // - The search string is an space or new line - // - The colon has been removed - // - There are no suggestions that match the search string - if (this.$.textarea.selectionStart !== - this._currentSearchString.length + this._colonIndex + 1 || - this._currentSearchString === ' ' || - this._currentSearchString === '\n' || - !(e.detail.value[this._colonIndex] === ':') || - !this._suggestions.length) { - this._resetEmojiDropdown(); - // Otherwise open the dropdown and set the position to be just below the - // cursor. - } else if (this.$.emojiSuggestions.isHidden) { - this._updateCaratPosition(); - } - this.$.textarea.textarea.focus(); - } - - _openEmojiDropdown() { - this.$.emojiSuggestions.open(); - this.$.reporting.reportInteraction('open-emoji-dropdown'); - } - - _formatSuggestions(matchedSuggestions) { - const suggestions = []; - for (const suggestion of matchedSuggestions) { - suggestion.dataValue = suggestion.value; - suggestion.text = suggestion.value + ' ' + suggestion.match; - suggestions.push(suggestion); - } - this.set('_suggestions', suggestions); - } - - _determineSuggestions(emojiText) { - if (!emojiText.length) { - this._formatSuggestions(ALL_SUGGESTIONS); - this.disableEnterKeyForSelectingEmoji = true; - } else { - const matches = ALL_SUGGESTIONS - .filter(suggestion => suggestion.match.includes(emojiText)) - .slice(0, MAX_ITEMS_DROPDOWN); - this._formatSuggestions(matches); - this.disableEnterKeyForSelectingEmoji = false; - } - } - - _resetEmojiDropdown() { - // hide and reset the autocomplete dropdown. - Polymer.dom.flush(); - this._currentSearchString = ''; - this._hideAutocomplete = true; - this.closeDropdown(); - this._colonIndex = null; - this.$.textarea.textarea.focus(); - } - - _handleTextChanged(text) { - this.dispatchEvent( - new CustomEvent('value-changed', {detail: {value: text}})); + if (this.hideBorder) { + this.$.textarea.classList.add('noBorder'); } } - customElements.define(GrTextarea.is, GrTextarea); -})(); + closeDropdown() { + return this.$.emojiSuggestions.close(); + } + + getNativeTextarea() { + return this.$.textarea.textarea; + } + + putCursorAtEnd() { + const textarea = this.getNativeTextarea(); + // Put the cursor at the end always. + textarea.selectionStart = textarea.value.length; + textarea.selectionEnd = textarea.selectionStart; + this.async(() => { + textarea.focus(); + }); + } + + _handleEscKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this._resetEmojiDropdown(); + } + + _handleUpKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this.$.emojiSuggestions.cursorUp(); + this.$.textarea.textarea.focus(); + this.disableEnterKeyForSelectingEmoji = false; + } + + _handleDownKey(e) { + if (this._hideAutocomplete) { return; } + e.preventDefault(); + e.stopPropagation(); + this.$.emojiSuggestions.cursorDown(); + this.$.textarea.textarea.focus(); + this.disableEnterKeyForSelectingEmoji = false; + } + + _handleEnterByKey(e) { + if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) { + return; + } + e.preventDefault(); + e.stopPropagation(); + this._setEmoji(this.$.emojiSuggestions.getCurrentText()); + } + + _handleEmojiSelect(e) { + this._setEmoji(e.detail.selected.dataset.value); + } + + _setEmoji(text) { + const colonIndex = this._colonIndex; + this.text = this._getText(text); + this.$.textarea.selectionStart = colonIndex + 1; + this.$.textarea.selectionEnd = colonIndex + 1; + this.$.reporting.reportInteraction('select-emoji', {type: text}); + this._resetEmojiDropdown(); + } + + _getText(value) { + return this.text.substr(0, this._colonIndex || 0) + + value + this.text.substr(this.$.textarea.selectionStart); + } + + /** + * Uses a hidden element with the same width and styling of the textarea and + * the text up until the point of interest. Then caratSpan element is added + * to the end and is set to be the positionTarget for the dropdown. Together + * this allows the dropdown to appear near where the user is typing. + */ + _updateCaratPosition() { + this._hideAutocomplete = false; + this.$.hiddenText.textContent = this.$.textarea.value.substr(0, + this.$.textarea.selectionStart); + + const caratSpan = this.$.caratSpan; + this.$.hiddenText.appendChild(caratSpan); + this.$.emojiSuggestions.positionTarget = caratSpan; + this._openEmojiDropdown(); + } + + _getFontSize() { + const fontSizePx = getComputedStyle(this).fontSize || '12px'; + return parseInt(fontSizePx.substr(0, fontSizePx.length - 2), + 10); + } + + _getScrollTop() { + return document.body.scrollTop; + } + + /** + * _handleKeydown used for key handling in the this.$.textarea AND all child + * autocomplete options. + */ + _onValueChanged(e) { + // Relay the event. + this.fire('bind-value-changed', e); + + // If cursor is not in textarea (just opened with colon as last char), + // Don't do anything. + if (!e.currentTarget.focused) { return; } + + const charAtCursor = e.detail && e.detail.value ? + e.detail.value[this.$.textarea.selectionStart - 1] : ''; + if (charAtCursor !== ':' && this._colonIndex == null) { return; } + + // When a colon is detected, set a colon index. We are interested only on + // colons after space or in beginning of textarea + if (charAtCursor === ':') { + if (this.$.textarea.selectionStart < 2 || + e.detail.value[this.$.textarea.selectionStart - 2] === ' ') { + this._colonIndex = this.$.textarea.selectionStart - 1; + } + } + + this._currentSearchString = e.detail.value.substr(this._colonIndex + 1, + this.$.textarea.selectionStart - this._colonIndex - 1); + // Under the following conditions, close and reset the dropdown: + // - The cursor is no longer at the end of the current search string + // - The search string is an space or new line + // - The colon has been removed + // - There are no suggestions that match the search string + if (this.$.textarea.selectionStart !== + this._currentSearchString.length + this._colonIndex + 1 || + this._currentSearchString === ' ' || + this._currentSearchString === '\n' || + !(e.detail.value[this._colonIndex] === ':') || + !this._suggestions.length) { + this._resetEmojiDropdown(); + // Otherwise open the dropdown and set the position to be just below the + // cursor. + } else if (this.$.emojiSuggestions.isHidden) { + this._updateCaratPosition(); + } + this.$.textarea.textarea.focus(); + } + + _openEmojiDropdown() { + this.$.emojiSuggestions.open(); + this.$.reporting.reportInteraction('open-emoji-dropdown'); + } + + _formatSuggestions(matchedSuggestions) { + const suggestions = []; + for (const suggestion of matchedSuggestions) { + suggestion.dataValue = suggestion.value; + suggestion.text = suggestion.value + ' ' + suggestion.match; + suggestions.push(suggestion); + } + this.set('_suggestions', suggestions); + } + + _determineSuggestions(emojiText) { + if (!emojiText.length) { + this._formatSuggestions(ALL_SUGGESTIONS); + this.disableEnterKeyForSelectingEmoji = true; + } else { + const matches = ALL_SUGGESTIONS + .filter(suggestion => suggestion.match.includes(emojiText)) + .slice(0, MAX_ITEMS_DROPDOWN); + this._formatSuggestions(matches); + this.disableEnterKeyForSelectingEmoji = false; + } + } + + _resetEmojiDropdown() { + // hide and reset the autocomplete dropdown. + flush(); + this._currentSearchString = ''; + this._hideAutocomplete = true; + this.closeDropdown(); + this._colonIndex = null; + this.$.textarea.textarea.focus(); + } + + _handleTextChanged(text) { + this.dispatchEvent( + new CustomEvent('value-changed', {detail: {value: text}})); + } +} + +customElements.define(GrTextarea.is, GrTextarea);
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js index 42a4f3b..99dd52d 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_html.js
@@ -1,33 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> -<link rel="import" href="/bower_components/polymer/polymer.html"> - -<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html"> -<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html"> -<link rel="import" href="../../shared/gr-autocomplete-dropdown/gr-autocomplete-dropdown.html"> -<link rel="import" href="../../shared/gr-cursor-manager/gr-cursor-manager.html"> -<link rel="import" href="../../shared/gr-overlay/gr-overlay.html"> -<link rel="import" href="/bower_components/iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> -<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html"> -<link rel="import" href="../../../styles/shared-styles.html"> -<link rel="import" href="../../core/gr-reporting/gr-reporting.html"> - -<dom-module id="gr-textarea"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { display: flex; @@ -82,27 +71,8 @@ hiddenText in order to correctly position the dropdown. After being moved, it is set as the positionTarget for the emojiSuggestions dropdown. --> <span id="caratSpan"></span> - <gr-autocomplete-dropdown - vertical-align="top" - horizontal-align="left" - dynamic-align - id="emojiSuggestions" - suggestions="[[_suggestions]]" - index="[[_index]]" - vertical-offset="[[_verticalOffset]]" - on-dropdown-closed="_resetEmojiDropdown" - on-item-selected="_handleEmojiSelect"> + <gr-autocomplete-dropdown vertical-align="top" horizontal-align="left" dynamic-align="" id="emojiSuggestions" suggestions="[[_suggestions]]" index="[[_index]]" vertical-offset="[[_verticalOffset]]" on-dropdown-closed="_resetEmojiDropdown" on-item-selected="_handleEmojiSelect"> </gr-autocomplete-dropdown> - <iron-autogrow-textarea - id="textarea" - autocomplete="[[autocomplete]]" - placeholder=[[placeholder]] - disabled="[[disabled]]" - rows="[[rows]]" - max-rows="[[maxRows]]" - value="{{text}}" - on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea> + <iron-autogrow-textarea id="textarea" autocomplete="[[autocomplete]]" placeholder="[[placeholder]]" disabled="[[disabled]]" rows="[[rows]]" max-rows="[[maxRows]]" value="{{text}}" on-bind-value-changed="_onValueChanged"></iron-autogrow-textarea> <gr-reporting id="reporting"></gr-reporting> - </template> - <script src="gr-textarea.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html index 674089d..9ede81c 100644 --- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html +++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -19,15 +19,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-textarea</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-textarea.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-textarea.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-textarea.js'; +void(0); +</script> <test-fixture id="basic"> <template> <gr-textarea></gr-textarea> @@ -46,16 +51,301 @@ </template> </test-fixture> -<script> - suite('gr-textarea tests', async () => { - await readyToTest(); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-textarea.js'; +suite('gr-textarea tests', () => { + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('basic'); + sandbox.stub(element.$.reporting, 'reportInteraction'); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('monospace is set properly', () => { + assert.isFalse(element.classList.contains('monospace')); + }); + + test('hideBorder is set properly', () => { + assert.isFalse(element.$.textarea.classList.contains('noBorder')); + }); + + test('emoji selector is not open with the textarea lacks focus', () => { + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + assert.isFalse(!element.$.emojiSuggestions.isHidden); + }); + + test('emoji selector is not open when a general text is entered', () => { + MockInteractions.focus(element.$.textarea); + element.$.textarea.selectionStart = 9; + element.$.textarea.selectionEnd = 9; + element.text = 'some text'; + assert.isFalse(!element.$.emojiSuggestions.isHidden); + }); + + test('emoji selector opens when a colon is typed & the textarea has focus', + () => { + MockInteractions.focus(element.$.textarea); + // Needed for Safari tests. selectionStart is not updated when text is + // updated. + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + flushAsynchronousOperations(); + assert.isFalse(element.$.emojiSuggestions.isHidden); + assert.equal(element._colonIndex, 0); + assert.isFalse(element._hideAutocomplete); + assert.equal(element._currentSearchString, ''); + }); + + test('emoji selector opens when a colon is typed after space', + () => { + MockInteractions.focus(element.$.textarea); + // Needed for Safari tests. selectionStart is not updated when text is + // updated. + element.$.textarea.selectionStart = 2; + element.$.textarea.selectionEnd = 2; + element.text = ' :'; + flushAsynchronousOperations(); + assert.isFalse(element.$.emojiSuggestions.isHidden); + assert.equal(element._colonIndex, 1); + assert.isFalse(element._hideAutocomplete); + assert.equal(element._currentSearchString, ''); + }); + + test('emoji selector doesn\`t open when a colon is typed after character', + () => { + MockInteractions.focus(element.$.textarea); + // Needed for Safari tests. selectionStart is not updated when text is + // updated. + element.$.textarea.selectionStart = 5; + element.$.textarea.selectionEnd = 5; + element.text = 'test:'; + flushAsynchronousOperations(); + assert.isTrue(element.$.emojiSuggestions.isHidden); + assert.isTrue(element._hideAutocomplete); + }); + + test('emoji selector opens when a colon is typed and some substring', + () => { + MockInteractions.focus(element.$.textarea); + // Needed for Safari tests. selectionStart is not updated when text is + // updated. + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + element.$.textarea.selectionStart = 2; + element.$.textarea.selectionEnd = 2; + element.text = ':t'; + flushAsynchronousOperations(); + assert.isFalse(element.$.emojiSuggestions.isHidden); + assert.equal(element._colonIndex, 0); + assert.isFalse(element._hideAutocomplete); + assert.equal(element._currentSearchString, 't'); + }); + + test('emoji selector opens when a colon is typed in middle of text', + () => { + MockInteractions.focus(element.$.textarea); + // Needed for Safari tests. selectionStart is not updated when text is + // updated. + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + // Since selectionStart is on Chrome set always on end of text, we + // stub it to 1 + const text = ': hello'; + sandbox.stub(element.$, 'textarea', { + selectionStart: 1, + value: text, + textarea: { + focus: () => {}, + }, + }); + element.text = text; + flushAsynchronousOperations(); + assert.isFalse(element.$.emojiSuggestions.isHidden); + assert.equal(element._colonIndex, 0); + assert.isFalse(element._hideAutocomplete); + assert.equal(element._currentSearchString, ''); + }); + test('emoji selector closes when text changes before the colon', () => { + const resetStub = sandbox.stub(element, '_resetEmojiDropdown'); + MockInteractions.focus(element.$.textarea); + flushAsynchronousOperations(); + element.$.textarea.selectionStart = 10; + element.$.textarea.selectionEnd = 10; + element.text = 'test test '; + element.$.textarea.selectionStart = 12; + element.$.textarea.selectionEnd = 12; + element.text = 'test test :'; + element.$.textarea.selectionStart = 15; + element.$.textarea.selectionEnd = 15; + element.text = 'test test :smi'; + + assert.equal(element._currentSearchString, 'smi'); + assert.isFalse(resetStub.called); + element.text = 'test test test :smi'; + assert.isTrue(resetStub.called); + }); + + test('_resetEmojiDropdown', () => { + const closeSpy = sandbox.spy(element, 'closeDropdown'); + element._resetEmojiDropdown(); + assert.equal(element._currentSearchString, ''); + assert.isTrue(element._hideAutocomplete); + assert.equal(element._colonIndex, null); + + element.$.emojiSuggestions.open(); + flushAsynchronousOperations(); + element._resetEmojiDropdown(); + assert.isTrue(closeSpy.called); + }); + + test('_determineSuggestions', () => { + const emojiText = 'tear'; + const formatSpy = sandbox.spy(element, '_formatSuggestions'); + element._determineSuggestions(emojiText); + assert.isTrue(formatSpy.called); + assert.isTrue(formatSpy.lastCall.calledWithExactly( + [{dataValue: '😂', value: '😂', match: 'tears :\')', + text: '😂 tears :\')'}, + {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'}, + ])); + }); + + test('_formatSuggestions', () => { + const matchedSuggestions = [{value: '😢', match: 'tear'}, + {value: '😂', match: 'tears'}]; + element._formatSuggestions(matchedSuggestions); + assert.deepEqual( + [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'}, + {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}], + element._suggestions); + }); + + test('_handleEmojiSelect', () => { + element.$.textarea.selectionStart = 16; + element.$.textarea.selectionEnd = 16; + element.text = 'test test :tears'; + element._colonIndex = 10; + const selectedItem = {dataset: {value: '😂'}}; + const event = {detail: {selected: selectedItem}}; + element._handleEmojiSelect(event); + assert.equal(element.text, 'test test 😂'); + }); + + test('_updateCaratPosition', () => { + element.$.textarea.selectionStart = 4; + element.$.textarea.selectionEnd = 4; + element.text = 'test'; + element._updateCaratPosition(); + assert.deepEqual(element.$.hiddenText.innerHTML, element.text + + element.$.caratSpan.outerHTML); + }); + + test('emoji dropdown is closed when iron-overlay-closed is fired', () => { + const resetSpy = sandbox.spy(element, '_resetEmojiDropdown'); + element.$.emojiSuggestions.fire('dropdown-closed'); + assert.isTrue(resetSpy.called); + }); + + test('_onValueChanged fires bind-value-changed', () => { + const listenerStub = sinon.stub(); + const eventObject = {currentTarget: {focused: false}}; + element.addEventListener('bind-value-changed', listenerStub); + element._onValueChanged(eventObject); + assert.isTrue(listenerStub.called); + }); + + suite('keyboard shortcuts', () => { + function setupDropdown(callback) { + MockInteractions.focus(element.$.textarea); + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 2; + element.text = ':1'; + flushAsynchronousOperations(); + } + + test('escape key', () => { + const resetSpy = sandbox.spy(element, '_resetEmojiDropdown'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); + assert.isFalse(resetSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); + assert.isTrue(resetSpy.called); + assert.isFalse(!element.$.emojiSuggestions.isHidden); + }); + + test('up key', () => { + const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); + assert.isFalse(upSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); + assert.isTrue(upSpy.called); + }); + + test('down key', () => { + const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); + assert.isFalse(downSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); + assert.isTrue(downSpy.called); + }); + + test('enter key', () => { + const enterSpy = sandbox.spy(element.$.emojiSuggestions, + 'getCursorTarget'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isFalse(enterSpy.called); + setupDropdown(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isTrue(enterSpy.called); + flushAsynchronousOperations(); + assert.equal(element.text, '💯'); + }); + + test('enter key - ignored on just colon without more information', () => { + const enterSpy = sandbox.spy(element.$.emojiSuggestions, + 'getCursorTarget'); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isFalse(enterSpy.called); + MockInteractions.focus(element.$.textarea); + element.$.textarea.selectionStart = 1; + element.$.textarea.selectionEnd = 1; + element.text = ':'; + flushAsynchronousOperations(); + MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); + assert.isFalse(enterSpy.called); + }); + }); + + suite('gr-textarea monospace', () => { + // gr-textarea set monospace class in the ready() method. + // In Polymer2, ready() is called from the fixture(...) method, + // If ready() is called again later, some nested elements doesn't + // handle it correctly. A separate test-fixture is used to set + // properties before ready() is called. + let element; let sandbox; setup(() => { sandbox = sinon.sandbox.create(); - element = fixture('basic'); - sandbox.stub(element.$.reporting, 'reportInteraction'); + element = fixture('monospace'); }); teardown(() => { @@ -63,315 +353,32 @@ }); test('monospace is set properly', () => { - assert.isFalse(element.classList.contains('monospace')); + assert.isTrue(element.classList.contains('monospace')); + }); + }); + + suite('gr-textarea hideBorder', () => { + // gr-textarea set noBorder class in the ready() method. + // In Polymer2, ready() is called from the fixture(...) method, + // If ready() is called again later, some nested elements doesn't + // handle it correctly. A separate test-fixture is used to set + // properties before ready() is called. + + let element; + let sandbox; + + setup(() => { + sandbox = sinon.sandbox.create(); + element = fixture('hideBorder'); + }); + + teardown(() => { + sandbox.restore(); }); test('hideBorder is set properly', () => { - assert.isFalse(element.$.textarea.classList.contains('noBorder')); - }); - - test('emoji selector is not open with the textarea lacks focus', () => { - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; - element.text = ':'; - assert.isFalse(!element.$.emojiSuggestions.isHidden); - }); - - test('emoji selector is not open when a general text is entered', () => { - MockInteractions.focus(element.$.textarea); - element.$.textarea.selectionStart = 9; - element.$.textarea.selectionEnd = 9; - element.text = 'some text'; - assert.isFalse(!element.$.emojiSuggestions.isHidden); - }); - - test('emoji selector opens when a colon is typed & the textarea has focus', - () => { - MockInteractions.focus(element.$.textarea); - // Needed for Safari tests. selectionStart is not updated when text is - // updated. - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; - element.text = ':'; - flushAsynchronousOperations(); - assert.isFalse(element.$.emojiSuggestions.isHidden); - assert.equal(element._colonIndex, 0); - assert.isFalse(element._hideAutocomplete); - assert.equal(element._currentSearchString, ''); - }); - - test('emoji selector opens when a colon is typed after space', - () => { - MockInteractions.focus(element.$.textarea); - // Needed for Safari tests. selectionStart is not updated when text is - // updated. - element.$.textarea.selectionStart = 2; - element.$.textarea.selectionEnd = 2; - element.text = ' :'; - flushAsynchronousOperations(); - assert.isFalse(element.$.emojiSuggestions.isHidden); - assert.equal(element._colonIndex, 1); - assert.isFalse(element._hideAutocomplete); - assert.equal(element._currentSearchString, ''); - }); - - test('emoji selector doesn\`t open when a colon is typed after character', - () => { - MockInteractions.focus(element.$.textarea); - // Needed for Safari tests. selectionStart is not updated when text is - // updated. - element.$.textarea.selectionStart = 5; - element.$.textarea.selectionEnd = 5; - element.text = 'test:'; - flushAsynchronousOperations(); - assert.isTrue(element.$.emojiSuggestions.isHidden); - assert.isTrue(element._hideAutocomplete); - }); - - test('emoji selector opens when a colon is typed and some substring', - () => { - MockInteractions.focus(element.$.textarea); - // Needed for Safari tests. selectionStart is not updated when text is - // updated. - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; - element.text = ':'; - element.$.textarea.selectionStart = 2; - element.$.textarea.selectionEnd = 2; - element.text = ':t'; - flushAsynchronousOperations(); - assert.isFalse(element.$.emojiSuggestions.isHidden); - assert.equal(element._colonIndex, 0); - assert.isFalse(element._hideAutocomplete); - assert.equal(element._currentSearchString, 't'); - }); - - test('emoji selector opens when a colon is typed in middle of text', - () => { - MockInteractions.focus(element.$.textarea); - // Needed for Safari tests. selectionStart is not updated when text is - // updated. - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; - // Since selectionStart is on Chrome set always on end of text, we - // stub it to 1 - const text = ': hello'; - sandbox.stub(element.$, 'textarea', { - selectionStart: 1, - value: text, - textarea: { - focus: () => {}, - }, - }); - element.text = text; - flushAsynchronousOperations(); - assert.isFalse(element.$.emojiSuggestions.isHidden); - assert.equal(element._colonIndex, 0); - assert.isFalse(element._hideAutocomplete); - assert.equal(element._currentSearchString, ''); - }); - test('emoji selector closes when text changes before the colon', () => { - const resetStub = sandbox.stub(element, '_resetEmojiDropdown'); - MockInteractions.focus(element.$.textarea); - flushAsynchronousOperations(); - element.$.textarea.selectionStart = 10; - element.$.textarea.selectionEnd = 10; - element.text = 'test test '; - element.$.textarea.selectionStart = 12; - element.$.textarea.selectionEnd = 12; - element.text = 'test test :'; - element.$.textarea.selectionStart = 15; - element.$.textarea.selectionEnd = 15; - element.text = 'test test :smi'; - - assert.equal(element._currentSearchString, 'smi'); - assert.isFalse(resetStub.called); - element.text = 'test test test :smi'; - assert.isTrue(resetStub.called); - }); - - test('_resetEmojiDropdown', () => { - const closeSpy = sandbox.spy(element, 'closeDropdown'); - element._resetEmojiDropdown(); - assert.equal(element._currentSearchString, ''); - assert.isTrue(element._hideAutocomplete); - assert.equal(element._colonIndex, null); - - element.$.emojiSuggestions.open(); - flushAsynchronousOperations(); - element._resetEmojiDropdown(); - assert.isTrue(closeSpy.called); - }); - - test('_determineSuggestions', () => { - const emojiText = 'tear'; - const formatSpy = sandbox.spy(element, '_formatSuggestions'); - element._determineSuggestions(emojiText); - assert.isTrue(formatSpy.called); - assert.isTrue(formatSpy.lastCall.calledWithExactly( - [{dataValue: '😂', value: '😂', match: 'tears :\')', - text: '😂 tears :\')'}, - {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'}, - ])); - }); - - test('_formatSuggestions', () => { - const matchedSuggestions = [{value: '😢', match: 'tear'}, - {value: '😂', match: 'tears'}]; - element._formatSuggestions(matchedSuggestions); - assert.deepEqual( - [{value: '😢', dataValue: '😢', match: 'tear', text: '😢 tear'}, - {value: '😂', dataValue: '😂', match: 'tears', text: '😂 tears'}], - element._suggestions); - }); - - test('_handleEmojiSelect', () => { - element.$.textarea.selectionStart = 16; - element.$.textarea.selectionEnd = 16; - element.text = 'test test :tears'; - element._colonIndex = 10; - const selectedItem = {dataset: {value: '😂'}}; - const event = {detail: {selected: selectedItem}}; - element._handleEmojiSelect(event); - assert.equal(element.text, 'test test 😂'); - }); - - test('_updateCaratPosition', () => { - element.$.textarea.selectionStart = 4; - element.$.textarea.selectionEnd = 4; - element.text = 'test'; - element._updateCaratPosition(); - assert.deepEqual(element.$.hiddenText.innerHTML, element.text + - element.$.caratSpan.outerHTML); - }); - - test('emoji dropdown is closed when iron-overlay-closed is fired', () => { - const resetSpy = sandbox.spy(element, '_resetEmojiDropdown'); - element.$.emojiSuggestions.fire('dropdown-closed'); - assert.isTrue(resetSpy.called); - }); - - test('_onValueChanged fires bind-value-changed', () => { - const listenerStub = sinon.stub(); - const eventObject = {currentTarget: {focused: false}}; - element.addEventListener('bind-value-changed', listenerStub); - element._onValueChanged(eventObject); - assert.isTrue(listenerStub.called); - }); - - suite('keyboard shortcuts', () => { - function setupDropdown(callback) { - MockInteractions.focus(element.$.textarea); - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; - element.text = ':'; - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 2; - element.text = ':1'; - flushAsynchronousOperations(); - } - - test('escape key', () => { - const resetSpy = sandbox.spy(element, '_resetEmojiDropdown'); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); - assert.isFalse(resetSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 27); - assert.isTrue(resetSpy.called); - assert.isFalse(!element.$.emojiSuggestions.isHidden); - }); - - test('up key', () => { - const upSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorUp'); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); - assert.isFalse(upSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 38); - assert.isTrue(upSpy.called); - }); - - test('down key', () => { - const downSpy = sandbox.spy(element.$.emojiSuggestions, 'cursorDown'); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); - assert.isFalse(downSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 40); - assert.isTrue(downSpy.called); - }); - - test('enter key', () => { - const enterSpy = sandbox.spy(element.$.emojiSuggestions, - 'getCursorTarget'); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); - assert.isFalse(enterSpy.called); - setupDropdown(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); - assert.isTrue(enterSpy.called); - flushAsynchronousOperations(); - assert.equal(element.text, '💯'); - }); - - test('enter key - ignored on just colon without more information', () => { - const enterSpy = sandbox.spy(element.$.emojiSuggestions, - 'getCursorTarget'); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); - assert.isFalse(enterSpy.called); - MockInteractions.focus(element.$.textarea); - element.$.textarea.selectionStart = 1; - element.$.textarea.selectionEnd = 1; - element.text = ':'; - flushAsynchronousOperations(); - MockInteractions.pressAndReleaseKeyOn(element.$.textarea, 13); - assert.isFalse(enterSpy.called); - }); - }); - - suite('gr-textarea monospace', () => { - // gr-textarea set monospace class in the ready() method. - // In Polymer2, ready() is called from the fixture(...) method, - // If ready() is called again later, some nested elements doesn't - // handle it correctly. A separate test-fixture is used to set - // properties before ready() is called. - - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('monospace'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('monospace is set properly', () => { - assert.isTrue(element.classList.contains('monospace')); - }); - }); - - suite('gr-textarea hideBorder', () => { - // gr-textarea set noBorder class in the ready() method. - // In Polymer2, ready() is called from the fixture(...) method, - // If ready() is called again later, some nested elements doesn't - // handle it correctly. A separate test-fixture is used to set - // properties before ready() is called. - - let element; - let sandbox; - - setup(() => { - sandbox = sinon.sandbox.create(); - element = fixture('hideBorder'); - }); - - teardown(() => { - sandbox.restore(); - }); - - test('hideBorder is set properly', () => { - assert.isTrue(element.$.textarea.classList.contains('noBorder')); - }); + assert.isTrue(element.$.textarea.classList.contains('noBorder')); }); }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js index baa0fc9..3c9181f 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content.js
@@ -14,33 +14,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** - * @appliesMixin Gerrit.TooltipMixin - * @extends Polymer.Element - */ - class GrTooltipContent extends Polymer.mixinBehaviors( [ - Gerrit.TooltipBehavior, - ], Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element))) { - static get is() { return 'gr-tooltip-content'; } +import '../gr-icons/gr-icons.js'; +import '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js'; +import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-tooltip-content_html.js'; - static get properties() { - return { - maxWidth: { - type: String, - reflectToAttribute: true, - }, - showIcon: { - type: Boolean, - value: false, - }, - }; - } +/** + * @appliesMixin Gerrit.TooltipMixin + * @extends Polymer.Element + */ +class GrTooltipContent extends mixinBehaviors( [ + Gerrit.TooltipBehavior, +], GestureEventListeners( + LegacyElementMixin( + PolymerElement))) { + static get template() { return htmlTemplate; } + + static get is() { return 'gr-tooltip-content'; } + + static get properties() { + return { + maxWidth: { + type: String, + reflectToAttribute: true, + }, + showIcon: { + type: Boolean, + value: false, + }, + }; } +} - customElements.define(GrTooltipContent.is, GrTooltipContent); -})(); +customElements.define(GrTooltipContent.is, GrTooltipContent);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js index ec56912..e4b5891 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_html.js
@@ -1,26 +1,22 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../gr-icons/gr-icons.html"> -<link rel="import" href="../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.html"> - -<dom-module id="gr-tooltip-content"> - <template> +export const htmlTemplate = html` <style> iron-icon { width: var(--line-height-normal); @@ -29,7 +25,5 @@ } </style> <slot></slot><!-- - --><iron-icon icon="gr-icons:info" hidden$="[[!showIcon]]"></iron-icon> - </template> - <script src="gr-tooltip-content.js"></script> -</dom-module> + --><iron-icon icon="gr-icons:info" hidden\$="[[!showIcon]]"></iron-icon> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html index 8237552..853f4c2 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip-content/gr-tooltip-content_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-storage</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-tooltip-content.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-tooltip-content.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-tooltip-content.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,29 +40,32 @@ </template> </test-fixture> -<script> - suite('gr-tooltip-content tests', async () => { - await readyToTest(); - let element; - setup(() => { - element = fixture('basic'); - }); - - test('icon is not visible by default', () => { - assert.equal(Polymer.dom(element.root) - .querySelector('iron-icon').hidden, true); - }); - - test('position-below attribute is reflected', () => { - assert.isFalse(element.hasAttribute('position-below')); - element.positionBelow = true; - assert.isTrue(element.hasAttribute('position-below')); - }); - - test('icon is visible with showIcon property', () => { - element.showIcon = true; - assert.equal(Polymer.dom(element.root) - .querySelector('iron-icon').hidden, false); - }); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-tooltip-content.js'; +import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js'; +suite('gr-tooltip-content tests', () => { + let element; + setup(() => { + element = fixture('basic'); }); + + test('icon is not visible by default', () => { + assert.equal(dom(element.root) + .querySelector('iron-icon').hidden, true); + }); + + test('position-below attribute is reflected', () => { + assert.isFalse(element.hasAttribute('position-below')); + element.positionBelow = true; + assert.isTrue(element.hasAttribute('position-below')); + }); + + test('icon is visible with showIcon property', () => { + element.showIcon = true; + assert.equal(dom(element.root) + .querySelector('iron-icon').hidden, false); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js index 6f458d1..0cd2d7c 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.js
@@ -14,33 +14,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -(function() { - 'use strict'; +import '../../../scripts/bundled-polymer.js'; - /** @extends Polymer.Element */ - class GrTooltip extends Polymer.GestureEventListeners( - Polymer.LegacyElementMixin( - Polymer.Element)) { - static get is() { return 'gr-tooltip'; } +import '../../../styles/shared-styles.js'; +import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js'; +import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js'; +import {PolymerElement} from '@polymer/polymer/polymer-element.js'; +import {htmlTemplate} from './gr-tooltip_html.js'; - static get properties() { - return { - text: String, - maxWidth: { - type: String, - observer: '_updateWidth', - }, - positionBelow: { - type: Boolean, - reflectToAttribute: true, - }, - }; - } +/** @extends Polymer.Element */ +class GrTooltip extends GestureEventListeners( + LegacyElementMixin( + PolymerElement)) { + static get template() { return htmlTemplate; } - _updateWidth(maxWidth) { - this.updateStyles({'--tooltip-max-width': maxWidth}); - } + static get is() { return 'gr-tooltip'; } + + static get properties() { + return { + text: String, + maxWidth: { + type: String, + observer: '_updateWidth', + }, + positionBelow: { + type: Boolean, + reflectToAttribute: true, + }, + }; } - customElements.define(GrTooltip.is, GrTooltip); -})(); + _updateWidth(maxWidth) { + this.updateStyles({'--tooltip-max-width': maxWidth}); + } +} + +customElements.define(GrTooltip.is, GrTooltip);
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js index d78d554..5f9ce51 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_html.js
@@ -1,25 +1,22 @@ -<!-- -@license -Copyright (C) 2016 The Android Open Source Project +/** + * @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.js'; -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. ---> - -<link rel="import" href="/bower_components/polymer/polymer.html"> -<link rel="import" href="../../../styles/shared-styles.html"> - -<dom-module id="gr-tooltip"> - <template> +export const htmlTemplate = html` <style include="shared-styles"> :host { --gr-tooltip-arrow-size: .5em; @@ -66,6 +63,4 @@ [[text]] <i class="arrowPositionAbove arrow"></i> </div> - </template> - <script src="gr-tooltip.js"></script> -</dom-module> +`;
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html index 4c9b954..be5e26e 100644 --- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html +++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip_test.html
@@ -18,15 +18,20 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>gr-storage</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="gr-tooltip.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./gr-tooltip.js"></script> -<script>void(0);</script> +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-tooltip.js'; +void(0); +</script> <test-fixture id="basic"> <template> @@ -35,35 +40,37 @@ </template> </test-fixture> -<script> - suite('gr-tooltip tests', async () => { - await readyToTest(); - let element; - setup(() => { - element = fixture('basic'); - }); - - test('max-width is respected if set', () => { - element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' + - ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'; - element.maxWidth = '50px'; - assert.equal(getComputedStyle(element).width, '50px'); - }); - - test('the correct arrow is displayed', () => { - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.arrowPositionBelow')).display, - 'none'); - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.arrowPositionAbove')) - .display, 'none'); - element.positionBelow = true; - assert.notEqual(getComputedStyle(element.shadowRoot - .querySelector('.arrowPositionBelow')) - .display, 'none'); - assert.equal(getComputedStyle(element.shadowRoot - .querySelector('.arrowPositionAbove')) - .display, 'none'); - }); +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './gr-tooltip.js'; +suite('gr-tooltip tests', () => { + let element; + setup(() => { + element = fixture('basic'); }); + + test('max-width is respected if set', () => { + element.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit' + + ', sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'; + element.maxWidth = '50px'; + assert.equal(getComputedStyle(element).width, '50px'); + }); + + test('the correct arrow is displayed', () => { + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.arrowPositionBelow')).display, + 'none'); + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.arrowPositionAbove')) + .display, 'none'); + element.positionBelow = true; + assert.notEqual(getComputedStyle(element.shadowRoot + .querySelector('.arrowPositionBelow')) + .display, 'none'); + assert.equal(getComputedStyle(element.shadowRoot + .querySelector('.arrowPositionAbove')) + .display, 'none'); + }); +}); </script>
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js index 239e0fa..f89234f 100644 --- a/polygerrit-ui/app/elements/shared/revision-info/revision-info.js +++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info.js
@@ -1,88 +1,83 @@ -<!-- -@license -Copyright (C) 2017 The Android Open Source Project +/** + * @license + * Copyright (C) 2017 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 '../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.js'; -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 +/** + * @constructor + * @param {Object} change A change object resulting from a change detail + * call that includes revision information. + */ +function RevisionInfo(change) { + this._change = change; +} -http://www.apache.org/licenses/LICENSE-2.0 +/** + * Get the largest number of parents of the commit in any revision. For + * example, with normal changes this will always return 1. For merge changes + * wherein the revisions are merge commits this will return 2 or potentially + * more. + * + * @return {number} + */ +RevisionInfo.prototype.getMaxParents = function() { + if (!this._change || !this._change.revisions) { + return 0; + } + return Object.values(this._change.revisions) + .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 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. ---> -<link rel="import" href="../../../behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html"> -<script> - (function() { - 'use strict'; +/** + * Get an object that maps revision numbers to the number of parents of the + * commit of that revision. + * + * @return {!Object} + */ +RevisionInfo.prototype.getParentCountMap = function() { + const result = {}; + if (!this._change || !this._change.revisions) { + return {}; + } + Object.values(this._change.revisions) + .forEach(rev => { result[rev._number] = rev.commit.parents.length; }); + return result; +}; - /** - * @constructor - * @param {Object} change A change object resulting from a change detail - * call that includes revision information. - */ - function RevisionInfo(change) { - this._change = change; - } +/** + * @param {number|string} patchNum + * @return {number} + */ +RevisionInfo.prototype.getParentCount = function(patchNum) { + return this.getParentCountMap()[patchNum]; +}; - /** - * Get the largest number of parents of the commit in any revision. For - * example, with normal changes this will always return 1. For merge changes - * wherein the revisions are merge commits this will return 2 or potentially - * more. - * - * @return {number} - */ - RevisionInfo.prototype.getMaxParents = function() { - if (!this._change || !this._change.revisions) { - return 0; - } - return Object.values(this._change.revisions) - .reduce((acc, rev) => Math.max(rev.commit.parents.length, acc), 0); - }; +/** + * Get the commit ID of the (0-offset) indexed parent in the given revision + * number. + * + * @param {number|string} patchNum + * @param {number} parentIndex (0-offset) + * @return {string} + */ +RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) { + const rev = Object.values(this._change.revisions).find(rev => + Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum)); + return rev.commit.parents[parentIndex].commit; +}; - /** - * Get an object that maps revision numbers to the number of parents of the - * commit of that revision. - * - * @return {!Object} - */ - RevisionInfo.prototype.getParentCountMap = function() { - const result = {}; - if (!this._change || !this._change.revisions) { - return {}; - } - Object.values(this._change.revisions) - .forEach(rev => { result[rev._number] = rev.commit.parents.length; }); - return result; - }; - - /** - * @param {number|string} patchNum - * @return {number} - */ - RevisionInfo.prototype.getParentCount = function(patchNum) { - return this.getParentCountMap()[patchNum]; - }; - - /** - * Get the commit ID of the (0-offset) indexed parent in the given revision - * number. - * - * @param {number|string} patchNum - * @param {number} parentIndex (0-offset) - * @return {string} - */ - RevisionInfo.prototype.getParentId = function(patchNum, parentIndex) { - const rev = Object.values(this._change.revisions).find(rev => - Gerrit.PatchSetBehavior.patchNumEquals(rev._number, patchNum)); - return rev.commit.parents[parentIndex].commit; - }; - - window.Gerrit = window.Gerrit || {}; - window.Gerrit.RevisionInfo = RevisionInfo; - })(); -</script> +window.Gerrit = window.Gerrit || {}; +window.Gerrit.RevisionInfo = RevisionInfo;
diff --git a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html index fb7a011..4946e2e 100644 --- a/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html +++ b/polygerrit-ui/app/elements/shared/revision-info/revision-info_test.html
@@ -19,72 +19,74 @@ <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> <title>revision-info</title> -<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script> +<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script> -<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script> -<script src="/bower_components/web-component-tester/browser.js"></script> -<script src="../../../test/test-pre-setup.js"></script> -<link rel="import" href="../../../test/common-test-setup.html"/> -<link rel="import" href="revision-info.html"> +<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script> +<script src="/components/wct-browser-legacy/browser.js"></script> +<script type="module" src="../../../test/test-pre-setup.js"></script> +<script type="module" src="../../../test/common-test-setup.js"></script> +<script type="module" src="./revision-info.js"></script> -<script> - suite('revision-info tests', async () => { - await readyToTest(); - let mockChange; +<script type="module"> +import '../../../test/test-pre-setup.js'; +import '../../../test/common-test-setup.js'; +import './revision-info.js'; +suite('revision-info tests', () => { + let mockChange; - setup(() => { - mockChange = { - revisions: { - r1: {_number: 1, commit: {parents: [ - {commit: 'p1'}, - {commit: 'p2'}, - {commit: 'p3'}, - ]}}, - r2: {_number: 2, commit: {parents: [ - {commit: 'p1'}, - {commit: 'p4'}, - ]}}, - r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}}, - r4: {_number: 4, commit: {parents: [ - {commit: 'p2'}, - {commit: 'p3'}, - ]}}, - r5: {_number: 5, commit: {parents: [ - {commit: 'p5'}, - {commit: 'p2'}, - {commit: 'p3'}, - ]}}, - }, - }; - }); - - test('getMaxParents', () => { - const ri = new window.Gerrit.RevisionInfo(mockChange); - assert.equal(ri.getMaxParents(), 3); - }); - - test('getParentCountMap', () => { - const ri = new window.Gerrit.RevisionInfo(mockChange); - assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3}); - }); - - test('getParentCount', () => { - const ri = new window.Gerrit.RevisionInfo(mockChange); - assert.deepEqual(ri.getParentCount(1), 3); - assert.deepEqual(ri.getParentCount(3), 1); - }); - - test('getParentCount', () => { - const ri = new window.Gerrit.RevisionInfo(mockChange); - assert.deepEqual(ri.getParentCount(1), 3); - assert.deepEqual(ri.getParentCount(3), 1); - }); - - test('getParentId', () => { - const ri = new window.Gerrit.RevisionInfo(mockChange); - assert.deepEqual(ri.getParentId(1, 2), 'p3'); - assert.deepEqual(ri.getParentId(2, 1), 'p4'); - assert.deepEqual(ri.getParentId(3, 0), 'p5'); - }); + setup(() => { + mockChange = { + revisions: { + r1: {_number: 1, commit: {parents: [ + {commit: 'p1'}, + {commit: 'p2'}, + {commit: 'p3'}, + ]}}, + r2: {_number: 2, commit: {parents: [ + {commit: 'p1'}, + {commit: 'p4'}, + ]}}, + r3: {_number: 3, commit: {parents: [{commit: 'p5'}]}}, + r4: {_number: 4, commit: {parents: [ + {commit: 'p2'}, + {commit: 'p3'}, + ]}}, + r5: {_number: 5, commit: {parents: [ + {commit: 'p5'}, + {commit: 'p2'}, + {commit: 'p3'}, + ]}}, + }, + }; }); + + test('getMaxParents', () => { + const ri = new window.Gerrit.RevisionInfo(mockChange); + assert.equal(ri.getMaxParents(), 3); + }); + + test('getParentCountMap', () => { + const ri = new window.Gerrit.RevisionInfo(mockChange); + assert.deepEqual(ri.getParentCountMap(), {1: 3, 2: 2, 3: 1, 4: 2, 5: 3}); + }); + + test('getParentCount', () => { + const ri = new window.Gerrit.RevisionInfo(mockChange); + assert.deepEqual(ri.getParentCount(1), 3); + assert.deepEqual(ri.getParentCount(3), 1); + }); + + test('getParentCount', () => { + const ri = new window.Gerrit.RevisionInfo(mockChange); + assert.deepEqual(ri.getParentCount(1), 3); + assert.deepEqual(ri.getParentCount(3), 1); + }); + + test('getParentId', () => { + const ri = new window.Gerrit.RevisionInfo(mockChange); + assert.deepEqual(ri.getParentId(1, 2), 'p3'); + assert.deepEqual(ri.getParentId(2, 1), 'p4'); + assert.deepEqual(ri.getParentId(3, 0), 'p5'); + }); +}); </script>