diff options
author | Paladox none <thomasmulhall410@yahoo.com> | 2022-04-21 13:10:43 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2022-04-21 13:10:43 +0000 |
commit | 5b17467c95c5b56b5b2ebcec38b0363ad39a2d65 (patch) | |
tree | 1d1c9ec06b9929a552bee9ef6135eb0d4a3bfe70 | |
parent | 8b5b9c50eb8f4cfe5233bf186559cee1cd33ae8a (diff) | |
parent | fe69211ee01bdff2138802a51e450b9c8c3b1084 (diff) |
Merge changes Ifd3c323b,Ia8a19a22 into stable-3.6
* changes:
Convert gr-repo-access to lit
Migrate gr-access-section to lit
7 files changed, 1124 insertions, 1045 deletions
diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts index be6f56e573..8fa2e90042 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section.ts @@ -14,22 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import '@polymer/iron-input/iron-input'; -import '../../../styles/gr-font-styles'; -import '../../../styles/gr-form-styles'; -import '../../../styles/shared-styles'; import '../../shared/gr-button/gr-button'; import '../../shared/gr-icons/gr-icons'; import '../gr-permission/gr-permission'; -import {PolymerElement} from '@polymer/polymer/polymer-element'; -import {htmlTemplate} from './gr-access-section_html'; import { AccessPermissions, PermissionArray, PermissionArrayItem, toSortedPermissionsArray, } from '../../../utils/access-util'; -import {customElement, property} from '@polymer/decorators'; import { EditablePermissionInfo, PermissionAccessSection, @@ -41,10 +36,15 @@ import { LabelNameToLabelTypeInfoMap, RepoName, } from '../../../types/common'; -import {PolymerDomRepeatEvent} from '../../../types/types'; -import {fireEvent} from '../../../utils/event-util'; -import {GrButton} from '../../shared/gr-button/gr-button'; +import {fire, fireEvent} from '../../../utils/event-util'; import {IronInputElement} from '@polymer/iron-input/iron-input'; +import {fontStyles} from '../../../styles/gr-font-styles'; +import {formStyles} from '../../../styles/gr-form-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {LitElement, PropertyValues, html, css} from 'lit'; +import {customElement, property, query, state} from 'lit/decorators'; +import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events'; +import {assertIsDefined, queryAndAssert} from '../../../utils/common-util'; /** * Fired when the section has been modified or removed. @@ -66,22 +66,9 @@ const REFS_NAME = 'refs/'; const ON_BEHALF_OF = '(On Behalf Of)'; const LABEL = 'Label'; -export interface GrAccessSection { - $: { - addBtn: GrButton; - deleteBtn: GrButton; - editBtn: GrButton; - permissionSelect: HTMLSelectElement; - section: HTMLFieldSetElement; - undoRemoveBtn: GrButton; - }; -} - @customElement('gr-access-section') -export class GrAccessSection extends PolymerElement { - static get template() { - return htmlTemplate; - } +export class GrAccessSection extends LitElement { + @query('#permissionSelect') private permissionSelect?: HTMLSelectElement; @property({type: String}) repo?: RepoName; @@ -89,7 +76,7 @@ export class GrAccessSection extends PolymerElement { @property({type: Object}) capabilities?: CapabilityInfoMap; - @property({type: Object, notify: true, observer: '_updateSection'}) + @property({type: Object}) section?: PermissionAccessSection; @property({type: Object}) @@ -98,7 +85,7 @@ export class GrAccessSection extends PolymerElement { @property({type: Object}) labels?: LabelNameToLabelTypeInfoMap; - @property({type: Boolean, observer: '_handleEditingChanged'}) + @property({type: Boolean}) editing = false; @property({type: Boolean}) @@ -107,43 +94,220 @@ export class GrAccessSection extends PolymerElement { @property({type: Array}) ownerOf?: GitRef[]; - @property({type: String}) - _originalId?: GitRef; + // private but used in test + @state() originalId?: GitRef; - @property({type: Boolean}) - _editingRef = false; + // private but used in test + @state() editingRef = false; - @property({type: Boolean}) - _deleted = false; + // private but used in test + @state() deleted = false; - @property({type: Array}) - _permissions?: PermissionArray<EditablePermissionInfo>; + // private but used in test + @state() permissions?: PermissionArray<EditablePermissionInfo>; constructor() { super(); - this.addEventListener('access-saved', () => this._handleAccessSaved()); + this.addEventListener('access-saved', () => this.handleAccessSaved()); + } + + static override get styles() { + return [ + formStyles, + fontStyles, + sharedStyles, + css` + :host { + display: block; + margin-bottom: var(--spacing-l); + } + fieldset { + border: 1px solid var(--border-color); + } + .name { + align-items: center; + display: flex; + } + .header, + #deletedContainer { + align-items: center; + background: var(--table-header-background-color); + border-bottom: 1px dotted var(--border-color); + display: flex; + justify-content: space-between; + min-height: 3em; + padding: 0 var(--spacing-m); + } + #deletedContainer { + border-bottom: 0; + } + .sectionContent { + padding: var(--spacing-m); + } + #editBtn, + .editing #editBtn.global, + #deletedContainer, + .deleted #mainContainer, + #addPermission, + #deleteBtn, + .editingRef .name, + .editRefInput { + display: none; + } + .editing #editBtn, + .editingRef .editRefInput { + display: flex; + } + .deleted #deletedContainer { + display: flex; + } + .editing #addPermission, + #mainContainer, + .editing #deleteBtn { + display: block; + } + .editing #deleteBtn, + #undoRemoveBtn { + padding-right: var(--spacing-m); + } + `, + ]; } - _updateSection(section: PermissionAccessSection) { - this._permissions = toSortedPermissionsArray(section.value.permissions); - this._originalId = section.id; + override render() { + if (!this.section) return; + return html` + <fieldset + id="section" + class="gr-form-styles ${this.computeSectionClass()}" + > + <div id="mainContainer"> + <div class="header"> + <div class="name"> + <h3 class="heading-3">${this.computeSectionName()}</h3> + <gr-button + id="editBtn" + link + class=${this.section?.id === GLOBAL_NAME ? 'global' : ''} + @click=${this.editReference} + > + <iron-icon id="icon" icon="gr-icons:create"></iron-icon> + </gr-button> + </div> + <iron-input + class="editRefInput" + .bindValue=${this.section?.id} + @input=${this.handleValueChange} + @bind-value-changed=${this.handleIdBindValueChanged} + > + <input + class="editRefInput" + type="text" + @input=${this.handleValueChange} + /> + </iron-input> + <gr-button link id="deleteBtn" @click=${this.handleRemoveReference} + >Remove</gr-button + > + </div> + <!-- end header --> + <div class="sectionContent"> + ${this.permissions?.map((permission, index) => + this.renderPermission(permission, index) + )} + <div id="addPermission"> + Add permission: + <select id="permissionSelect"> + ${this.computePermissions().map(item => + this.renderPermissionOptions(item) + )} + </select> + <gr-button link id="addBtn" @click=${this.handleAddPermission} + >Add</gr-button + > + </div> + <!-- end addPermission --> + </div> + <!-- end sectionContent --> + </div> + <!-- end mainContainer --> + <div id="deletedContainer"> + <span>${this.computeSectionName()} was deleted</span> + <gr-button link="" id="undoRemoveBtn" @click=${this._handleUndoRemove} + >Undo</gr-button + > + </div> + <!-- end deletedContainer --> + </fieldset> + `; } - _handleAccessSaved() { - if (!this.section) { - return; + private renderPermission( + permission: PermissionArrayItem<EditablePermissionInfo>, + index: number + ) { + return html` + <gr-permission + .name=${this.computePermissionName(permission)} + .permission=${permission} + .labels=${this.labels} + .section=${this.section?.id} + .editing=${this.editing} + .groups=${this.groups} + .repo=${this.repo} + @added-permission-removed=${() => { + this.handleAddedPermissionRemoved(index); + }} + @permission-changed=${( + e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>> + ) => { + this.handlePermissionChanged(e, index); + }} + > + </gr-permission> + `; + } + + private renderPermissionOptions(item: { + id: string; + value: {name: string; id: string}; + }) { + return html`<option value=${item.value.id}>${item.value.name}</option>`; + } + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('section')) { + this.updateSection(); + } + if (changedProperties.has('editing')) { + this.handleEditingChanged(changedProperties.get('editing') as boolean); } + } + + // private but used in test + updateSection() { + this.permissions = toSortedPermissionsArray( + this.section!.value.permissions + ); + this.originalId = this.section!.id; + } + + // private but used in test + handleAccessSaved() { + if (!this.section) return; // Set a new 'original' value to keep track of after the value has been // saved. - this._updateSection(this.section); + this.updateSection(); } - _handleValueChange() { + // private but used in test + handleValueChange() { if (!this.section) { return; } if (!this.section.value.added) { - this.section.value.modified = this.section.id !== this._originalId; + this.section.value.modified = this.section.id !== this.originalId; + this.requestUpdate(); // 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 @@ -151,25 +315,28 @@ export class GrAccessSection extends PolymerElement { fireEvent(this, 'access-modified'); } this.section.value.updatedId = this.section.id; + this.requestUpdate(); } - _handleEditingChanged(editing: boolean, editingOld: boolean) { + private handleEditingChanged(editingOld: boolean) { // Ignore when editing gets set initially. if (!editingOld) { return; } - if (!this.section || !this._permissions) { + if (!this.section || !this.permissions) { return; } // Restore original values if no longer editing. - if (!editing) { - this._editingRef = false; - this._deleted = false; + if (!this.editing) { + this.editingRef = false; + this.deleted = false; delete this.section.value.deleted; // Restore section ref. - this.set(['section', 'id'], this._originalId); + this.section.id = this.originalId as GitRef; + this.requestUpdate(); + fire(this, 'section-changed', {value: this.section}); // Remove any unsaved but added permissions. - this._permissions = this._permissions.filter(p => !p.value.added); + 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]; @@ -178,22 +345,17 @@ export class GrAccessSection extends PolymerElement { } } - _computePermissions( - name: string, - capabilities?: CapabilityInfoMap, - labels?: LabelNameToLabelTypeInfoMap, - // This is just for triggering re-computation. We don't use the value. - _?: unknown - ) { + // private but used in test + computePermissions() { let allPermissions; const section = this.section; if (!section || !section.value) { return []; } - if (name === GLOBAL_NAME) { - allPermissions = toSortedPermissionsArray(capabilities); + if (section.id === GLOBAL_NAME) { + allPermissions = toSortedPermissionsArray(this.capabilities); } else { - const labelOptions = this._computeLabelOptions(labels); + const labelOptions = this.computeLabelOptions(); allPermissions = labelOptions.concat( toSortedPermissionsArray(AccessPermissions) ); @@ -203,22 +365,21 @@ export class GrAccessSection extends PolymerElement { ); } - _handleAddedPermissionRemoved(e: PolymerDomRepeatEvent) { - if (!this._permissions) { + private handleAddedPermissionRemoved(index: number) { + if (!this.permissions) { return; } - const index = e.model.index; - this._permissions = this._permissions + this.permissions = this.permissions .slice(0, index) - .concat(this._permissions.slice(index + 1, this._permissions.length)); + .concat(this.permissions.slice(index + 1, this.permissions.length)); } - _computeLabelOptions(labels?: LabelNameToLabelTypeInfoMap) { + computeLabelOptions() { const labelOptions = []; - if (!labels) { + if (!this.labels) { return []; } - for (const labelName of Object.keys(labels)) { + for (const labelName of Object.keys(this.labels)) { labelOptions.push({ id: 'label-' + labelName, value: { @@ -237,13 +398,12 @@ export class GrAccessSection extends PolymerElement { return labelOptions; } - _computePermissionName( - name: string, - permission: PermissionArrayItem<EditablePermissionInfo>, - capabilities?: CapabilityInfoMap + // private but used in test + computePermissionName( + permission: PermissionArrayItem<EditablePermissionInfo> ): string | undefined { - if (name === GLOBAL_NAME) { - return capabilities?.[permission.id].name; + if (this.section?.id === GLOBAL_NAME) { + return this.capabilities?.[permission.id].name; } else if (AccessPermissions[permission.id]) { return AccessPermissions[permission.id].name; } else if (permission.value.label) { @@ -256,15 +416,19 @@ export class GrAccessSection extends PolymerElement { return undefined; } - _computeSectionName(name: string) { + // private but used in test + computeSectionName() { + let name = this.section?.id; // 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; + this.editingRef = true; // Needed for the title value. This is the same default as GWT. - name = NEW_NAME; + name = NEW_NAME as GitRef; // Needed for the input field value. - this.set('section.id', name); + this.section!.id = name; + fire(this, 'section-changed', {value: this.section!}); + this.requestUpdate(); } if (name === GLOBAL_NAME) { return 'Global Capabilities'; @@ -274,14 +438,14 @@ export class GrAccessSection extends PolymerElement { return name; } - _handleRemoveReference() { + private handleRemoveReference() { if (!this.section) { return; } if (this.section.value.added) { fireEvent(this, 'added-section-removed'); } - this._deleted = true; + this.deleted = true; this.section.value.deleted = true; fireEvent(this, 'access-modified'); } @@ -290,59 +454,46 @@ export class GrAccessSection extends PolymerElement { if (!this.section) { return; } - this._deleted = false; + this.deleted = false; delete this.section.value.deleted; + this.requestUpdate(); } editRefInput() { - return this.root!.querySelector( - 'iron-input.editRefInput' - ) as IronInputElement; + return queryAndAssert<IronInputElement>(this, 'iron-input.editRefInput'); } editReference() { - this._editingRef = true; + this.editingRef = true; this.editRefInput().focus(); } - _isEditEnabled( - canUpload: boolean | undefined, - ownerOf: GitRef[] | undefined, - sectionId: GitRef - ) { - return canUpload || (ownerOf && ownerOf.indexOf(sectionId) >= 0); + private isEditEnabled() { + return ( + this.canUpload || + (this.ownerOf && this.ownerOf.indexOf(this.section!.id) >= 0) + ); } - _computeSectionClass( - editing: boolean, - canUpload: boolean | undefined, - ownerOf: GitRef[] | undefined, - editingRef: boolean, - deleted: boolean - ) { + // private but used in test + computeSectionClass() { const classList = []; - if ( - editing && - this.section && - this._isEditEnabled(canUpload, ownerOf, this.section.id) - ) { + if (this.editing && this.section && this.isEditEnabled()) { classList.push('editing'); } - if (editingRef) { + if (this.editingRef) { classList.push('editingRef'); } - if (deleted) { + if (this.deleted) { classList.push('deleted'); } return classList.join(' '); } - _computeEditBtnClass(name: string) { - return name === GLOBAL_NAME ? 'global' : ''; - } - - _handleAddPermission() { - const value = this.$.permissionSelect.value as GitRef; + // private but used in test + handleAddPermission() { + assertIsDefined(this.permissionSelect, 'permissionSelect'); + const value = this.permissionSelect.value as GitRef; const permission: PermissionArrayItem<EditablePermissionInfo> = { id: value, value: {rules: {}, added: true}, @@ -370,12 +521,31 @@ export class GrAccessSection extends PolymerElement { } // 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); + this.permissions!.push(permission); + this.section!.value.permissions[permission.id] = permission.value; + this.requestUpdate(); + fire(this, 'section-changed', {value: this.section!}); } + + private handleIdBindValueChanged = (e: BindValueChangeEvent) => { + this.section!.id = e.detail.value as GitRef; + this.requestUpdate(); + fire(this, 'section-changed', {value: this.section!}); + }; + + private handlePermissionChanged = ( + e: ValueChangedEvent<PermissionArrayItem<EditablePermissionInfo>>, + index: number + ) => { + this.permissions![index] = e.detail.value; + this.requestUpdate(); + }; } declare global { + interface HTMLElementEventMap { + 'section-changed': ValueChangedEvent<PermissionAccessSection>; + } interface HTMLElementTagNameMap { 'gr-access-section': GrAccessSection; } diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts deleted file mode 100644 index 65a31991d9..0000000000 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_html.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {html} from '@polymer/polymer/lib/utils/html-tag'; - -export const htmlTemplate = html` - <style include="gr-font-styles"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <style include="shared-styles"> - :host { - display: block; - margin-bottom: var(--spacing-l); - } - fieldset { - border: 1px solid var(--border-color); - } - .name { - align-items: center; - display: flex; - } - .header, - #deletedContainer { - align-items: center; - background: var(--table-header-background-color); - border-bottom: 1px dotted var(--border-color); - display: flex; - justify-content: space-between; - min-height: 3em; - padding: 0 var(--spacing-m); - } - #deletedContainer { - border-bottom: 0; - } - .sectionContent { - padding: var(--spacing-m); - } - #editBtn, - .editing #editBtn.global, - #deletedContainer, - .deleted #mainContainer, - #addPermission, - #deleteBtn, - .editingRef .name, - .editRefInput { - display: none; - } - .editing #editBtn, - .editingRef .editRefInput { - display: flex; - } - .deleted #deletedContainer { - display: flex; - } - .editing #addPermission, - #mainContainer, - .editing #deleteBtn { - display: block; - } - .editing #deleteBtn, - #undoRemoveBtn { - padding-right: var(--spacing-m); - } - </style> - <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)]]" - > - <div id="mainContainer"> - <div class="header"> - <div class="name"> - <h3 class="heading-3">[[_computeSectionName(section.id)]]</h3> - <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> - <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, capabilities)]]" - permission="{{permission}}" - labels="[[labels]]" - section="[[section.id]]" - editing="[[editing]]" - groups="[[groups]]" - repo="[[repo]]" - on-added-permission-removed="_handleAddedPermissionRemoved" - > - </gr-permission> - </template> - <div id="addPermission"> - Add permission: - <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.*)]]" - > - <option value="[[item.value.id]]">[[item.value.name]]</option> - </template> - </select> - <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 - > - </div> - <!-- end deletedContainer --> - </fieldset> -`; diff --git a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts index 88961ed771..f4d81c41e6 100644 --- a/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts +++ b/polygerrit-ui/app/elements/admin/gr-access-section/gr-access-section_test.ts @@ -23,19 +23,21 @@ import { } from '../../../utils/access-util'; import {GrAccessSection} from './gr-access-section'; import {GitRef} from '../../../types/common'; -import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions'; - -const fixture = fixtureFromElement('gr-access-section'); +import {queryAndAssert} from '../../../utils/common-util'; +import {GrButton} from '../../shared/gr-button/gr-button'; +import {fixture, html} from '@open-wc/testing-helpers'; suite('gr-access-section tests', () => { let element: GrAccessSection; - setup(() => { - element = fixture.instantiate(); + setup(async () => { + element = await fixture<GrAccessSection>(html` + <gr-access-section></gr-access-section> + `); }); suite('unit tests', () => { - setup(() => { + setup(async () => { element.section = { id: 'refs/*' as GitRef, value: { @@ -76,12 +78,12 @@ suite('gr-access-section tests', () => { default_value: 0, }, }; - element._updateSection(element.section); - flush(); + element.updateSection(); + await element.updateComplete; }); - test('_updateSection', () => { - // _updateSection was called in setup, so just make assertions. + test('updateSection', () => { + // updateSection was called in setup, so just make assertions. const expectedPermissions = [ { id: 'read' as GitRef, @@ -90,11 +92,11 @@ suite('gr-access-section tests', () => { }, }, ]; - assert.deepEqual(element._permissions, expectedPermissions); - assert.equal(element._originalId, element.section!.id); + assert.deepEqual(element.permissions, expectedPermissions); + assert.equal(element.originalId, element.section!.id); }); - test('_computeLabelOptions', () => { + test('computeLabelOptions', () => { const expectedLabelOptions = [ { id: 'label-Code-Review', @@ -112,20 +114,17 @@ suite('gr-access-section tests', () => { }, ]; - assert.deepEqual( - element._computeLabelOptions(element.labels), - expectedLabelOptions - ); + assert.deepEqual(element.computeLabelOptions(), expectedLabelOptions); }); - test('_handleAccessSaved', () => { - assert.equal(element._originalId, 'refs/*' as GitRef); + test('handleAccessSaved', () => { + assert.equal(element.originalId, 'refs/*' as GitRef); element.section!.id = 'refs/for/bar' as GitRef; - element._handleAccessSaved(); - assert.equal(element._originalId, 'refs/for/bar' as GitRef); + element.handleAccessSaved(); + assert.equal(element.originalId, 'refs/for/bar' as GitRef); }); - test('_computePermissions', () => { + test('computePermissions', () => { const capabilities = { push: { id: '', @@ -166,26 +165,54 @@ suite('gr-access-section tests', () => { }, ]; + element.section = { + id: 'refs/*' as GitRef, + value: { + permissions: { + read: { + rules: {}, + }, + }, + }, + }; + // For global capabilities, just return the sorted array filtered by // existing permissions. - let name = 'GLOBAL_CAPABILITIES'; - assert.deepEqual( - element._computePermissions(name, capabilities, element.labels), - expectedPermissions - ); + element.section = { + id: 'GLOBAL_CAPABILITIES' as GitRef, + value: { + permissions: { + read: { + rules: {}, + }, + }, + }, + }; + element.capabilities = capabilities; + assert.deepEqual(element.computePermissions(), expectedPermissions); // For everything else, include possible label values before filtering. - name = 'refs/for/*'; + element.section.id = 'refs/for/*' as GitRef; assert.deepEqual( - element._computePermissions(name, capabilities, element.labels), + element.computePermissions(), labelOptions .concat(toSortedPermissionsArray(AccessPermissions)) .filter(permission => permission.id !== 'read') ); }); - test('_computePermissionName', () => { - let name = 'GLOBAL_CAPABILITIES'; + test('computePermissionName', () => { + element.section = { + id: 'GLOBAL_CAPABILITIES' as GitRef, + value: { + permissions: { + read: { + rules: {}, + }, + }, + }, + }; + let permission; permission = { @@ -193,22 +220,22 @@ suite('gr-access-section tests', () => { value: {rules: {}}, }; assert.equal( - element._computePermissionName(name, permission, element.capabilities), + element.computePermissionName(permission), element.capabilities![permission.id].name ); - name = 'refs/for/*'; + element.section.id = 'refs/for/*' as GitRef; permission = { id: 'abandon' as GitRef, value: {rules: {}}, }; assert.equal( - element._computePermissionName(name, permission, element.capabilities), + element.computePermissionName(permission), AccessPermissions[permission.id].name ); - name = 'refs/for/*'; + element.section.id = 'refs/for/*' as GitRef; permission = { id: 'label-Code-Review' as GitRef, value: { @@ -218,7 +245,7 @@ suite('gr-access-section tests', () => { }; assert.equal( - element._computePermissionName(name, permission, element.capabilities), + element.computePermissionName(permission), 'Label Code-Review' ); @@ -231,136 +258,63 @@ suite('gr-access-section tests', () => { }; assert.equal( - element._computePermissionName(name, permission, element.capabilities), + element.computePermissionName(permission), 'Label Code-Review(On Behalf Of)' ); }); - test('_computeSectionName', () => { - let name = ''; + test('computeSectionName', () => { // When computing the section name for an undefined name, it means a // new section is being added. In this case, it should default to // 'refs/heads/*'. - element._editingRef = false; - assert.equal( - element._computeSectionName(name), - 'Reference: refs/heads/*' - ); - assert.isTrue(element._editingRef); + element.editingRef = false; + element.section!.id = '' as GitRef; + assert.equal(element.computeSectionName(), '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); + element.editingRef = false; + element.section!.id = 'GLOBAL_CAPABILITIES' as GitRef; + assert.equal(element.computeSectionName(), 'Global Capabilities'); + assert.isFalse(element.editingRef); + + element.section!.id = 'refs/for/*' as GitRef; + assert.equal(element.computeSectionName(), 'Reference: refs/for/*'); + assert.isFalse(element.editingRef); }); test('editReference', () => { element.editReference(); - assert.isTrue(element._editingRef); + assert.isTrue(element.editingRef); }); - test('_computeSectionClass', () => { - let editingRef = false; - let canUpload = false; - let ownerOf: GitRef[] | undefined = []; - let editing = false; - let deleted = false; - assert.equal( - element._computeSectionClass( - editing, - canUpload, - ownerOf, - editingRef, - deleted - ), - '' - ); + test('computeSectionClass', () => { + element.editingRef = false; + element.canUpload = false; + element.ownerOf = []; + element.editing = false; + element.deleted = false; + assert.equal(element.computeSectionClass(), ''); - editing = true; - assert.equal( - element._computeSectionClass( - editing, - canUpload, - ownerOf, - editingRef, - deleted - ), - '' - ); + element.editing = true; + assert.equal(element.computeSectionClass(), ''); - ownerOf = ['refs/*' as GitRef]; - assert.equal( - element._computeSectionClass( - editing, - canUpload, - ownerOf, - editingRef, - deleted - ), - 'editing' - ); + element.ownerOf = ['refs/*' as GitRef]; + assert.equal(element.computeSectionClass(), 'editing'); - ownerOf = []; - canUpload = true; - assert.equal( - element._computeSectionClass( - editing, - canUpload, - ownerOf, - editingRef, - deleted - ), - 'editing' - ); + element.ownerOf = []; + element.canUpload = true; + assert.equal(element.computeSectionClass(), 'editing'); - editingRef = true; - assert.equal( - element._computeSectionClass( - editing, - canUpload, - ownerOf, - editingRef, - deleted - ), - 'editing editingRef' - ); + element.editingRef = true; + assert.equal(element.computeSectionClass(), 'editing editingRef'); - deleted = true; - assert.equal( - element._computeSectionClass( - editing, - canUpload, - ownerOf, - editingRef, - deleted - ), - 'editing editingRef deleted' - ); + element.deleted = true; + assert.equal(element.computeSectionClass(), '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), ''); + element.editingRef = false; + assert.equal(element.computeSectionClass(), 'editing deleted'); }); }); @@ -380,7 +334,7 @@ suite('gr-access-section tests', () => { }; }); suite('Global section', () => { - setup(() => { + setup(async () => { element.section = { id: 'GLOBAL_CAPABILITIES' as GitRef, value: { @@ -409,23 +363,41 @@ suite('gr-access-section tests', () => { name: 'Create Account', }, }; - element._updateSection(element.section); - flush(); + element.updateSection(); + await element.updateComplete; }); 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')); + assert.isFalse( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('editing') + ); + assert.isFalse( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('deleted') + ); + assert.isTrue( + queryAndAssert<GrButton>(element, '#editBtn').classList.contains( + 'global' + ) + ); element.editing = true; element.canUpload = true; element.ownerOf = []; - assert.equal(getComputedStyle(element.$.editBtn).display, 'none'); + assert.equal( + getComputedStyle(queryAndAssert<GrButton>(element, '#editBtn')) + .display, + 'none' + ); }); }); suite('Non-global section', () => { - setup(() => { + setup(async () => { element.section = { id: 'refs/*' as GitRef, value: { @@ -437,32 +409,51 @@ suite('gr-access-section tests', () => { }, }; element.capabilities = {}; - element._updateSection(element.section); - flush(); + element.updateSection(); + await element.updateComplete; }); - 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')); + test('classes are assigned correctly', async () => { + assert.isFalse( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('editing') + ); + assert.isFalse( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('deleted') + ); + assert.isFalse( + queryAndAssert<GrButton>(element, '#editBtn').classList.contains( + 'global' + ) + ); element.editing = true; element.canUpload = true; element.ownerOf = []; - flush(); - assert.notEqual(getComputedStyle(element.$.editBtn).display, 'none'); + await element.updateComplete; + assert.notEqual( + getComputedStyle(queryAndAssert<GrButton>(element, '#editBtn')) + .display, + 'none' + ); }); - test('add permission', () => { + test('add permission', async () => { element.editing = true; - element.$.permissionSelect.value = 'label-Code-Review'; - assert.equal(element._permissions!.length, 1); + queryAndAssert<HTMLSelectElement>(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); - flush(); + queryAndAssert<GrButton>(element, '#addBtn').click(); + await element.updateComplete; // The permission is added to both the permissions array and also // the section's permission object. - assert.equal(element._permissions!.length, 2); + assert.equal(element.permissions!.length, 2); let permission; permission = { @@ -473,17 +464,18 @@ suite('gr-access-section tests', () => { rules: {}, }, }; - assert.equal(element._permissions!.length, 2); - assert.deepEqual(element._permissions![1], permission); + 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); - flush(); + queryAndAssert<HTMLSelectElement>(element, '#permissionSelect').value = + 'abandon'; + queryAndAssert<GrButton>(element, '#addBtn').click(); + await element.updateComplete; permission = { id: 'abandon' as GitRef, @@ -493,8 +485,8 @@ suite('gr-access-section tests', () => { }, }; - assert.equal(element._permissions!.length, 3); - assert.deepEqual(element._permissions![2], permission); + 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'], @@ -503,7 +495,8 @@ suite('gr-access-section tests', () => { // Unsaved changes are discarded when editing is cancelled. element.editing = false; - assert.equal(element._permissions!.length, 1); + await element.updateComplete; + assert.equal(element.permissions!.length, 1); assert.equal(Object.keys(element.section!.value.permissions).length, 1); }); @@ -514,97 +507,131 @@ suite('gr-access-section tests', () => { id: 'refs/for/bar' as GitRef, value: {permissions: {}}, }; - assert.isFalse(element.$.section.classList.contains('editing')); + await element.updateComplete; + assert.isFalse( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('editing') + ); element.editing = true; - assert.isTrue(element.$.section.classList.contains('editing')); - assert.isFalse(element._editingRef); - MockInteractions.tap(element.$.editBtn); + await element.updateComplete; + assert.isTrue( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('editing') + ); + assert.isFalse(element.editingRef); + queryAndAssert<GrButton>(element, '#editBtn').click(); element.editRefInput().bindValue = 'new/ref'; - await flush(); + await element.updateComplete; assert.equal(element.section.id, 'new/ref'); - assert.isTrue(element._editingRef); - assert.isTrue(element.$.section.classList.contains('editingRef')); + assert.isTrue(element.editingRef); + assert.isTrue( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('editingRef') + ); element.editing = false; - assert.isFalse(element._editingRef); + await element.updateComplete; + assert.isFalse(element.editingRef); assert.equal(element.section.id, 'refs/for/bar'); }); - test('_handleValueChange', () => { + test('handleValueChange', async () => { // For an existing section. const modifiedHandler = sinon.stub(); element.section = { id: 'refs/for/bar' as GitRef, value: {permissions: {}}, }; + await element.updateComplete; assert.notOk(element.section.value.updatedId); element.section.id = 'refs/for/baz' as GitRef; + await element.updateComplete; element.addEventListener('access-modified', modifiedHandler); assert.isNotOk(element.section.value.modified); - element._handleValueChange(); + 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' as GitRef; - element._handleValueChange(); + await element.updateComplete; + element.handleValueChange(); assert.isFalse(element.section.value.modified); assert.equal(modifiedHandler.callCount, 2); // For a new section. element.section.value.added = true; - element._handleValueChange(); + await element.updateComplete; + element.handleValueChange(); assert.isFalse(element.section.value.modified); assert.equal(modifiedHandler.callCount, 2); element.section.id = 'refs/for/bar' as GitRef; - element._handleValueChange(); + await element.updateComplete; + element.handleValueChange(); assert.isFalse(element.section.value.modified); assert.equal(modifiedHandler.callCount, 2); }); - test('remove section', () => { + test('remove section', async () => { element.editing = true; element.canUpload = true; element.ownerOf = []; - assert.isFalse(element._deleted); + await element.updateComplete; + assert.isFalse(element.deleted); assert.isNotOk(element.section!.value.deleted); - MockInteractions.tap(element.$.deleteBtn); - flush(); - assert.isTrue(element._deleted); + queryAndAssert<GrButton>(element, '#deleteBtn').click(); + await element.updateComplete; + assert.isTrue(element.deleted); assert.isTrue(element.section!.value.deleted); - assert.isTrue(element.$.section.classList.contains('deleted')); + assert.isTrue( + queryAndAssert<HTMLFieldSetElement>( + element, + '#section' + ).classList.contains('deleted') + ); assert.isTrue(element.section!.value.deleted); - MockInteractions.tap(element.$.undoRemoveBtn); - flush(); - assert.isFalse(element._deleted); + queryAndAssert<GrButton>(element, '#undoRemoveBtn').click(); + await element.updateComplete; + assert.isFalse(element.deleted); assert.isNotOk(element.section!.value.deleted); - MockInteractions.tap(element.$.deleteBtn); - assert.isTrue(element._deleted); + queryAndAssert<GrButton>(element, '#deleteBtn').click(); + await element.updateComplete; + assert.isTrue(element.deleted); assert.isTrue(element.section!.value.deleted); element.editing = false; - assert.isFalse(element._deleted); + await element.updateComplete; + assert.isFalse(element.deleted); assert.isNotOk(element.section!.value.deleted); }); - test('removing an added permission', () => { + test('removing an added permission', async () => { element.editing = true; - assert.equal(element._permissions!.length, 1); + await element.updateComplete; + assert.equal(element.permissions!.length, 1); element.shadowRoot!.querySelector('gr-permission')!.dispatchEvent( new CustomEvent('added-permission-removed', { composed: true, bubbles: true, }) ); - flush(); - assert.equal(element._permissions!.length, 0); + await element.updateComplete; + assert.equal(element.permissions!.length, 0); }); - test('remove an added section', () => { + test('remove an added section', async () => { const removeStub = sinon.stub(); element.addEventListener('added-section-removed', removeStub); element.editing = true; element.section!.value.added = true; - MockInteractions.tap(element.$.deleteBtn); + await element.updateComplete; + queryAndAssert<GrButton>(element, '#deleteBtn').click(); + await element.updateComplete; assert.isTrue(removeStub.called); }); }); diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts index 81d53f2c28..fba349d997 100644 --- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts +++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission.ts @@ -46,7 +46,7 @@ import { EditableProjectAccessGroups, } from '../gr-repo-access/gr-repo-access-interfaces'; import {getAppContext} from '../../../services/app-context'; -import {fireEvent} from '../../../utils/event-util'; +import {fire, fireEvent} from '../../../utils/event-util'; import {sharedStyles} from '../../../styles/shared-styles'; import {paperStyles} from '../../../styles/gr-paper-styles'; import {formStyles} from '../../../styles/gr-form-styles'; @@ -353,6 +353,7 @@ export class GrPermission extends LitElement { // Restore exclusive bit to original. this.permission.value.exclusive = this.originalExclusiveValue; + fire(this, 'permission-changed', {value: this.permission}); this.requestUpdate(); } } @@ -578,17 +579,18 @@ export class GrPermission extends LitElement { } private handleRuleChanged(e: CustomEvent, index: number) { - if (this.rules === undefined || e.detail.value === undefined) return; - if (isNaN(index)) { - return; - } - this.rules.splice(index, e.detail.value); + this.rules!.splice(index, e.detail.value); this.handleRulesChanged(); this.requestUpdate(); } } declare global { + interface HTMLElementEventMap { + 'permission-changed': ValueChangedEvent< + PermissionArrayItem<EditablePermissionInfo> + >; + } interface HTMLElementTagNameMap { 'gr-permission': GrPermission; } diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts index f8b36c4c97..07a91883a8 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access.ts @@ -14,18 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import '../../../styles/gr-font-styles'; -import '../../../styles/gr-menu-page-styles'; -import '../../../styles/gr-subpage-styles'; -import '../../../styles/shared-styles'; + import '../gr-access-section/gr-access-section'; -import {flush} from '@polymer/polymer/lib/legacy/polymer.dom'; -import {PolymerElement} from '@polymer/polymer/polymer-element'; -import {htmlTemplate} from './gr-repo-access_html'; import {encodeURL, getBaseUrl, singleDecodeURL} from '../../../utils/url-util'; import {GerritNav} from '../../core/gr-navigation/gr-navigation'; import {toSortedPermissionsArray} from '../../../utils/access-util'; -import {customElement, property} from '@polymer/decorators'; import { RepoName, ProjectInfo, @@ -51,106 +44,296 @@ import { import {firePageError, fireAlert} from '../../../utils/event-util'; import {getAppContext} from '../../../services/app-context'; import {WebLinkInfo} from '../../../types/diff'; +import {fontStyles} from '../../../styles/gr-font-styles'; +import {menuPageStyles} from '../../../styles/gr-menu-page-styles'; +import {subpageStyles} from '../../../styles/gr-subpage-styles'; +import {sharedStyles} from '../../../styles/shared-styles'; +import {LitElement, PropertyValues, css, html} from 'lit'; +import {customElement, property, query, state} from 'lit/decorators'; +import {assertIsDefined} from '../../../utils/common-util'; +import {ValueChangedEvent} from '../../../types/events'; +import {ifDefined} from 'lit/directives/if-defined'; const NOTHING_TO_SAVE = 'No changes to save.'; const MAX_AUTOCOMPLETE_RESULTS = 50; +declare global { + interface HTMLElementEventMap { + 'text-changed': CustomEvent<string>; + } + interface HTMLElementTagNameMap { + 'gr-repo-access': GrRepoAccess; + } +} + /** * Fired when save is a no-op * * @event show-alert */ @customElement('gr-repo-access') -export class GrRepoAccess extends PolymerElement { - static get template() { - return htmlTemplate; - } +export class GrRepoAccess extends LitElement { + @query('gr-access-section:last-of-type') accessSection?: GrAccessSection; - @property({type: String, observer: '_repoChanged'}) + @property({type: String}) repo?: RepoName; @property({type: String}) path?: string; - @property({type: Boolean}) - _canUpload?: boolean = false; // restAPI can return undefined - - @property({type: String}) - _inheritFromFilter?: RepoName; + // private but used in test + @state() canUpload?: boolean = false; // restAPI can return undefined - @property({type: Object}) - _query: AutocompleteQuery; + // private but used in test + @state() inheritFromFilter?: RepoName; - @property({type: Array}) - _ownerOf?: GitRef[]; + // private but used in test + @state() ownerOf?: GitRef[]; - @property({type: Object}) - _capabilities?: CapabilityInfoMap; + // private but used in test + @state() capabilities?: CapabilityInfoMap; - @property({type: Object}) - _groups?: ProjectAccessGroups; + // private but used in test + @state() groups?: ProjectAccessGroups; - @property({type: Object}) - _inheritsFrom?: ProjectInfo; + // private but used in test + @state() inheritsFrom?: ProjectInfo; - @property({type: Object}) - _labels?: LabelNameToLabelTypeInfoMap; + // private but used in test + @state() labels?: LabelNameToLabelTypeInfoMap; - @property({type: Object}) - _local?: EditableLocalAccessSectionInfo; + // private but used in test + @state() local?: EditableLocalAccessSectionInfo; - @property({type: Boolean, observer: '_handleEditingChanged'}) - _editing = false; + // private but used in test + @state() editing = false; - @property({type: Boolean}) - _modified = false; + // private but used in test + @state() modified = false; - @property({type: Array}) - _sections?: PermissionAccessSection[]; + // private but used in test + @state() sections?: PermissionAccessSection[]; - @property({type: Array}) - _weblinks?: WebLinkInfo[]; + @state() private weblinks?: WebLinkInfo[]; - @property({type: Boolean}) - _loading = true; + // private but used in test + @state() loading = true; // private but used in the tests originalInheritsFrom?: ProjectInfo; + private readonly query: AutocompleteQuery; + private readonly restApiService = getAppContext().restApiService; constructor() { super(); - this._query = () => this._getInheritFromSuggestions(); + this.query = () => this.getInheritFromSuggestions(); this.addEventListener('access-modified', () => this._handleAccessModified() ); } + static override get styles() { + return [ + fontStyles, + menuPageStyles, + subpageStyles, + sharedStyles, + css` + gr-button, + #inheritsFrom, + #editInheritFromInput, + .editing #inheritFromName, + .weblinks, + .editing .invisible { + display: none; + } + #inheritsFrom.show { + display: flex; + min-height: 2em; + align-items: center; + } + .weblink { + margin-right: var(--spacing-xs); + } + gr-access-section { + margin-top: var(--spacing-l); + } + .weblinks.show, + .referenceContainer { + display: block; + } + .rightsText { + margin-right: var(--spacing-s); + } + + .editing gr-button, + .admin #editBtn { + display: inline-block; + margin: var(--spacing-l) 0; + } + .editing #editInheritFromInput { + display: inline-block; + } + `, + ]; + } + + override render() { + return html` + <div class="main ${this.computeMainClass()}"> + <div id="loading" class=${this.loading ? 'loading' : ''}> + Loading... + </div> + <div id="loadedContent" class=${this.loading ? 'loading' : ''}> + <h3 + id="inheritsFrom" + class="heading-3 ${this.editing || this.inheritsFrom?.id?.length + ? 'show' + : ''}" + > + <span class="rightsText">Rights Inherit From</span> + <a + id="inheritFromName" + href=${this.computeParentHref()} + rel="noopener" + > + ${this.inheritsFrom?.name}</a + > + <gr-autocomplete + id="editInheritFromInput" + .text=${this.inheritFromFilter} + .query=${this.query} + @commit=${(e: ValueChangedEvent) => { + this.handleUpdateInheritFrom(e); + }} + @bind-value-changed=${(e: ValueChangedEvent) => { + this.handleUpdateInheritFrom(e); + }} + @text-changed=${(e: ValueChangedEvent) => { + this.handleEditInheritFromTextChanged(e); + }} + ></gr-autocomplete> + </h3> + <div class="weblinks ${this.weblinks?.length ? 'show' : ''}"> + History: + ${this.weblinks?.map(webLink => this.renderWebLinks(webLink))} + </div> + ${this.sections?.map((section, index) => + this.renderPermissionSections(section, index) + )} + <div class="referenceContainer"> + <gr-button + id="addReferenceBtn" + @click=${() => this.handleCreateSection()} + >Add Reference</gr-button + > + </div> + <div> + <gr-button + id="editBtn" + @click=${() => { + this.handleEdit(); + }} + >${this.editing ? 'Cancel' : 'Edit'}</gr-button + > + <gr-button + id="saveBtn" + class=${this.ownerOf && this.ownerOf.length === 0 + ? 'invisible' + : ''} + primary + ?disabled=${!this.modified} + @click=${this.handleSave} + >Save</gr-button + > + <gr-button + id="saveReviewBtn" + class=${!this.canUpload ? 'invisible' : ''} + primary + ?disabled=${!this.modified} + @click=${this.handleSaveForReview} + >Save for review</gr-button + > + </div> + </div> + </div> + `; + } + + private renderWebLinks(webLink: WebLinkInfo) { + return html` + <a + class="weblink" + href=${webLink.url} + rel="noopener" + target=${ifDefined(webLink.target)} + > + ${webLink.name} + </a> + `; + } + + private renderPermissionSections( + section: PermissionAccessSection, + index: number + ) { + return html` + <gr-access-section + .capabilities=${this.capabilities} + .section=${section} + .labels=${this.labels} + .canUpload=${this.canUpload} + .editing=${this.editing} + .ownerOf=${this.ownerOf} + .groups=${this.groups} + .repo=${this.repo} + @added-section-removed=${() => { + this.handleAddedSectionRemoved(index); + }} + @section-changed=${(e: ValueChangedEvent<PermissionAccessSection>) => { + this.handleAccessSectionChanged(e, index); + }} + ></gr-access-section> + `; + } + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('repo')) { + this._repoChanged(this.repo); + } + + if (changedProperties.has('editing')) { + this.handleEditingChanged(changedProperties.get('editing') as boolean); + this.requestUpdate(); + } + } + _handleAccessModified() { - this._modified = true; + this.modified = true; } _repoChanged(repo?: RepoName) { - this._loading = true; + this.loading = true; if (!repo) { return Promise.resolve(); } - return this._reload(repo); + return this.reload(repo); } - _reload(repo: RepoName) { + private reload(repo: RepoName) { const errFn = (response?: Response | null) => { firePageError(response); }; - this._editing = false; + this.editing = false; // Always reset sections when a project changes. - this._sections = []; + this.sections = []; const sectionsPromises = this.restApiService .getRepoAccessRights(repo, errFn) .then(res => { @@ -162,26 +345,26 @@ export class GrRepoAccess extends PolymerElement { // the ones data bound to gr-autocomplete, so the original value // can be restored if the user cancels. if (res.inherits_from) { - this._inheritsFrom = {...res.inherits_from}; + this.inheritsFrom = {...res.inherits_from}; this.originalInheritsFrom = {...res.inherits_from}; } else { - this._inheritsFrom = undefined; + this.inheritsFrom = undefined; this.originalInheritsFrom = undefined; } // 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.inheritFromFilter = res.inherits_from ? res.inherits_from.name : ('' as RepoName); // 'as EditableLocalAccessSectionInfo' is required because res.local // type doesn't have index signature - this._local = res.local as EditableLocalAccessSectionInfo; - this._groups = res.groups; - this._weblinks = res.config_web_links || []; - this._canUpload = res.can_upload; - this._ownerOf = res.owner_of || []; - return toSortedPermissionsArray(this._local); + this.local = res.local as EditableLocalAccessSectionInfo; + this.groups = res.groups; + this.weblinks = res.config_web_links || []; + this.canUpload = res.can_upload; + this.ownerOf = res.owner_of || []; + return toSortedPermissionsArray(this.local); }); const capabilitiesPromises = this.restApiService @@ -209,25 +392,26 @@ export class GrRepoAccess extends PolymerElement { capabilitiesPromises, labelsPromises, ]).then(([sections, capabilities, labels]) => { - this._capabilities = capabilities; - this._labels = labels; - this._sections = sections; - this._loading = false; + this.capabilities = capabilities; + this.labels = labels; + this.sections = sections; + this.loading = false; }); } - _handleUpdateInheritFrom(e: CustomEvent<{value: string}>) { - this._inheritsFrom = { - ...(this._inheritsFrom ?? {}), + // private but used in test + handleUpdateInheritFrom(e: ValueChangedEvent) { + this.inheritsFrom = { + ...(this.inheritsFrom ?? {}), id: e.detail.value as UrlEncodedRepoName, - name: this._inheritFromFilter, + name: this.inheritFromFilter, }; this._handleAccessModified(); } - _getInheritFromSuggestions(): Promise<AutocompleteSuggestion[]> { + private getInheritFromSuggestions(): Promise<AutocompleteSuggestion[]> { return this.restApiService - .getRepos(this._inheritFromFilter, MAX_AUTOCOMPLETE_RESULTS) + .getRepos(this.inheritFromFilter, MAX_AUTOCOMPLETE_RESULTS) .then(response => { const projects: AutocompleteSuggestion[] = []; if (!response) { @@ -243,67 +427,47 @@ export class GrRepoAccess extends PolymerElement { }); } - _computeLoadingClass(loading: boolean) { - return loading ? 'loading' : ''; - } - - _handleEdit() { - this._editing = !this._editing; - } - - _editOrCancel(editing: boolean) { - return editing ? 'Cancel' : 'Edit'; + private handleEdit() { + this.editing = !this.editing; } - _computeWebLinkClass(weblinks?: string[]) { - return weblinks && weblinks.length ? 'show' : ''; - } - - _computeShowInherit(inheritsFrom?: ProjectInfo) { - return this._editing || inheritsFrom?.id?.length ? 'show' : ''; - } - - // TODO(TS): Unclear what is model here, provide a better explanation - _handleAddedSectionRemoved(e: CustomEvent & {model: {index: string}}) { - if (!this._sections) { - return; - } - const index = Number(e.model.index); - if (isNaN(index)) { - return; - } - this._sections = this._sections + private handleAddedSectionRemoved(index: number) { + if (!this.sections) return; + this.sections = this.sections .slice(0, index) - .concat(this._sections.slice(index + 1, this._sections.length)); + .concat(this.sections.slice(index + 1, this.sections.length)); } - _handleEditingChanged(editing: boolean, editingOld: boolean) { + private handleEditingChanged(editingOld: boolean) { // Ignore when editing gets set initially. - if (!editingOld || editing) { + if (!editingOld || this.editing) { return; } // Remove any unsaved but added refs. - if (this._sections) { - this._sections = this._sections.filter(p => !p.value.added); + if (this.sections) { + this.sections = this.sections.filter(p => !p.value.added); } // Restore inheritFrom. - if (this._inheritsFrom) { - this._inheritsFrom = this.originalInheritsFrom + if (this.inheritsFrom) { + this.inheritsFrom = this.originalInheritsFrom ? {...this.originalInheritsFrom} : undefined; - this._inheritFromFilter = this.originalInheritsFrom?.name; + this.inheritFromFilter = this.originalInheritsFrom?.name; } - if (!this._local) { + if (!this.local) { return; } - for (const key of Object.keys(this._local)) { - if (this._local[key].added) { - delete this._local[key]; + for (const key of Object.keys(this.local)) { + if (this.local[key].added) { + delete this.local[key]; } } } - _updateRemoveObj(addRemoveObj: {remove: PropertyTreeNode}, path: string[]) { + private updateRemoveObj( + addRemoveObj: {remove: PropertyTreeNode}, + path: string[] + ) { let curPos: PropertyTreeNode = addRemoveObj.remove; for (const item of path) { if (!curPos[item]) { @@ -327,7 +491,7 @@ export class GrRepoAccess extends PolymerElement { return addRemoveObj; } - _updateAddObj( + private updateAddObj( addRemoveObj: {add: PropertyTreeNode}, path: string[], value: PropertyTreeNode | PrimitiveValue @@ -351,8 +515,10 @@ export class GrRepoAccess extends PolymerElement { /** * Used to recursively remove any objects with a 'deleted' bit. + * + * private but used in test */ - _recursivelyRemoveDeleted(obj?: PropertyTreeNode) { + recursivelyRemoveDeleted(obj?: PropertyTreeNode) { if (!obj) return; for (const k of Object.keys(obj)) { const node = obj[k]; @@ -361,12 +527,13 @@ export class GrRepoAccess extends PolymerElement { delete obj[k]; return; } - this._recursivelyRemoveDeleted(node); + this.recursivelyRemoveDeleted(node); } } } - _recursivelyUpdateAddRemoveObj( + // private but used in test + recursivelyUpdateAddRemoveObj( obj: PropertyTreeNode | undefined, addRemoveObj: { add: PropertyTreeNode; @@ -381,36 +548,36 @@ export class GrRepoAccess extends PolymerElement { const updatedId = node.updatedId; const ref = updatedId ? updatedId : k; if (node.deleted) { - this._updateRemoveObj(addRemoveObj, path.concat(k)); + this.updateRemoveObj(addRemoveObj, path.concat(k)); continue; } else if (node.modified) { - this._updateRemoveObj(addRemoveObj, path.concat(k)); - this._updateAddObj(addRemoveObj, path.concat(ref), node); + this.updateRemoveObj(addRemoveObj, path.concat(k)); + this.updateAddObj(addRemoveObj, path.concat(ref), node); /* 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. */ + 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( + this.recursivelyRemoveDeleted( addRemoveObj.add[updatedId] as PropertyTreeNode ); } continue; } else if (node.added) { - this._updateAddObj(addRemoveObj, path.concat(ref), node); + this.updateAddObj(addRemoveObj, path.concat(ref), node); /** * 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( + this.recursivelyRemoveDeleted( addRemoveObj.add[k] as PropertyTreeNode ); continue; } - this._recursivelyUpdateAddRemoveObj(node, addRemoveObj, path.concat(k)); + this.recursivelyUpdateAddRemoveObj(node, addRemoveObj, path.concat(k)); } } } @@ -418,8 +585,10 @@ export class GrRepoAccess extends PolymerElement { /** * Returns an object formatted for saving or submitting access changes for * review + * + * private but used in test */ - _computeAddAndRemove() { + computeAddAndRemove() { const addRemoveObj: { add: PropertyTreeNode; remove: PropertyTreeNode; @@ -432,8 +601,8 @@ export class GrRepoAccess extends PolymerElement { const originalInheritsFromId = this.originalInheritsFrom ? singleDecodeURL(this.originalInheritsFrom.id) : undefined; - const inheritsFromId = this._inheritsFrom - ? singleDecodeURL(this._inheritsFrom.id) + const inheritsFromId = this.inheritsFrom + ? singleDecodeURL(this.inheritsFrom.id) : undefined; const inheritFromChanged = @@ -442,12 +611,12 @@ export class GrRepoAccess extends PolymerElement { // Inherit from added (did not have one initially); (!originalInheritsFromId && inheritsFromId); - if (!this._local) { + if (!this.local) { return addRemoveObj; } - this._recursivelyUpdateAddRemoveObj( - this._local as unknown as PropertyTreeNode, + this.recursivelyUpdateAddRemoveObj( + this.local as unknown as PropertyTreeNode, addRemoveObj ); @@ -457,30 +626,25 @@ export class GrRepoAccess extends PolymerElement { return addRemoveObj; } - _handleCreateSection() { - if (!this._local) { - return; - } + private async handleCreateSection() { + if (!this.local) return; 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]) { + while (this.local[newRef]) { newRef = `${newRef}*`; } const section = {permissions: {}, added: true}; - this.push('_sections', {id: newRef, value: section}); - this.set(['_local', newRef], section); - flush(); + this.sections!.push({id: newRef as GitRef, value: section}); + this.local[newRef] = section; + this.requestUpdate(); + assertIsDefined(this.accessSection, 'accessSection'); // Template already instantiated at this point - ( - this.root!.querySelector( - 'gr-access-section:last-of-type' - ) as GrAccessSection - ).editReference(); + this.accessSection.editReference(); } - _getObjforSave(): ProjectAccessInput | undefined { - const addRemoveObj = this._computeAddAndRemove(); + private getObjforSave(): ProjectAccessInput | undefined { + const addRemoveObj = this.computeAddAndRemove(); // If there are no changes, don't actually save. if ( !Object.keys(addRemoveObj.add).length && @@ -500,8 +664,9 @@ export class GrRepoAccess extends PolymerElement { return obj; } - _handleSave(e: Event) { - const obj = this._getObjforSave(); + // private but used in test + handleSave(e: Event) { + const obj = this.getObjforSave(); if (!obj) { return; } @@ -516,18 +681,19 @@ export class GrRepoAccess extends PolymerElement { return this.restApiService .setRepoAccessRights(repo, obj) .then(() => { - this._reload(repo); + this.reload(repo); }) .finally(() => { - this._modified = false; + this.modified = false; if (button) { button.loading = false; } }); } - _handleSaveForReview(e: Event) { - const obj = this._getObjforSave(); + // private but used in test + handleSaveForReview(e: Event) { + const obj = this.getObjforSave(); if (!obj) { return; } @@ -544,43 +710,42 @@ export class GrRepoAccess extends PolymerElement { GerritNav.navigateToChange(change); }) .finally(() => { - this._modified = false; + this.modified = false; if (button) { button.loading = false; } }); } - _computeSaveReviewBtnClass(canUpload?: boolean) { - return !canUpload ? 'invisible' : ''; - } - - _computeSaveBtnClass(ownerOf?: GitRef[]) { - return ownerOf && ownerOf.length === 0 ? 'invisible' : ''; - } - - _computeMainClass( - ownerOf: GitRef[] | undefined, - canUpload: boolean, - editing: boolean - ) { + // private but used in test + computeMainClass() { const classList = []; - if ((ownerOf && ownerOf.length > 0) || canUpload) { + if ((this.ownerOf && this.ownerOf.length > 0) || this.canUpload) { classList.push('admin'); } - if (editing) { + if (this.editing) { classList.push('editing'); } return classList.join(' '); } - _computeParentHref(repoName: RepoName) { - return getBaseUrl() + `/admin/repos/${encodeURL(repoName, true)},access`; + computeParentHref() { + if (!this.inheritsFrom?.name) return ''; + return `${getBaseUrl()}/admin/repos/${encodeURL( + this.inheritsFrom.name, + true + )},access`; } -} -declare global { - interface HTMLElementTagNameMap { - 'gr-repo-access': GrRepoAccess; + private handleEditInheritFromTextChanged(e: ValueChangedEvent) { + this.inheritFromFilter = e.detail.value as RepoName; + } + + private handleAccessSectionChanged( + e: ValueChangedEvent<PermissionAccessSection>, + index: number + ) { + this.sections![index] = e.detail.value; + this.requestUpdate(); } } diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts deleted file mode 100644 index 8f88619fae..0000000000 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_html.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @license - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import {html} from '@polymer/polymer/lib/utils/html-tag'; - -export const htmlTemplate = html` - <style include="gr-font-styles"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <style include="shared-styles"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <style include="gr-subpage-styles"> - gr-button, - #inheritsFrom, - #editInheritFromInput, - .editing #inheritFromName, - .weblinks, - .editing .invisible { - display: none; - } - #inheritsFrom.show { - display: flex; - min-height: 2em; - align-items: center; - } - .weblink { - margin-right: var(--spacing-xs); - } - gr-access-section { - margin-top: var(--spacing-l); - } - .weblinks.show, - .referenceContainer { - display: block; - } - .rightsText { - margin-right: var(--spacing-s); - } - - .editing gr-button, - .admin #editBtn { - display: inline-block; - margin: var(--spacing-l) 0; - } - .editing #editInheritFromInput { - display: inline-block; - } - </style> - <style include="gr-menu-page-styles"> - /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ - </style> - <div class$="main [[_computeMainClass(_ownerOf, _canUpload, _editing)]]"> - <div id="loading" class$="[[_computeLoadingClass(_loading)]]"> - Loading... - </div> - <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]"> - <h3 - id="inheritsFrom" - class$="heading-3 [[_computeShowInherit(_inheritsFrom)]]" - > - <span class="rightsText">Rights Inherit From</span> - <a - href$="[[_computeParentHref(_inheritsFrom.name)]]" - rel="noopener" - id="inheritFromName" - > - [[_inheritsFrom.name]]</a - > - <gr-autocomplete - id="editInheritFromInput" - text="{{_inheritFromFilter}}" - query="[[_query]]" - on-commit="_handleUpdateInheritFrom" - on-bind-value-changed="_handleUpdateInheritFrom" - ></gr-autocomplete> - </h3> - <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]]" - > - [[link.name]] - </a> - </template> - </div> - <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]]" - repo="[[repo]]" - on-added-section-removed="_handleAddedSectionRemoved" - ></gr-access-section> - </template> - <div class="referenceContainer"> - <gr-button id="addReferenceBtn" on-click="_handleCreateSection" - >Add Reference</gr-button - > - </div> - <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 - > - </div> - </div> - </div> -`; diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts index 37cc4882d4..caa2d13133 100644 --- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts +++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.ts @@ -40,12 +40,10 @@ import { AutocompleteCommitEvent, GrAutocomplete, } from '../../shared/gr-autocomplete/gr-autocomplete'; -import * as MockInteractions from '@polymer/iron-test-helpers/mock-interactions'; import {GrAccessSection} from '../gr-access-section/gr-access-section'; import {GrPermission} from '../gr-permission/gr-permission'; import {createChange} from '../../../test/test-data-generators'; - -const basicFixture = fixtureFromElement('gr-repo-access'); +import {fixture, html} from '@open-wc/testing-helpers'; suite('gr-repo-access tests', () => { let element: GrRepoAccess; @@ -128,19 +126,21 @@ suite('gr-repo-access tests', () => { }, }; setup(async () => { - element = basicFixture.instantiate(); + element = await fixture<GrRepoAccess>(html` + <gr-repo-access></gr-repo-access> + `); stubRestApi('getAccount').returns(Promise.resolve(undefined)); repoStub = stubRestApi('getRepo').returns(Promise.resolve(repoRes)); - element._loading = false; - element._ownerOf = []; - element._canUpload = false; - await flush(); + element.loading = false; + element.ownerOf = []; + element.canUpload = false; + await element.updateComplete; }); test('_repoChanged called when repo name changes', async () => { const repoChangedStub = sinon.stub(element, '_repoChanged'); element.repo = 'New Repo' as RepoName; - await flush(); + await element.updateComplete; assert.isTrue(repoChangedStub.called); }); @@ -160,13 +160,13 @@ suite('gr-repo-access tests', () => { assert.isTrue(accessStub.called); assert.isTrue(capabilitiesStub.called); assert.isTrue(repoStub.called); - assert.isNotOk(element._inheritsFrom); - assert.deepEqual(element._local, accessRes.local); + assert.isNotOk(element.inheritsFrom); + assert.deepEqual(element.local, accessRes.local); assert.deepEqual( - element._sections, + element.sections, toSortedPermissionsArray(accessRes.local) ); - assert.deepEqual(element._labels, repoRes.labels); + assert.deepEqual(element.labels, repoRes.labels); assert.equal( getComputedStyle(queryAndAssert<HTMLDivElement>(element, '.weblinks')) .display, @@ -175,7 +175,7 @@ suite('gr-repo-access tests', () => { await element._repoChanged('Another New Repo' as RepoName); assert.deepEqual( - element._sections, + element.sections, toSortedPermissionsArray(accessRes2.local) ); assert.equal( @@ -205,56 +205,66 @@ suite('gr-repo-access tests', () => { assert.isFalse(repoStub.called); }); - test('_computeParentHref', () => { - assert.equal( - element._computeParentHref('test-repo' as RepoName), - '/admin/repos/test-repo,access' - ); + test('computeParentHref', () => { + element.inheritsFrom!.name = 'test-repo' as RepoName; + assert.equal(element.computeParentHref(), '/admin/repos/test-repo,access'); }); - test('_computeMainClass', () => { - let ownerOf = ['refs/*'] as GitRef[]; - const editing = true; - const canUpload = false; - assert.equal(element._computeMainClass(ownerOf, canUpload, false), 'admin'); - assert.equal( - element._computeMainClass(ownerOf, canUpload, editing), - 'admin editing' - ); - ownerOf = []; - assert.equal(element._computeMainClass(ownerOf, canUpload, false), ''); - assert.equal( - element._computeMainClass(ownerOf, canUpload, editing), - 'editing' - ); + test('computeMainClass', () => { + element.ownerOf = ['refs/*'] as GitRef[]; + element.editing = false; + element.canUpload = false; + assert.equal(element.computeMainClass(), 'admin'); + element.editing = true; + assert.equal(element.computeMainClass(), 'admin editing'); + element.ownerOf = []; + element.editing = false; + assert.equal(element.computeMainClass(), ''); + element.editing = true; + assert.equal(element.computeMainClass(), 'editing'); }); test('inherit section', async () => { - element._local = {}; - element._ownerOf = []; - const computeParentHrefStub = sinon.stub(element, '_computeParentHref'); - await flush(); + element.local = {}; + element.ownerOf = []; + const computeParentHrefStub = sinon.stub(element, 'computeParentHref'); + await element.updateComplete; // 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(computeParentHrefStub.called); + assert.equal( + getComputedStyle( + queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom') + ).display, + 'none' + ); // When in edit mode, the autocomplete should appear. - element._editing = true; + element.editing = true; // When editing, the autocomplete should still not be shown. - assert.equal(getComputedStyle(element.$.inheritsFrom).display, 'none'); + assert.equal( + getComputedStyle( + queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom') + ).display, + 'none' + ); - element._editing = false; - element._inheritsFrom = { + element.editing = false; + element.inheritsFrom = { id: '1234' as UrlEncodedRepoName, name: 'another-repo' as RepoName, }; - await flush(); + await element.updateComplete; // When there is a parent project, the link should be displayed. - assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none'); assert.notEqual( - getComputedStyle(element.$.inheritFromName).display, + getComputedStyle( + queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom') + ).display, + 'none' + ); + assert.notEqual( + getComputedStyle( + queryAndAssert<HTMLAnchorElement>(element, '#inheritFromName') + ).display, 'none' ); assert.equal( @@ -264,10 +274,21 @@ suite('gr-repo-access tests', () => { 'none' ); assert.isTrue(computeParentHrefStub.called); - element._editing = true; + element.editing = true; + await element.updateComplete; // When editing, the autocomplete should be shown. - assert.notEqual(getComputedStyle(element.$.inheritsFrom).display, 'none'); - assert.equal(getComputedStyle(element.$.inheritFromName).display, 'none'); + assert.notEqual( + getComputedStyle( + queryAndAssert<HTMLHeadingElement>(element, '#inheritsFrom') + ).display, + 'none' + ); + assert.equal( + getComputedStyle( + queryAndAssert<HTMLAnchorElement>(element, '#inheritFromName') + ).display, + 'none' + ); assert.notEqual( getComputedStyle( queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput') @@ -276,20 +297,16 @@ suite('gr-repo-access tests', () => { ); }); - test('_handleUpdateInheritFrom', async () => { - element._inheritFromFilter = 'foo bar baz' as RepoName; - element._handleUpdateInheritFrom({ + test('handleUpdateInheritFrom', async () => { + element.inheritFromFilter = 'foo bar baz' as RepoName; + await element.updateComplete; + element.handleUpdateInheritFrom({ detail: {value: 'abc+123'}, } as CustomEvent); - await flush(); - assert.isOk(element._inheritsFrom); - assert.equal(element._inheritsFrom!.id, 'abc+123'); - assert.equal(element._inheritsFrom!.name, 'foo bar baz' as RepoName); - }); - - test('_computeLoadingClass', () => { - assert.equal(element._computeLoadingClass(true), 'loading'); - assert.equal(element._computeLoadingClass(false), ''); + await element.updateComplete; + assert.isOk(element.inheritsFrom); + assert.equal(element.inheritsFrom!.id, 'abc+123'); + assert.equal(element.inheritsFrom!.name, 'foo bar baz' as RepoName); }); test('fires page-error', async () => { @@ -341,10 +358,10 @@ suite('gr-repo-access tests', () => { ).display, 'none' ); - element._inheritsFrom = { + element.inheritsFrom = { id: 'test-project' as UrlEncodedRepoName, }; - await flush(); + await element.updateComplete; assert.equal( getComputedStyle( queryAndAssert<GrAutocomplete>(element, '#editInheritFromInput') @@ -352,8 +369,8 @@ suite('gr-repo-access tests', () => { 'none' ); - MockInteractions.tap(queryAndAssert<GrButton>(element, '#editBtn')); - await flush(); + queryAndAssert<GrButton>(element, '#editBtn').click(); + await element.updateComplete; // Edit button changes to Cancel button, and Save button is visible but // disabled. @@ -406,19 +423,19 @@ suite('gr-repo-access tests', () => { setup(async () => { // 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 = toSortedPermissionsArray(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)); - await flush(); + element.local = JSON.parse(JSON.stringify(accessRes.local)); + element.ownerOf = []; + element.sections = toSortedPermissionsArray(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)); + await element.updateComplete; }); test('removing an added section', async () => { - element._editing = true; - await flush(); - assert.equal(element._sections!.length, 1); + element.editing = true; + await element.updateComplete; + assert.equal(element.sections!.length, 1); queryAndAssert<GrAccessSection>( element, 'gr-access-section' @@ -428,31 +445,38 @@ suite('gr-repo-access tests', () => { bubbles: true, }) ); - await flush(); - assert.equal(element._sections!.length, 0); + await element.updateComplete; + 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', async () => { + assert.equal( + getComputedStyle(queryAndAssert<GrButton>(element, '#saveReviewBtn')) + .display, + 'none' + ); + assert.equal( + getComputedStyle(queryAndAssert<GrButton>(element, '#editBtn')).display, + 'none' + ); }); test('button visibility for non ref owner with upload privilege', async () => { - element._canUpload = true; - await flush(); + element.canUpload = true; + await element.updateComplete; testEditSaveCancelBtns(false, true); }); test('button visibility for ref owner', async () => { - element._ownerOf = ['refs/for/*'] as GitRef[]; - await flush(); + element.ownerOf = ['refs/for/*'] as GitRef[]; + await element.updateComplete; testEditSaveCancelBtns(true, false); }); test('button visibility for ref owner and upload', async () => { - element._ownerOf = ['refs/for/*'] as GitRef[]; - element._canUpload = true; - await flush(); + element.ownerOf = ['refs/for/*'] as GitRef[]; + element.canUpload = true; + await element.updateComplete; testEditSaveCancelBtns(true, false); }); @@ -467,15 +491,15 @@ suite('gr-repo-access tests', () => { bubbles: true, }) ); - await flush(); + await element.updateComplete; assert.isTrue(handleAccessModifiedSpy.called); }); test('_handleAccessModified called when parent changes', async () => { - element._inheritsFrom = { + element.inheritsFrom = { id: 'test-project' as UrlEncodedRepoName, }; - await flush(); + await element.updateComplete; queryAndAssert<GrAutocomplete>( element, '#editInheritFromInput' @@ -497,22 +521,22 @@ suite('gr-repo-access tests', () => { bubbles: true, }) ); - await flush(); + await element.updateComplete; assert.isTrue(handleAccessModifiedSpy.called); }); - test('_handleSaveForReview', async () => { + test('handleSaveForReview', async () => { const saveStub = stubRestApi('setRepoAccessRightsForReview'); - sinon.stub(element, '_computeAddAndRemove').returns({ + sinon.stub(element, 'computeAddAndRemove').returns({ add: {}, remove: {}, }); - element._handleSaveForReview(new Event('test')); - await flush(); + element.handleSaveForReview(new Event('test')); + await element.updateComplete; assert.isFalse(saveStub.called); }); - test('_recursivelyRemoveDeleted', () => { + test('recursivelyRemoveDeleted', () => { const obj = { 'refs/*': { permissions: { @@ -542,11 +566,11 @@ suite('gr-repo-access tests', () => { }, }, }; - element._recursivelyRemoveDeleted(obj); + element.recursivelyRemoveDeleted(obj); assert.deepEqual(obj, expectedResult); }); - test('_recursivelyUpdateAddRemoveObj on new added section', () => { + test('recursivelyUpdateAddRemoveObj on new added section', () => { const obj = { 'refs/for/*': { permissions: { @@ -608,46 +632,46 @@ suite('gr-repo-access tests', () => { remove: {}, }; const updateObj = {add: {}, remove: {}}; - element._recursivelyUpdateAddRemoveObj(obj, updateObj); + element.recursivelyUpdateAddRemoveObj(obj, updateObj); assert.deepEqual(updateObj, expectedResult); }); - test('_handleSaveForReview with no changes', () => { - assert.deepEqual(element._computeAddAndRemove(), {add: {}, remove: {}}); + test('handleSaveForReview with no changes', () => { + assert.deepEqual(element.computeAddAndRemove(), {add: {}, remove: {}}); }); - test('_handleSaveForReview parent change', async () => { - element._inheritsFrom = { + test('handleSaveForReview parent change', async () => { + element.inheritsFrom = { id: 'test-project' as UrlEncodedRepoName, }; element.originalInheritsFrom = { id: 'test-project-original' as UrlEncodedRepoName, }; - await flush(); - assert.deepEqual(element._computeAddAndRemove(), { + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), { parent: 'test-project', add: {}, remove: {}, }); }); - test('_handleSaveForReview new parent with spaces', async () => { - element._inheritsFrom = { + test('handleSaveForReview new parent with spaces', async () => { + element.inheritsFrom = { id: 'spaces+in+project+name' as UrlEncodedRepoName, }; element.originalInheritsFrom = {id: 'old-project' as UrlEncodedRepoName}; - await flush(); - assert.deepEqual(element._computeAddAndRemove(), { + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), { parent: 'spaces in project name', add: {}, remove: {}, }); }); - test('_handleSaveForReview rules', async () => { + test('handleSaveForReview rules', async () => { // Delete a rule. - element._local!['refs/*'].permissions.owner.rules[123].deleted = true; - await flush(); + element.local!['refs/*'].permissions.owner.rules[123].deleted = true; + await element.updateComplete; let expectedInput = { add: {}, remove: { @@ -662,14 +686,14 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Undo deleting a rule. - delete element._local!['refs/*'].permissions.owner.rules[123].deleted; + delete element.local!['refs/*'].permissions.owner.rules[123].deleted; // Modify a rule. - element._local!['refs/*'].permissions.owner.rules[123].modified = true; - await flush(); + element.local!['refs/*'].permissions.owner.rules[123].modified = true; + await element.updateComplete; expectedInput = { add: { 'refs/*': { @@ -694,10 +718,10 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); }); - test('_computeAddAndRemove permissions', async () => { + test('computeAddAndRemove permissions', async () => { // Add a new rule to a permission. let expectedInput = {}; @@ -722,22 +746,20 @@ suite('gr-repo-access tests', () => { element, 'gr-access-section' ); - queryAndAssert<GrPermission>( + await queryAndAssert<GrPermission>( grAccessSection, 'gr-permission' ).handleAddRuleItem({ detail: {value: 'Maintainers'}, } as AutocompleteCommitEvent); - - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Remove the added rule. - delete element._local!['refs/*'].permissions.owner.rules.Maintainers; + delete element.local!['refs/*'].permissions.owner.rules.Maintainers; // Delete a permission. - element._local!['refs/*'].permissions.owner.deleted = true; - await flush(); + element.local!['refs/*'].permissions.owner.deleted = true; + await element.updateComplete; expectedInput = { add: {}, @@ -749,14 +771,14 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Undo delete permission. - delete element._local!['refs/*'].permissions.owner.deleted; + delete element.local!['refs/*'].permissions.owner.deleted; // Modify a permission. - element._local!['refs/*'].permissions.owner.modified = true; - await flush(); + element.local!['refs/*'].permissions.owner.modified = true; + await element.updateComplete; expectedInput = { add: { 'refs/*': { @@ -779,10 +801,10 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); }); - test('_computeAddAndRemove sections', async () => { + test('computeAddAndRemove sections', async () => { // Add a new permission to a section let expectedInput = {}; @@ -803,9 +825,9 @@ suite('gr-repo-access tests', () => { queryAndAssert<GrAccessSection>( element, 'gr-access-section' - )._handleAddPermission(); - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + ).handleAddPermission(); + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Add a new rule to the new permission. expectedInput = { @@ -833,20 +855,19 @@ suite('gr-repo-access tests', () => { element, 'gr-access-section' ); - const newPermission = queryAll<GrPermission>( + await queryAll<GrPermission>( grAccessSection, 'gr-permission' - )[2]; - newPermission.handleAddRuleItem({ + )[2].handleAddRuleItem({ detail: {value: 'Maintainers'}, } as AutocompleteCommitEvent); - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Modify a section reference. - element._local!['refs/*'].updatedId = 'refs/for/bar'; - element._local!['refs/*'].modified = true; - await flush(); + element.local!['refs/*'].updatedId = 'refs/for/bar'; + element.local!['refs/*'].modified = true; + await element.updateComplete; + expectedInput = { add: { 'refs/for/bar': { @@ -885,12 +906,11 @@ suite('gr-repo-access tests', () => { }, }, }; - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Delete a section. - element._local!['refs/*'].deleted = true; - await flush(); + element.local!['refs/*'].deleted = true; + await element.updateComplete; expectedInput = { add: {}, remove: { @@ -899,10 +919,10 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); }); - test('_computeAddAndRemove new section', async () => { + test('computeAddAndRemove new section', async () => { // Add a new permission to a section let expectedInput = {}; @@ -915,9 +935,9 @@ suite('gr-repo-access tests', () => { }, remove: {}, }; - MockInteractions.tap(element.$.addReferenceBtn); - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + queryAndAssert<GrButton>(element, '#addReferenceBtn').click(); + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), expectedInput); expectedInput = { add: { @@ -938,9 +958,9 @@ suite('gr-repo-access tests', () => { element, 'gr-access-section' )[1]; - newSection._handleAddPermission(); - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + newSection.handleAddPermission(); + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Add rule to the new permission. expectedInput = { @@ -966,18 +986,17 @@ suite('gr-repo-access tests', () => { remove: {}, }; - queryAndAssert<GrPermission>( + await queryAndAssert<GrPermission>( newSection, 'gr-permission' ).handleAddRuleItem({ detail: {value: 'Maintainers'}, } as AutocompleteCommitEvent); - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Modify a the reference from the default value. - element._local!['refs/for/*'].updatedId = 'refs/for/new'; - await flush(); + element.local!['refs/for/*'].updatedId = 'refs/for/new'; + await element.updateComplete; expectedInput = { add: { 'refs/for/new': { @@ -1001,14 +1020,14 @@ suite('gr-repo-access tests', () => { }, remove: {}, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); }); - test('_computeAddAndRemove combinations', async () => { + test('computeAddAndRemove combinations', async () => { // 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; - await flush(); + element.local!['refs/*'].permissions.owner.rules[123].modified = true; + element.local!['refs/*'].permissions.owner.deleted = true; + await element.updateComplete; let expectedInput = {}; expectedInput = { @@ -1021,16 +1040,16 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + 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; - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + element.local!['refs/*'].permissions.owner.rules[123].modified = false; + element.local!['refs/*'].permissions.owner.rules[123].deleted = true; + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Also modify a different rule inside of another permission. - element._local!['refs/*'].permissions.read.modified = true; - await flush(); + element.local!['refs/*'].permissions.read.modified = true; + await element.updateComplete; expectedInput = { add: { 'refs/*': { @@ -1053,14 +1072,14 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + 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; - await flush(); + 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; + await element.updateComplete; expectedInput = { add: { 'refs/*': { @@ -1084,21 +1103,19 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Add a rule to the existing permission; const grAccessSection = queryAndAssert<GrAccessSection>( element, 'gr-access-section' ); - const readPermission = queryAll<GrPermission>( + await queryAll<GrPermission>( grAccessSection, 'gr-permission' - )[1]; - readPermission.handleAddRuleItem({ + )[1].handleAddRuleItem({ detail: {value: 'Maintainers'}, } as AutocompleteCommitEvent); - await flush(); expectedInput = { add: { @@ -1124,12 +1141,12 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Change one of the refs - element._local!['refs/*'].updatedId = 'refs/for/bar'; - element._local!['refs/*'].modified = true; - await flush(); + element.local!['refs/*'].updatedId = 'refs/for/bar'; + element.local!['refs/*'].modified = true; + await element.updateComplete; expectedInput = { add: { @@ -1154,7 +1171,7 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); expectedInput = { add: {}, @@ -1164,27 +1181,28 @@ suite('gr-repo-access tests', () => { }, }, }; - element._local!['refs/*'].deleted = true; - await flush(); - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + element.local!['refs/*'].deleted = true; + await element.updateComplete; + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Add a new section. - MockInteractions.tap(element.$.addReferenceBtn); + queryAndAssert<GrButton>(element, '#addReferenceBtn').click(); + await element.updateComplete; let newSection = queryAll<GrAccessSection>( element, 'gr-access-section' )[1]; - newSection._handleAddPermission(); - await flush(); - queryAndAssert<GrPermission>( + newSection.handleAddPermission(); + await element.updateComplete; + await queryAndAssert<GrPermission>( newSection, 'gr-permission' ).handleAddRuleItem({ detail: {value: 'Maintainers'}, } as AutocompleteCommitEvent); // Modify a the reference from the default value. - element._local!['refs/for/*'].updatedId = 'refs/for/new'; - await flush(); + element.local!['refs/for/*'].updatedId = 'refs/for/new'; + await element.updateComplete; expectedInput = { add: { @@ -1213,13 +1231,13 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Modify newly added rule inside new ref. - element._local!['refs/for/*'].permissions['label-Code-Review'].rules[ + element.local!['refs/for/*'].permissions['label-Code-Review'].rules[ 'Maintainers' ].modified = true; - await flush(); + await element.updateComplete; expectedInput = { add: { 'refs/for/new': { @@ -1248,23 +1266,23 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); // Add a second new section. - MockInteractions.tap(element.$.addReferenceBtn); - await flush(); + queryAndAssert<GrButton>(element, '#addReferenceBtn').click(); + await element.updateComplete; newSection = queryAll<GrAccessSection>(element, 'gr-access-section')[2]; - newSection._handleAddPermission(); - await flush(); - queryAndAssert<GrPermission>( + newSection.handleAddPermission(); + await element.updateComplete; + await queryAndAssert<GrPermission>( newSection, 'gr-permission' ).handleAddRuleItem({ detail: {value: 'Maintainers'}, } as AutocompleteCommitEvent); // Modify a the reference from the default value. - element._local!['refs/for/**'].updatedId = 'refs/for/new2'; - await flush(); + element.local!['refs/for/**'].updatedId = 'refs/for/new2'; + await element.updateComplete; expectedInput = { add: { 'refs/for/new': { @@ -1311,26 +1329,26 @@ suite('gr-repo-access tests', () => { }, }, }; - assert.deepEqual(element._computeAddAndRemove(), expectedInput); + assert.deepEqual(element.computeAddAndRemove(), expectedInput); }); test('Unsaved added refs are discarded when edit cancelled', async () => { // Unsaved changes are discarded when editing is cancelled. - MockInteractions.tap(element.$.editBtn); - await flush(); - assert.equal(element._sections!.length, 1); - assert.equal(Object.keys(element._local!).length, 1); - MockInteractions.tap(element.$.addReferenceBtn); - await flush(); - assert.equal(element._sections!.length, 2); - assert.equal(Object.keys(element._local!).length, 2); - MockInteractions.tap(element.$.editBtn); - await flush(); - assert.equal(element._sections!.length, 1); - assert.equal(Object.keys(element._local!).length, 1); + queryAndAssert<GrButton>(element, '#editBtn').click(); + await element.updateComplete; + assert.equal(element.sections!.length, 1); + assert.equal(Object.keys(element.local!).length, 1); + queryAndAssert<GrButton>(element, '#addReferenceBtn').click(); + await element.updateComplete; + assert.equal(element.sections!.length, 2); + assert.equal(Object.keys(element.local!).length, 2); + queryAndAssert<GrButton>(element, '#editBtn').click(); + await element.updateComplete; + assert.equal(element.sections!.length, 1); + assert.equal(Object.keys(element.local!).length, 1); }); - test('_handleSave', async () => { + test('handleSave', async () => { const repoAccessInput = { add: { 'refs/*': { @@ -1365,19 +1383,22 @@ suite('gr-repo-access tests', () => { ); element.repo = 'test-repo' as RepoName; - sinon.stub(element, '_computeAddAndRemove').returns(repoAccessInput); + sinon.stub(element, 'computeAddAndRemove').returns(repoAccessInput); - element._modified = true; - MockInteractions.tap(element.$.saveBtn); - await flush(); - assert.equal(element.$.saveBtn.hasAttribute('loading'), true); + element.modified = true; + queryAndAssert<GrButton>(element, '#saveBtn').click(); + await element.updateComplete; + assert.equal( + queryAndAssert<GrButton>(element, '#saveBtn').hasAttribute('loading'), + true + ); resolver!({status: 200} as Response); - await flush(); + await element.updateComplete; assert.isTrue(saveStub.called); assert.isTrue(navigateToChangeStub.notCalled); }); - test('_handleSaveForReview', async () => { + test('handleSaveForReview', async () => { const repoAccessInput = { add: { 'refs/*': { @@ -1412,14 +1433,19 @@ suite('gr-repo-access tests', () => { ).returns(new Promise(r => (resolver = r))); element.repo = 'test-repo' as RepoName; - sinon.stub(element, '_computeAddAndRemove').returns(repoAccessInput); + sinon.stub(element, 'computeAddAndRemove').returns(repoAccessInput); - element._modified = true; - MockInteractions.tap(element.$.saveReviewBtn); - await flush(); - assert.equal(element.$.saveReviewBtn.hasAttribute('loading'), true); + element.modified = true; + queryAndAssert<GrButton>(element, '#saveReviewBtn').click(); + await element.updateComplete; + assert.equal( + queryAndAssert<GrButton>(element, '#saveReviewBtn').hasAttribute( + 'loading' + ), + true + ); resolver!(createChange()); - await flush(); + await element.updateComplete; assert.isTrue(saveForReviewStub.called); assert.isTrue( navigateToChangeStub.lastCall.calledWithExactly(createChange()) |