Convert gr-group to lit

Change-Id: I5e79bba6190d01a30d791e08cb9b1125bc6471a3
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index cf0fdd4..6bd1ac5 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -505,7 +505,7 @@
     suite('groups', () => {
       let getGroupConfigStub;
       setup(() => {
-        stub('gr-group', '_loadGroup').callsFake(() => Promise.resolve({}));
+        stub('gr-group', 'loadGroup').callsFake(() => Promise.resolve({}));
         stub('gr-group-members', '_loadGroupDetails').callsFake(() => {});
 
         getGroupConfigStub = stubRestApi('getGroupConfig');
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
index 9fde66c..d9bd304 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group.ts
@@ -16,35 +16,26 @@
  */
 
 import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
-import '../../../styles/gr-font-styles';
-import '../../../styles/gr-form-styles';
-import '../../../styles/gr-subpage-styles';
-import '../../../styles/shared-styles';
 import '../../shared/gr-autocomplete/gr-autocomplete';
 import '../../shared/gr-button/gr-button';
 import '../../shared/gr-copy-clipboard/gr-copy-clipboard';
 import '../../shared/gr-select/gr-select';
-import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
-import {GrButton} from '../../shared/gr-button/gr-button';
-import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
-import {GrSelect} from '../../shared/gr-select/gr-select';
-import {PolymerElement} from '@polymer/polymer/polymer-element';
-import {htmlTemplate} from './gr-group_html';
-import {customElement, property, observe} from '@polymer/decorators';
 import {
   AutocompleteSuggestion,
   AutocompleteQuery,
 } from '../../shared/gr-autocomplete/gr-autocomplete';
 import {GroupId, GroupInfo, GroupName} from '../../../types/common';
-import {
-  fireEvent,
-  firePageError,
-  fireTitleChange,
-} from '../../../utils/event-util';
+import {firePageError, fireTitleChange} from '../../../utils/event-util';
 import {appContext} from '../../../services/app-context';
 import {ErrorCallback} from '../../../api/rest';
 import {convertToString} from '../../../utils/string-util';
 import {BindValueChangeEvent} from '../../../types/events';
+import {fontStyles} from '../../../styles/gr-font-styles';
+import {formStyles} from '../../../styles/gr-form-styles';
+import {sharedStyles} from '../../../styles/shared-styles';
+import {subpageStyles} from '../../../styles/gr-subpage-styles';
+import {LitElement, css, html} from 'lit';
+import {customElement, property, state} from 'lit/decorators';
 
 const INTERNAL_GROUP_REGEX = /^[\da-f]{40}$/;
 
@@ -59,22 +50,6 @@
   },
 };
 
-export interface GrGroup {
-  $: {
-    loading: HTMLDivElement;
-    loadedContent: HTMLDivElement;
-    visibleToAll: GrSelect;
-    inputUpdateNameBtn: GrButton;
-    Title: HTMLHeadingElement;
-    groupNameInput: GrAutocomplete;
-    groupName: HTMLHeadingElement;
-    groupOwnerInput: GrAutocomplete;
-    groupOwner: HTMLHeadingElement;
-    inputUpdateOwnerBtn: GrButton;
-    uuid: GrCopyClipboard;
-  };
-}
-
 export interface GroupNameChangedDetail {
   name: GroupName;
   external: boolean;
@@ -91,72 +66,249 @@
 }
 
 @customElement('gr-group')
-export class GrGroup extends PolymerElement {
-  static get template() {
-    return htmlTemplate;
-  }
-
+export class GrGroup extends LitElement {
   /**
    * Fired when the group name changes.
    *
    * @event name-changed
    */
 
+  private readonly query: AutocompleteQuery;
+
   @property({type: String})
   groupId?: GroupId;
 
-  @property({type: Boolean})
-  _rename = false;
+  @state() private originalOwnerName?: string;
 
-  @property({type: Boolean})
-  _groupIsInternal = false;
+  @state() private originalDescriptionName?: string;
 
-  @property({type: Boolean})
-  _description = false;
+  @state() private originalOptionsVisibleToAll?: boolean;
 
-  @property({type: Boolean})
-  _owner = false;
+  @state() private submitTypes = Object.values(OPTIONS);
 
-  @property({type: Boolean})
-  _options = false;
+  /* private but used in test */
+  @state() isAdmin = false;
 
-  @property({type: Boolean})
-  _loading = true;
+  /* private but used in test */
+  @state() groupOwner = false;
 
-  @property({type: Object})
-  _groupConfig?: GroupInfo;
+  /* private but used in test */
+  @state() groupIsInternal = false;
 
-  @property({type: String})
-  _groupConfigOwner?: string;
+  /* private but used in test */
+  @state() loading = true;
 
-  @property({type: Object})
-  _groupName?: GroupName;
+  /* private but used in test */
+  @state() groupConfig?: GroupInfo;
 
-  @property({type: Boolean})
-  _groupOwner = false;
+  /* private but used in test */
+  @state() groupConfigOwner?: string;
 
-  @property({type: Array})
-  _submitTypes = Object.values(OPTIONS);
-
-  @property({type: Object})
-  _query: AutocompleteQuery;
-
-  @property({type: Boolean})
-  _isAdmin = false;
+  /* private but used in test */
+  @state() originalName?: GroupName;
 
   private readonly restApiService = appContext.restApiService;
 
   constructor() {
     super();
-    this._query = (input: string) => this._getGroupSuggestions(input);
+    this.query = (input: string) => this.getGroupSuggestions(input);
   }
 
   override connectedCallback() {
     super.connectedCallback();
-    this._loadGroup();
+    this.loadGroup();
   }
 
-  _loadGroup() {
+  static override get styles() {
+    return [
+      fontStyles,
+      formStyles,
+      sharedStyles,
+      subpageStyles,
+      css`
+        h3.edited:after {
+          color: var(--deemphasized-text-color);
+          content: ' *';
+        }
+      `,
+    ];
+  }
+
+  override render() {
+    return html`
+      <div class="main gr-form-styles read-only">
+        <div id="loading" class="${this.computeLoadingClass()}">Loading...</div>
+        <div id="loadedContent" class="${this.computeLoadingClass()}">
+          <h1 id="Title" class="heading-1">
+            ${convertToString(this.originalName)}
+          </h1>
+          <h2 id="configurations" class="heading-2">General</h2>
+          <div id="form">
+            <fieldset>
+              ${this.renderGroupUUID()} ${this.renderGroupName()}
+              ${this.renderGroupOwner()} ${this.renderGroupDescription()}
+              ${this.renderGroupOptions()}
+            </fieldset>
+          </div>
+        </div>
+      </div>
+    `;
+  }
+
+  private renderGroupUUID() {
+    return html`
+      <h3 id="groupUUID" class="heading-3">Group UUID</h3>
+      <fieldset>
+        <gr-copy-clipboard
+          id="uuid"
+          .text=${this.getGroupUUID()}
+        ></gr-copy-clipboard>
+      </fieldset>
+    `;
+  }
+
+  private renderGroupName() {
+    const groupNameEdited = this.originalName !== this.groupConfig?.name;
+    return html`
+      <h3
+        id="groupName"
+        class="heading-3 ${this.computeHeaderClass(groupNameEdited)}"
+      >
+        Group Name
+      </h3>
+      <fieldset>
+        <span class="value">
+          <gr-autocomplete
+            id="groupNameInput"
+            .text=${convertToString(this.groupConfig?.name)}
+            ?disabled=${this.computeGroupDisabled()}
+            @text-changed=${this.handleNameTextChanged}
+          ></gr-autocomplete>
+        </span>
+        <span class="value" ?disabled=${this.computeGroupDisabled()}>
+          <gr-button
+            id="inputUpdateNameBtn"
+            ?disabled=${!groupNameEdited}
+            @click=${this.handleSaveName}
+          >
+            Rename Group</gr-button
+          >
+        </span>
+      </fieldset>
+    `;
+  }
+
+  private renderGroupOwner() {
+    const groupOwnerNameEdited =
+      this.originalOwnerName !== this.groupConfig?.owner;
+    return html`
+      <h3
+        id="groupOwner"
+        class="heading-3 ${this.computeHeaderClass(groupOwnerNameEdited)}"
+      >
+        Owners
+      </h3>
+      <fieldset>
+        <span class="value">
+          <gr-autocomplete
+            id="groupOwnerInput"
+            .text=${convertToString(this.groupConfig?.owner)}
+            .value=${convertToString(this.groupConfigOwner)}
+            .query=${this.query}
+            ?disabled=${this.computeGroupDisabled()}
+            @text-changed=${this.handleOwnerTextChanged}
+            @value-changed=${this.handleOwnerValueChanged}
+          >
+          </gr-autocomplete>
+        </span>
+        <span class="value" ?disabled=${this.computeGroupDisabled()}>
+          <gr-button
+            id="inputUpdateOwnerBtn"
+            ?disabled=${!groupOwnerNameEdited}
+            @click=${this.handleSaveOwner}
+          >
+            Change Owners</gr-button
+          >
+        </span>
+      </fieldset>
+    `;
+  }
+
+  private renderGroupDescription() {
+    const groupDescriptionEdited =
+      this.originalDescriptionName !== this.groupConfig?.description;
+    return html`
+      <h3 class="heading-3 ${this.computeHeaderClass(groupDescriptionEdited)}">
+        Description
+      </h3>
+      <fieldset>
+        <div>
+          <iron-autogrow-textarea
+            class="description"
+            autocomplete="on"
+            ?disabled=${this.computeGroupDisabled()}
+            .bindValue=${convertToString(this.groupConfig?.description)}
+            @bind-value-changed=${this.handleDescriptionBindValueChanged}
+          ></iron-autogrow-textarea>
+        </div>
+        <span class="value" ?disabled=${this.computeGroupDisabled()}>
+          <gr-button
+            ?disabled=${!groupDescriptionEdited}
+            @click=${this.handleSaveDescription}
+          >
+            Save Description
+          </gr-button>
+        </span>
+      </fieldset>
+    `;
+  }
+
+  private renderGroupOptions() {
+    const groupOptionsEdited =
+      this.originalOptionsVisibleToAll !==
+      this.groupConfig?.options?.visible_to_all;
+    return html`
+      <h3
+        id="options"
+        class="heading-3 ${this.computeHeaderClass(groupOptionsEdited)}"
+      >
+        Group Options
+      </h3>
+      <fieldset>
+        <section>
+          <span class="title">
+            Make group visible to all registered users
+          </span>
+          <span class="value">
+            <gr-select
+              id="visibleToAll"
+              .bindValue="${this.groupConfig?.options?.visible_to_all}"
+              @bind-value-changed=${this.handleOptionsBindValueChanged}
+            >
+              <select ?disabled=${this.computeGroupDisabled()}>
+                ${this.submitTypes.map(
+                  item => html`
+                    <option value=${item.value}>${item.label}</option>
+                  `
+                )}
+              </select>
+            </gr-select>
+          </span>
+        </section>
+        <span class="value" ?disabled=${this.computeGroupDisabled()}>
+          <gr-button
+            ?disabled=${!groupOptionsEdited}
+            @click=${this.handleSaveOptions}
+          >
+            Save Group Options
+          </gr-button>
+        </span>
+      </fieldset>
+    `;
+  }
+
+  /* private but used in test */
+  async loadGroup() {
     if (!this.groupId) {
       return;
     }
@@ -167,154 +319,127 @@
       firePageError(response);
     };
 
-    return this.restApiService
-      .getGroupConfig(this.groupId, errFn)
-      .then(config => {
-        if (!config || !config.name) {
-          return Promise.resolve();
-        }
+    const config = await this.restApiService.getGroupConfig(
+      this.groupId,
+      errFn
+    );
+    if (!config || !config.name) return;
 
-        this._groupName = config.name;
-        this._groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
+    if (config.description === undefined) {
+      config.description = '';
+    }
 
-        promises.push(
-          this.restApiService.getIsAdmin().then(isAdmin => {
-            this._isAdmin = !!isAdmin;
-          })
-        );
+    this.originalName = config.name;
+    this.originalOwnerName = config.owner;
+    this.originalDescriptionName = config.description;
+    this.groupIsInternal = !!config.id.match(INTERNAL_GROUP_REGEX);
 
-        promises.push(
-          this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
-            this._groupOwner = !!isOwner;
-          })
-        );
+    promises.push(
+      this.restApiService.getIsAdmin().then(isAdmin => {
+        this.isAdmin = !!isAdmin;
+      })
+    );
 
-        // If visible to all is undefined, set to false. If it is defined
-        // as false, setting to false is fine. If any optional values
-        // are added with a default of true, then this would need to be an
-        // undefined check and not a truthy/falsy check.
-        if (config.options && !config.options.visible_to_all) {
-          config.options.visible_to_all = false;
-        }
-        this._groupConfig = config;
+    promises.push(
+      this.restApiService.getIsGroupOwner(config.name).then(isOwner => {
+        this.groupOwner = !!isOwner;
+      })
+    );
 
-        fireTitleChange(this, config.name);
+    // If visible to all is undefined, set to false. If it is defined
+    // as false, setting to false is fine. If any optional values
+    // are added with a default of true, then this would need to be an
+    // undefined check and not a truthy/falsy check.
+    if (config.options && !config.options.visible_to_all) {
+      config.options.visible_to_all = false;
+    }
+    this.groupConfig = config;
+    this.originalOptionsVisibleToAll = config?.options?.visible_to_all;
 
-        return Promise.all(promises).then(() => {
-          this._loading = false;
-        });
-      });
+    fireTitleChange(this, config.name);
+
+    await Promise.all(promises);
+    this.loading = false;
   }
 
-  _computeLoadingClass(loading: boolean) {
-    return loading ? 'loading' : '';
+  /* private but used in test */
+  computeLoadingClass() {
+    return this.loading ? 'loading' : '';
   }
 
-  _isLoading() {
-    return this._loading || this._loading === undefined;
-  }
-
-  _handleSaveName() {
-    const groupConfig = this._groupConfig;
+  /* private but used in test */
+  async handleSaveName() {
+    const groupConfig = this.groupConfig;
     if (!this.groupId || !groupConfig || !groupConfig.name) {
       return Promise.reject(new Error('invalid groupId or config name'));
     }
     const groupName = groupConfig.name;
-    return this.restApiService
-      .saveGroupName(this.groupId, groupName)
-      .then(config => {
-        if (config.status === 200) {
-          this._groupName = groupName;
-          const detail: GroupNameChangedDetail = {
-            name: groupName,
-            external: !this._groupIsInternal,
-          };
-          fireEvent(this, 'name-changed');
-          this.dispatchEvent(
-            new CustomEvent('name-changed', {
-              detail,
-              composed: true,
-              bubbles: true,
-            })
-          );
-          this._rename = false;
-        }
-      });
+    const config = await this.restApiService.saveGroupName(
+      this.groupId,
+      groupName
+    );
+    if (config.status === 200) {
+      this.originalName = groupName;
+      const detail: GroupNameChangedDetail = {
+        name: groupName,
+        external: !this.groupIsInternal,
+      };
+      this.dispatchEvent(
+        new CustomEvent('name-changed', {
+          detail,
+          composed: true,
+          bubbles: true,
+        })
+      );
+      this.requestUpdate();
+    }
+
+    return;
   }
 
-  _handleSaveOwner() {
-    if (!this.groupId || !this._groupConfig) return;
-    let owner = this._groupConfig.owner;
-    if (this._groupConfigOwner) {
-      owner = decodeURIComponent(this._groupConfigOwner);
+  /* private but used in test */
+  async handleSaveOwner() {
+    if (!this.groupId || !this.groupConfig) return;
+    let owner = this.groupConfig.owner;
+    if (this.groupConfigOwner) {
+      owner = decodeURIComponent(this.groupConfigOwner);
     }
     if (!owner) return;
-    return this.restApiService.saveGroupOwner(this.groupId, owner).then(() => {
-      this._owner = false;
-    });
+    await this.restApiService.saveGroupOwner(this.groupId, owner);
+    this.originalOwnerName = this.groupConfig?.owner;
+    this.groupConfigOwner = undefined;
   }
 
-  _handleSaveDescription() {
-    if (!this.groupId || !this._groupConfig || !this._groupConfig.description)
+  /* private but used in test */
+  async handleSaveDescription() {
+    if (
+      !this.groupId ||
+      !this.groupConfig ||
+      this.groupConfig.description === undefined
+    )
       return;
-    return this.restApiService
-      .saveGroupDescription(this.groupId, this._groupConfig.description)
-      .then(() => {
-        this._description = false;
-      });
+    await this.restApiService.saveGroupDescription(
+      this.groupId,
+      this.groupConfig.description
+    );
+    this.originalDescriptionName = this.groupConfig.description;
   }
 
-  _handleSaveOptions() {
-    if (!this.groupId || !this._groupConfig || !this._groupConfig.options)
-      return;
-    const visible = this._groupConfig.options.visible_to_all;
-
+  /* private but used in test */
+  async handleSaveOptions() {
+    if (!this.groupId || !this.groupConfig || !this.groupConfig.options) return;
+    const visible = this.groupConfig.options.visible_to_all;
     const options = {visible_to_all: visible};
-
-    return this.restApiService
-      .saveGroupOptions(this.groupId, options)
-      .then(() => {
-        this._options = false;
-      });
+    await this.restApiService.saveGroupOptions(this.groupId, options);
+    this.originalOptionsVisibleToAll =
+      this.groupConfig?.options?.visible_to_all;
   }
 
-  @observe('_groupConfig.name')
-  _handleConfigName() {
-    if (this._isLoading()) {
-      return;
-    }
-    this._rename = true;
-  }
-
-  @observe('_groupConfig.owner', '_groupConfigOwner')
-  _handleConfigOwner() {
-    if (this._isLoading()) {
-      return;
-    }
-    this._owner = true;
-  }
-
-  @observe('_groupConfig.description')
-  _handleConfigDescription() {
-    if (this._isLoading()) {
-      return;
-    }
-    this._description = true;
-  }
-
-  @observe('_groupConfig.options.visible_to_all')
-  _handleConfigOptions() {
-    if (this._isLoading()) {
-      return;
-    }
-    this._options = true;
-  }
-
-  _computeHeaderClass(configChanged: boolean) {
+  private computeHeaderClass(configChanged: boolean) {
     return configChanged ? 'edited' : '';
   }
 
-  _getGroupSuggestions(input: string) {
+  private getGroupSuggestions(input: string) {
     return this.restApiService.getSuggestedGroups(input).then(response => {
       const groups: AutocompleteSuggestion[] = [];
       for (const [name, group] of Object.entries(response ?? {})) {
@@ -324,37 +449,45 @@
     });
   }
 
-  _computeGroupDisabled(
-    owner: boolean,
-    admin: boolean,
-    groupIsInternal: boolean
-  ) {
-    return !(groupIsInternal && (admin || owner));
+  /* private but used in test */
+  computeGroupDisabled() {
+    return !(this.groupIsInternal && (this.isAdmin || this.groupOwner));
   }
 
-  _getGroupUUID(id: GroupId) {
+  private getGroupUUID() {
+    const id = this.groupConfig?.id;
     if (!id) return;
-
     return id.match(INTERNAL_GROUP_REGEX) ? id : decodeURIComponent(id);
   }
 
-  handleNameTextChanged(e: CustomEvent) {
-    this.set('_groupConfig.name', e.detail.value as GroupName);
+  private handleNameTextChanged(e: CustomEvent) {
+    if (!this.groupConfig || this.loading) return;
+    this.groupConfig.name = e.detail.value as GroupName;
+    this.requestUpdate();
   }
 
-  handleOwnerTextChanged(e: CustomEvent) {
-    this.set('_groupConfig.owner', e.detail.value);
+  private handleOwnerTextChanged(e: CustomEvent) {
+    if (!this.groupConfig || this.loading) return;
+    this.groupConfig.owner = e.detail.value;
+    this.requestUpdate();
   }
 
-  handleOwnerValueChanged(e: CustomEvent) {
-    this._groupConfigOwner = e.detail.value;
+  private handleOwnerValueChanged(e: CustomEvent) {
+    if (this.loading) return;
+    this.groupConfigOwner = e.detail.value;
+    this.requestUpdate();
   }
 
-  handleDescriptionBindValueChanged(e: BindValueChangeEvent) {
-    this.set('_groupConfig.description', e.detail.value);
+  private handleDescriptionBindValueChanged(e: BindValueChangeEvent) {
+    if (!this.groupConfig || this.loading) return;
+    this.groupConfig.description = e.detail.value;
+    this.requestUpdate();
   }
 
-  convertToString(value?: unknown) {
-    return convertToString(value);
+  private handleOptionsBindValueChanged(e: BindValueChangeEvent) {
+    if (!this.groupConfig || !this.groupConfig.options || this.loading) return;
+    this.groupConfig.options.visible_to_all = e.detail
+      .value as unknown as boolean;
+    this.requestUpdate();
   }
 }
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
deleted file mode 100644
index 6f27dfd..0000000
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_html.ts
+++ /dev/null
@@ -1,174 +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">
-    h3.edited:after {
-      color: var(--deemphasized-text-color);
-      content: ' *';
-    }
-  </style>
-  <style include="gr-form-styles">
-    /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */
-  </style>
-  <div class="main gr-form-styles read-only">
-    <div id="loading" class$="[[_computeLoadingClass(_loading)]]">
-      Loading...
-    </div>
-    <div id="loadedContent" class$="[[_computeLoadingClass(_loading)]]">
-      <h1 id="Title" class="heading-1">[[convertToString(_groupName)]]</h1>
-      <h2 id="configurations" class="heading-2">General</h2>
-      <div id="form">
-        <fieldset>
-          <h3 id="groupUUID" class="heading-3">Group UUID</h3>
-          <fieldset>
-            <gr-copy-clipboard
-              id="uuid"
-              text="[[_getGroupUUID(_groupConfig.id)]]"
-            ></gr-copy-clipboard>
-          </fieldset>
-          <h3
-            id="groupName"
-            class$="heading-3 [[_computeHeaderClass(_rename)]]"
-          >
-            Group Name
-          </h3>
-          <fieldset>
-            <span class="value">
-              <gr-autocomplete
-                id="groupNameInput"
-                text="[[convertToString(_groupConfig.name)]]"
-                disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-                on-text-changed="handleNameTextChanged"
-              ></gr-autocomplete>
-            </span>
-            <span
-              class="value"
-              disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-            >
-              <gr-button
-                id="inputUpdateNameBtn"
-                on-click="_handleSaveName"
-                disabled="[[!_rename]]"
-              >
-                Rename Group</gr-button
-              >
-            </span>
-          </fieldset>
-          <h3
-            id="groupOwner"
-            class$="heading-3 [[_computeHeaderClass(_owner)]]"
-          >
-            Owners
-          </h3>
-          <fieldset>
-            <span class="value">
-              <gr-autocomplete
-                id="groupOwnerInput"
-                text="[[convertToString(_groupConfig.owner)]]"
-                value="[[convertToString(_groupConfigOwner)]]"
-                query="[[_query]]"
-                disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-                on-text-changed="handleOwnerTextChanged"
-                on-value-changed="handleOwnerValueChanged"
-              >
-              </gr-autocomplete>
-            </span>
-            <span
-              class="value"
-              disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-            >
-              <gr-button
-                id="inputUpdateOwnerBtn"
-                on-click="_handleSaveOwner"
-                disabled="[[!_owner]]"
-              >
-                Change Owners</gr-button
-              >
-            </span>
-          </fieldset>
-          <h3 class$="heading-3 [[_computeHeaderClass(_description)]]">
-            Description
-          </h3>
-          <fieldset>
-            <div>
-              <iron-autogrow-textarea
-                class="description"
-                autocomplete="on"
-                bind-value="[[convertToString(_groupConfig.description)]]"
-                disabled="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-                on-bind-value-changed="handleDescriptionBindValueChanged"
-              ></iron-autogrow-textarea>
-            </div>
-            <span
-              class="value"
-              disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-            >
-              <gr-button
-                on-click="_handleSaveDescription"
-                disabled="[[!_description]]"
-              >
-                Save Description
-              </gr-button>
-            </span>
-          </fieldset>
-          <h3 id="options" class$="heading-3 [[_computeHeaderClass(_options)]]">
-            Group Options
-          </h3>
-          <fieldset>
-            <section>
-              <span class="title">
-                Make group visible to all registered users
-              </span>
-              <span class="value">
-                <gr-select
-                  id="visibleToAll"
-                  bind-value="{{_groupConfig.options.visible_to_all}}"
-                >
-                  <select
-                    disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-                  >
-                    <template is="dom-repeat" items="[[_submitTypes]]">
-                      <option value="[[convertToString(item.value)]]">
-                        [[item.label]]
-                      </option>
-                    </template>
-                  </select>
-                </gr-select>
-              </span>
-            </section>
-            <span
-              class="value"
-              disabled$="[[_computeGroupDisabled(_groupOwner, _isAdmin, _groupIsInternal)]]"
-            >
-              <gr-button on-click="_handleSaveOptions" disabled="[[!_options]]">
-                Save Group Options
-              </gr-button>
-            </span>
-          </fieldset>
-        </fieldset>
-      </div>
-    </div>
-  </div>
-`;
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
index 74e2364..5e96e33 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.ts
@@ -21,10 +21,15 @@
 import {
   addListenerForTest,
   mockPromise,
+  queryAndAssert,
   stubRestApi,
 } from '../../../test/test-utils';
 import {createGroupInfo} from '../../../test/test-data-generators.js';
 import {GroupId, GroupInfo, GroupName} from '../../../types/common';
+import {GrAutocomplete} from '../../shared/gr-autocomplete/gr-autocomplete';
+import {GrButton} from '../../shared/gr-button/gr-button';
+import {GrCopyClipboard} from '../../shared/gr-copy-clipboard/gr-copy-clipboard';
+import {GrSelect} from '../../shared/gr-select/gr-select';
 
 const basicFixture = fixtureFromElement('gr-group');
 
@@ -45,24 +50,43 @@
     name: 'Administrators' as GroupName,
   };
 
-  setup(() => {
+  setup(async () => {
     element = basicFixture.instantiate();
+    await element.updateComplete;
     groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
   });
 
   test('loading displays before group config is loaded', () => {
-    assert.isTrue(element.$.loading.classList.contains('loading'));
-    assert.isFalse(getComputedStyle(element.$.loading).display === 'none');
-    assert.isTrue(element.$.loadedContent.classList.contains('loading'));
-    assert.isTrue(getComputedStyle(element.$.loadedContent).display === 'none');
+    assert.isTrue(
+      queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+        'loading'
+      )
+    );
+    assert.isFalse(
+      getComputedStyle(queryAndAssert<HTMLDivElement>(element, '#loading'))
+        .display === 'none'
+    );
+    assert.isTrue(
+      queryAndAssert<HTMLDivElement>(
+        element,
+        '#loadedContent'
+      ).classList.contains('loading')
+    );
+    assert.isTrue(
+      getComputedStyle(
+        queryAndAssert<HTMLDivElement>(element, '#loadedContent')
+      ).display === 'none'
+    );
   });
 
   test('default values are populated with internal group', async () => {
     stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     element.groupId = '1' as GroupId;
-    await element._loadGroup();
-    assert.isTrue(element._groupIsInternal);
-    assert.isFalse(element.$.visibleToAll.bindValue);
+    await element.loadGroup();
+    assert.isTrue(element.groupIsInternal);
+    assert.isFalse(
+      queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue
+    );
   });
 
   test('default values with external group', async () => {
@@ -74,71 +98,102 @@
     );
     stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     element.groupId = '1' as GroupId;
-    await element._loadGroup();
-    assert.isFalse(element._groupIsInternal);
-    assert.isFalse(element.$.visibleToAll.bindValue);
+    await element.loadGroup();
+    assert.isFalse(element.groupIsInternal);
+    assert.isFalse(
+      queryAndAssert<GrSelect>(element, '#visibleToAll').bindValue
+    );
   });
 
   test('rename group', async () => {
     const groupName = 'test-group';
     const groupName2 = 'test-group2';
     element.groupId = '1' as GroupId;
-    element._groupConfig = {
+    element.groupConfig = {
       name: groupName as GroupName,
       id: '1' as GroupId,
     };
-    element._groupName = groupName as GroupName;
+    element.originalName = groupName as GroupName;
 
     stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     stubRestApi('saveGroupName').returns(
       Promise.resolve({...new Response(), status: 200})
     );
 
-    const button = element.$.inputUpdateNameBtn;
+    const button = queryAndAssert<GrButton>(element, '#inputUpdateNameBtn');
 
-    await element._loadGroup();
+    await element.loadGroup();
     assert.isTrue(button.hasAttribute('disabled'));
-    assert.isFalse(element.$.Title.classList.contains('edited'));
+    assert.isFalse(
+      queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+        'edited'
+      )
+    );
 
-    element.$.groupNameInput.text = groupName2;
+    queryAndAssert<GrAutocomplete>(element, '#groupNameInput').text =
+      groupName2;
 
-    await flush();
+    await element.updateComplete;
+
     assert.isFalse(button.hasAttribute('disabled'));
-    assert.isTrue(element.$.groupName.classList.contains('edited'));
+    assert.isTrue(
+      queryAndAssert<HTMLHeadingElement>(
+        element,
+        '#groupName'
+      ).classList.contains('edited')
+    );
 
-    await element._handleSaveName();
-    assert.isTrue(button.hasAttribute('disabled'));
-    assert.isFalse(element.$.Title.classList.contains('edited'));
-    assert.equal(element._groupName, groupName2);
+    await element.handleSaveName();
+    assert.isTrue(button.disabled);
+    assert.isFalse(
+      queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+        'edited'
+      )
+    );
+    assert.equal(element.originalName, groupName2);
   });
 
   test('rename group owner', async () => {
     const groupName = 'test-group';
     element.groupId = '1' as GroupId;
-    element._groupConfig = {
+    element.groupConfig = {
       name: groupName as GroupName,
       id: '1' as GroupId,
     };
-    element._groupConfigOwner = 'testId';
-    element._groupOwner = true;
+    element.groupConfigOwner = 'testId';
+    element.groupOwner = true;
 
     stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
 
-    const button = element.$.inputUpdateOwnerBtn;
+    const button = queryAndAssert<GrButton>(element, '#inputUpdateOwnerBtn');
 
-    await element._loadGroup();
-    assert.isTrue(button.hasAttribute('disabled'));
-    assert.isFalse(element.$.Title.classList.contains('edited'));
+    await element.loadGroup();
+    assert.isTrue(button.disabled);
+    assert.isFalse(
+      queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+        'edited'
+      )
+    );
 
-    element.$.groupOwnerInput.text = 'testId2';
+    queryAndAssert<GrAutocomplete>(element, '#groupOwnerInput').text =
+      'testId2';
 
-    await flush();
-    assert.isFalse(button.hasAttribute('disabled'));
-    assert.isTrue(element.$.groupOwner.classList.contains('edited'));
+    await element.updateComplete;
+    assert.isFalse(button.disabled);
+    assert.isTrue(
+      queryAndAssert<HTMLHeadingElement>(
+        element,
+        '#groupOwner'
+      ).classList.contains('edited')
+    );
 
-    await element._handleSaveOwner();
-    assert.isTrue(button.hasAttribute('disabled'));
-    assert.isFalse(element.$.Title.classList.contains('edited'));
+    await element.handleSaveOwner();
+    assert.isTrue(button.disabled);
+    assert.isFalse(
+      queryAndAssert<HTMLHeadingElement>(element, '#Title').classList.contains(
+        'edited'
+      )
+    );
   });
 
   test('test for undefined group name', async () => {
@@ -154,14 +209,18 @@
 
     // Test that loading shows instead of filling
     // in group details
-    await element._loadGroup();
-    assert.isTrue(element.$.loading.classList.contains('loading'));
+    await element.loadGroup();
+    assert.isTrue(
+      queryAndAssert<HTMLDivElement>(element, '#loading').classList.contains(
+        'loading'
+      )
+    );
 
-    assert.isTrue(element._loading);
+    assert.isTrue(element.loading);
   });
 
   test('test fire event', async () => {
-    element._groupConfig = {
+    element.groupConfig = {
       name: 'test-group' as GroupName,
       id: '1' as GroupId,
     };
@@ -171,53 +230,37 @@
     );
 
     const showStub = sinon.stub(element, 'dispatchEvent');
-    await element._handleSaveName();
+    await element.handleSaveName();
     assert.isTrue(showStub.called);
   });
 
-  test('_computeGroupDisabled', () => {
-    let admin = true;
-    let owner = false;
-    let groupIsInternal = true;
-    assert.equal(
-      element._computeGroupDisabled(owner, admin, groupIsInternal),
-      false
-    );
+  test('computeGroupDisabled', () => {
+    element.isAdmin = true;
+    element.groupOwner = false;
+    element.groupIsInternal = true;
+    assert.equal(element.computeGroupDisabled(), false);
 
-    admin = false;
-    assert.equal(
-      element._computeGroupDisabled(owner, admin, groupIsInternal),
-      true
-    );
+    element.isAdmin = false;
+    assert.equal(element.computeGroupDisabled(), true);
 
-    owner = true;
-    assert.equal(
-      element._computeGroupDisabled(owner, admin, groupIsInternal),
-      false
-    );
+    element.groupOwner = true;
+    assert.equal(element.computeGroupDisabled(), false);
 
-    owner = false;
-    assert.equal(
-      element._computeGroupDisabled(owner, admin, groupIsInternal),
-      true
-    );
+    element.groupOwner = false;
+    assert.equal(element.computeGroupDisabled(), true);
 
-    groupIsInternal = false;
-    assert.equal(
-      element._computeGroupDisabled(owner, admin, groupIsInternal),
-      true
-    );
+    element.groupIsInternal = false;
+    assert.equal(element.computeGroupDisabled(), true);
 
-    admin = true;
-    assert.equal(
-      element._computeGroupDisabled(owner, admin, groupIsInternal),
-      true
-    );
+    element.isAdmin = true;
+    assert.equal(element.computeGroupDisabled(), true);
   });
 
-  test('_computeLoadingClass', () => {
-    assert.equal(element._computeLoadingClass(true), 'loading');
-    assert.equal(element._computeLoadingClass(false), '');
+  test('computeLoadingClass', () => {
+    element.loading = true;
+    assert.equal(element.computeLoadingClass(), 'loading');
+    element.loading = false;
+    assert.equal(element.computeLoadingClass(), '');
   });
 
   test('fires page-error', async () => {
@@ -241,21 +284,31 @@
       promise.resolve();
     });
 
-    element._loadGroup();
+    await element.loadGroup();
     await promise;
   });
 
-  test('uuid', () => {
-    element._groupConfig = {
+  test('uuid', async () => {
+    element.groupConfig = {
       id: '6a1e70e1a88782771a91808c8af9bbb7a9871389' as GroupId,
     };
 
-    assert.equal(element._groupConfig.id, element.$.uuid.text);
+    await element.updateComplete;
 
-    element._groupConfig = {
+    assert.equal(
+      element.groupConfig.id,
+      queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+    );
+
+    element.groupConfig = {
       id: 'user%2Fgroup' as GroupId,
     };
 
-    assert.equal('user/group', element.$.uuid.text);
+    await element.updateComplete;
+
+    assert.equal(
+      'user/group',
+      queryAndAssert<GrCopyClipboard>(element, '#uuid').text
+    );
   });
 });