Migrate gr-access-section to lit

Release-Notes: skip
Change-Id: Ia8a19a224e22c3c9fe01c1f6099b2aef28fc845d
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 be6f56e..8fa2e90 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 @@
   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 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 @@
   @property({type: Object})
   capabilities?: CapabilityInfoMap;
 
-  @property({type: Object, notify: true, observer: '_updateSection'})
+  @property({type: Object})
   section?: PermissionAccessSection;
 
   @property({type: Object})
@@ -98,7 +85,7 @@
   @property({type: Object})
   labels?: LabelNameToLabelTypeInfoMap;
 
-  @property({type: Boolean, observer: '_handleEditingChanged'})
+  @property({type: Boolean})
   editing = false;
 
   @property({type: Boolean})
@@ -107,43 +94,220 @@
   @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());
   }
 
-  _updateSection(section: PermissionAccessSection) {
-    this._permissions = toSortedPermissionsArray(section.value.permissions);
-    this._originalId = section.id;
+  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);
+        }
+      `,
+    ];
   }
 
-  _handleAccessSaved() {
-    if (!this.section) {
-      return;
+  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>
+    `;
+  }
+
+  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 @@
       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 @@
     }
   }
 
-  _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 @@
     );
   }
 
-  _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 @@
     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 @@
     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 @@
     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 @@
     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 @@
     }
     // 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 65a3199..0000000
--- 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 88961ed..f4d81c4 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 @@
 } 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 @@
           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 @@
           },
         },
       ];
-      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 @@
         },
       ];
 
-      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 @@
         },
       ];
 
+      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 @@
         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 @@
       };
 
       assert.equal(
-        element._computePermissionName(name, permission, element.capabilities),
+        element.computePermissionName(permission),
         'Label Code-Review'
       );
 
@@ -231,136 +258,63 @@
       };
 
       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);
+      element.editingRef = false;
+      element.section!.id = 'GLOBAL_CAPABILITIES' as GitRef;
+      assert.equal(element.computeSectionName(), 'Global Capabilities');
+      assert.isFalse(element.editingRef);
 
-      name = 'refs/for/*';
-      assert.equal(element._computeSectionName(name), 'Reference: refs/for/*');
-      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('Global section', () => {
-      setup(() => {
+      setup(async () => {
         element.section = {
           id: 'GLOBAL_CAPABILITIES' as GitRef,
           value: {
@@ -409,23 +363,41 @@
             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 @@
           },
         };
         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 @@
             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 @@
           },
         };
 
-        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 @@
 
         // 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 @@
           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 81d53f2..fba349d 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 @@
   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 @@
 
       // Restore exclusive bit to original.
       this.permission.value.exclusive = this.originalExclusiveValue;
+      fire(this, 'permission-changed', {value: this.permission});
       this.requestUpdate();
     }
   }
@@ -578,17 +579,18 @@
   }
 
   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 f8b36c4..51b1e05 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
@@ -19,7 +19,6 @@
 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';
@@ -470,7 +469,6 @@
     const section = {permissions: {}, added: true};
     this.push('_sections', {id: newRef, value: section});
     this.set(['_local', newRef], section);
-    flush();
     // Template already instantiated at this point
     (
       this.root!.querySelector(
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 37cc488..fea5056 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
@@ -803,7 +803,7 @@
       queryAndAssert<GrAccessSection>(
         element,
         'gr-access-section'
-      )._handleAddPermission();
+      ).handleAddPermission();
       await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
@@ -938,7 +938,7 @@
         element,
         'gr-access-section'
       )[1];
-      newSection._handleAddPermission();
+      newSection.handleAddPermission();
       await flush();
       assert.deepEqual(element._computeAddAndRemove(), expectedInput);
 
@@ -1170,11 +1170,12 @@
 
       // Add a new section.
       MockInteractions.tap(element.$.addReferenceBtn);
+      await flush();
       let newSection = queryAll<GrAccessSection>(
         element,
         'gr-access-section'
       )[1];
-      newSection._handleAddPermission();
+      newSection.handleAddPermission();
       await flush();
       queryAndAssert<GrPermission>(
         newSection,
@@ -1254,7 +1255,7 @@
       MockInteractions.tap(element.$.addReferenceBtn);
       await flush();
       newSection = queryAll<GrAccessSection>(element, 'gr-access-section')[2];
-      newSection._handleAddPermission();
+      newSection.handleAddPermission();
       await flush();
       queryAndAssert<GrPermission>(
         newSection,