Convert polygerrit to es6-modules

This change replace all HTML imports with es6-modules. The only exceptions are:
* gr-app.html file, which can be deleted only after updating the
  gerrit/httpd/raw/PolyGerritIndexHtml.soy file.
* dark-theme.html which is loaded via importHref. Must be updated manually
  later in a separate change.

This change was produced automatically by ./es6-modules-converter.sh script.
No manual changes were made.

Change-Id: I0c447dd8c05757741e2c940720652d01d9fb7d67
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
index 9880e88..6f1eaa8 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.js
@@ -1,6 +1,6 @@
 /**
  * @license
- * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2015 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,797 +14,823 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-(function() {
-  'use strict';
+import '../../../scripts/bundled-polymer.js';
 
-  const STORAGE_DEBOUNCE_INTERVAL = 400;
-  const TOAST_DEBOUNCE_INTERVAL = 200;
+import '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
+import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea.js';
+import '../../../behaviors/fire-behavior/fire-behavior.js';
+import '../../../styles/shared-styles.js';
+import '../../core/gr-reporting/gr-reporting.js';
+import '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.js';
+import '../../plugins/gr-endpoint-param/gr-endpoint-param.js';
+import '../gr-button/gr-button.js';
+import '../gr-dialog/gr-dialog.js';
+import '../gr-date-formatter/gr-date-formatter.js';
+import '../gr-formatted-text/gr-formatted-text.js';
+import '../gr-icons/gr-icons.js';
+import '../gr-overlay/gr-overlay.js';
+import '../gr-rest-api-interface/gr-rest-api-interface.js';
+import '../gr-storage/gr-storage.js';
+import '../gr-textarea/gr-textarea.js';
+import '../gr-tooltip-content/gr-tooltip-content.js';
+import '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.js';
+import '../../../scripts/rootElement.js';
+import {flush, dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {mixinBehaviors} from '@polymer/polymer/lib/legacy/class.js';
+import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners.js';
+import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
+import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {htmlTemplate} from './gr-comment_html.js';
 
-  const SAVING_MESSAGE = 'Saving';
-  const DRAFT_SINGULAR = 'draft...';
-  const DRAFT_PLURAL = 'drafts...';
-  const SAVED_MESSAGE = 'All changes saved';
+const STORAGE_DEBOUNCE_INTERVAL = 400;
+const TOAST_DEBOUNCE_INTERVAL = 200;
 
-  const REPORT_CREATE_DRAFT = 'CreateDraftComment';
-  const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
-  const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+const SAVING_MESSAGE = 'Saving';
+const DRAFT_SINGULAR = 'draft...';
+const DRAFT_PLURAL = 'drafts...';
+const SAVED_MESSAGE = 'All changes saved';
 
-  const FILE = 'FILE';
+const REPORT_CREATE_DRAFT = 'CreateDraftComment';
+const REPORT_UPDATE_DRAFT = 'UpdateDraftComment';
+const REPORT_DISCARD_DRAFT = 'DiscardDraftComment';
+
+const FILE = 'FILE';
+
+/**
+ * All candidates tips to show, will pick randomly.
+ */
+const RESPECTFUL_REVIEW_TIPS= [
+  'DO: Assume competence.',
+  'DO: Provide rationale or context.',
+  'DO: Consider how comments may be interpreted.',
+  'DON’T: Criticize the person.',
+  'DON’T: Use harsh language.',
+];
+
+/**
+ * @appliesMixin Gerrit.FireMixin
+ * @appliesMixin Gerrit.KeyboardShortcutMixin
+ * @extends Polymer.Element
+ */
+class GrComment extends mixinBehaviors( [
+  Gerrit.FireBehavior,
+  Gerrit.KeyboardShortcutBehavior,
+], GestureEventListeners(
+    LegacyElementMixin(
+        PolymerElement))) {
+  static get template() { return htmlTemplate; }
+
+  static get is() { return 'gr-comment'; }
+  /**
+   * Fired when the create fix comment action is triggered.
+   *
+   * @event create-fix-comment
+   */
 
   /**
-   * All candidates tips to show, will pick randomly.
+   * Fired when the show fix preview action is triggered.
+   *
+   * @event open-fix-preview
    */
-  const RESPECTFUL_REVIEW_TIPS= [
-    'DO: Assume competence.',
-    'DO: Provide rationale or context.',
-    'DO: Consider how comments may be interpreted.',
-    'DON’T: Criticize the person.',
-    'DON’T: Use harsh language.',
-  ];
 
   /**
-   * @appliesMixin Gerrit.FireMixin
-   * @appliesMixin Gerrit.KeyboardShortcutMixin
-   * @extends Polymer.Element
+   * Fired when this comment is discarded.
+   *
+   * @event comment-discard
    */
-  class GrComment extends Polymer.mixinBehaviors( [
-    Gerrit.FireBehavior,
-    Gerrit.KeyboardShortcutBehavior,
-  ], Polymer.GestureEventListeners(
-      Polymer.LegacyElementMixin(
-          Polymer.Element))) {
-    static get is() { return 'gr-comment'; }
-    /**
-     * Fired when the create fix comment action is triggered.
-     *
-     * @event create-fix-comment
-     */
 
-    /**
-     * Fired when the show fix preview action is triggered.
-     *
-     * @event open-fix-preview
-     */
+  /**
+   * Fired when this comment is saved.
+   *
+   * @event comment-save
+   */
 
-    /**
-     * Fired when this comment is discarded.
-     *
-     * @event comment-discard
-     */
+  /**
+   * Fired when this comment is updated.
+   *
+   * @event comment-update
+   */
 
-    /**
-     * Fired when this comment is saved.
-     *
-     * @event comment-save
-     */
+  /**
+   * Fired when the comment's timestamp is tapped.
+   *
+   * @event comment-anchor-tap
+   */
 
-    /**
-     * Fired when this comment is updated.
-     *
-     * @event comment-update
-     */
+  static get properties() {
+    return {
+      changeNum: String,
+      /** @type {!Gerrit.Comment} */
+      comment: {
+        type: Object,
+        notify: true,
+        observer: '_commentChanged',
+      },
+      comments: {
+        type: Array,
+      },
+      isRobotComment: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      disabled: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      draft: {
+        type: Boolean,
+        value: false,
+        observer: '_draftChanged',
+      },
+      editing: {
+        type: Boolean,
+        value: false,
+        observer: '_editingChanged',
+      },
+      discarding: {
+        type: Boolean,
+        value: false,
+        reflectToAttribute: true,
+      },
+      hasChildren: Boolean,
+      patchNum: String,
+      showActions: Boolean,
+      _showHumanActions: Boolean,
+      _showRobotActions: Boolean,
+      collapsed: {
+        type: Boolean,
+        value: true,
+        observer: '_toggleCollapseClass',
+      },
+      /** @type {?} */
+      projectConfig: Object,
+      robotButtonDisabled: Boolean,
+      _hasHumanReply: Boolean,
+      _isAdmin: {
+        type: Boolean,
+        value: false,
+      },
 
-    /**
-     * Fired when the comment's timestamp is tapped.
-     *
-     * @event comment-anchor-tap
-     */
+      _xhrPromise: Object, // Used for testing.
+      _messageText: {
+        type: String,
+        value: '',
+        observer: '_messageTextChanged',
+      },
+      commentSide: String,
+      side: String,
 
-    static get properties() {
-      return {
-        changeNum: String,
-        /** @type {!Gerrit.Comment} */
-        comment: {
-          type: Object,
-          notify: true,
-          observer: '_commentChanged',
-        },
-        comments: {
-          type: Array,
-        },
-        isRobotComment: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        disabled: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        draft: {
-          type: Boolean,
-          value: false,
-          observer: '_draftChanged',
-        },
-        editing: {
-          type: Boolean,
-          value: false,
-          observer: '_editingChanged',
-        },
-        discarding: {
-          type: Boolean,
-          value: false,
-          reflectToAttribute: true,
-        },
-        hasChildren: Boolean,
-        patchNum: String,
-        showActions: Boolean,
-        _showHumanActions: Boolean,
-        _showRobotActions: Boolean,
-        collapsed: {
-          type: Boolean,
-          value: true,
-          observer: '_toggleCollapseClass',
-        },
-        /** @type {?} */
-        projectConfig: Object,
-        robotButtonDisabled: Boolean,
-        _hasHumanReply: Boolean,
-        _isAdmin: {
-          type: Boolean,
-          value: false,
-        },
+      resolved: Boolean,
 
-        _xhrPromise: Object, // Used for testing.
-        _messageText: {
-          type: String,
-          value: '',
-          observer: '_messageTextChanged',
-        },
-        commentSide: String,
-        side: String,
+      _numPendingDraftRequests: {
+        type: Object,
+        value:
+          {number: 0}, // Intentional to share the object across instances.
+      },
 
-        resolved: Boolean,
+      _enableOverlay: {
+        type: Boolean,
+        value: false,
+      },
 
-        _numPendingDraftRequests: {
-          type: Object,
-          value:
-            {number: 0}, // Intentional to share the object across instances.
-        },
+      /**
+       * Property for storing references to overlay elements. When the overlays
+       * are moved to Gerrit.getRootElement() to be shown they are no-longer
+       * children, so they can't be queried along the tree, so they are stored
+       * here.
+       */
+      _overlays: {
+        type: Object,
+        value: () => { return {}; },
+      },
 
-        _enableOverlay: {
-          type: Boolean,
-          value: false,
-        },
+      _showRespectfulTip: {
+        type: Boolean,
+        value: false,
+      },
+      _respectfulReviewTip: String,
+      _respectfulTipDismissed: {
+        type: Boolean,
+        value: false,
+      },
+    };
+  }
 
-        /**
-         * Property for storing references to overlay elements. When the overlays
-         * are moved to Gerrit.getRootElement() to be shown they are no-longer
-         * children, so they can't be queried along the tree, so they are stored
-         * here.
-         */
-        _overlays: {
-          type: Object,
-          value: () => { return {}; },
-        },
+  static get observers() {
+    return [
+      '_commentMessageChanged(comment.message)',
+      '_loadLocalDraft(changeNum, patchNum, comment)',
+      '_isRobotComment(comment)',
+      '_calculateActionstoShow(showActions, isRobotComment)',
+      '_computeHasHumanReply(comment, comments.*)',
+      '_onEditingChange(editing)',
+    ];
+  }
 
-        _showRespectfulTip: {
-          type: Boolean,
-          value: false,
-        },
-        _respectfulReviewTip: String,
-        _respectfulTipDismissed: {
-          type: Boolean,
-          value: false,
-        },
-      };
+  get keyBindings() {
+    return {
+      'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
+      'esc': '_handleEsc',
+    };
+  }
+
+  /** @override */
+  attached() {
+    super.attached();
+    if (this.editing) {
+      this.collapsed = false;
+    } else if (this.comment) {
+      this.collapsed = this.comment.collapsed;
     }
+    this._getIsAdmin().then(isAdmin => {
+      this._isAdmin = isAdmin;
+    });
+  }
 
-    static get observers() {
-      return [
-        '_commentMessageChanged(comment.message)',
-        '_loadLocalDraft(changeNum, patchNum, comment)',
-        '_isRobotComment(comment)',
-        '_calculateActionstoShow(showActions, isRobotComment)',
-        '_computeHasHumanReply(comment, comments.*)',
-        '_onEditingChange(editing)',
-      ];
+  /** @override */
+  detached() {
+    super.detached();
+    this.cancelDebouncer('fire-update');
+    if (this.textarea) {
+      this.textarea.closeDropdown();
     }
+  }
 
-    get keyBindings() {
-      return {
-        'ctrl+enter meta+enter ctrl+s meta+s': '_handleSaveKey',
-        'esc': '_handleEsc',
-      };
-    }
-
-    /** @override */
-    attached() {
-      super.attached();
-      if (this.editing) {
-        this.collapsed = false;
-      } else if (this.comment) {
-        this.collapsed = this.comment.collapsed;
-      }
-      this._getIsAdmin().then(isAdmin => {
-        this._isAdmin = isAdmin;
-      });
-    }
-
-    /** @override */
-    detached() {
-      super.detached();
-      this.cancelDebouncer('fire-update');
-      if (this.textarea) {
-        this.textarea.closeDropdown();
-      }
-    }
-
-    _onEditingChange(editing) {
-      if (!editing) return;
-      // visibility based on cache this will make sure we only and always show
-      // a tip once every Math.max(a day, period between creating comments)
-      const cachedVisibilityOfRespectfulTip =
-        this.$.storage.getRespectfulTipVisibility();
-      if (!cachedVisibilityOfRespectfulTip) {
-        // we still want to show the tip with a probability of 30%
-        if (this.getRandomNum(0, 3) >= 1) return;
-        this._showRespectfulTip = true;
-        const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
-        this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
-        this.$.reporting.reportInteraction(
-            'respectful-tip-appeared',
-            {tip: this._respectfulReviewTip}
-        );
-        // update cache
-        this.$.storage.setRespectfulTipVisibility();
-      }
-    }
-
-    /** Set as a separate method so easy to stub. */
-    getRandomNum(min, max) {
-      return Math.floor(Math.random() * (max - min) + min);
-    }
-
-    _computeVisibilityOfTip(showTip, tipDismissed) {
-      return showTip && !tipDismissed;
-    }
-
-    _dismissRespectfulTip() {
-      this._respectfulTipDismissed = true;
+  _onEditingChange(editing) {
+    if (!editing) return;
+    // visibility based on cache this will make sure we only and always show
+    // a tip once every Math.max(a day, period between creating comments)
+    const cachedVisibilityOfRespectfulTip =
+      this.$.storage.getRespectfulTipVisibility();
+    if (!cachedVisibilityOfRespectfulTip) {
+      // we still want to show the tip with a probability of 30%
+      if (this.getRandomNum(0, 3) >= 1) return;
+      this._showRespectfulTip = true;
+      const randomIdx = this.getRandomNum(0, RESPECTFUL_REVIEW_TIPS.length);
+      this._respectfulReviewTip = RESPECTFUL_REVIEW_TIPS[randomIdx];
       this.$.reporting.reportInteraction(
-          'respectful-tip-dismissed',
+          'respectful-tip-appeared',
           {tip: this._respectfulReviewTip}
       );
-      // add a 3 day delay to the tip cache
-      this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3);
+      // update cache
+      this.$.storage.setRespectfulTipVisibility();
+    }
+  }
+
+  /** Set as a separate method so easy to stub. */
+  getRandomNum(min, max) {
+    return Math.floor(Math.random() * (max - min) + min);
+  }
+
+  _computeVisibilityOfTip(showTip, tipDismissed) {
+    return showTip && !tipDismissed;
+  }
+
+  _dismissRespectfulTip() {
+    this._respectfulTipDismissed = true;
+    this.$.reporting.reportInteraction(
+        'respectful-tip-dismissed',
+        {tip: this._respectfulReviewTip}
+    );
+    // add a 3 day delay to the tip cache
+    this.$.storage.setRespectfulTipVisibility(/* delayDays= */ 3);
+  }
+
+  _onRespectfulReadMoreClick() {
+    this.$.reporting.reportInteraction('respectful-read-more-clicked');
+  }
+
+  get textarea() {
+    return this.shadowRoot.querySelector('#editTextarea');
+  }
+
+  get confirmDeleteOverlay() {
+    if (!this._overlays.confirmDelete) {
+      this._enableOverlay = true;
+      flush();
+      this._overlays.confirmDelete = this.shadowRoot
+          .querySelector('#confirmDeleteOverlay');
+    }
+    return this._overlays.confirmDelete;
+  }
+
+  get confirmDiscardOverlay() {
+    if (!this._overlays.confirmDiscard) {
+      this._enableOverlay = true;
+      flush();
+      this._overlays.confirmDiscard = this.shadowRoot
+          .querySelector('#confirmDiscardOverlay');
+    }
+    return this._overlays.confirmDiscard;
+  }
+
+  _computeShowHideIcon(collapsed) {
+    return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
+  }
+
+  _calculateActionstoShow(showActions, isRobotComment) {
+    // Polymer 2: check for undefined
+    if ([showActions, isRobotComment].some(arg => arg === undefined)) {
+      return;
     }
 
-    _onRespectfulReadMoreClick() {
-      this.$.reporting.reportInteraction('respectful-read-more-clicked');
+    this._showHumanActions = showActions && !isRobotComment;
+    this._showRobotActions = showActions && isRobotComment;
+  }
+
+  _isRobotComment(comment) {
+    this.isRobotComment = !!comment.robot_id;
+  }
+
+  isOnParent() {
+    return this.side === 'PARENT';
+  }
+
+  _getIsAdmin() {
+    return this.$.restAPI.getIsAdmin();
+  }
+
+  /**
+   * @param {*=} opt_comment
+   */
+  save(opt_comment) {
+    let comment = opt_comment;
+    if (!comment) {
+      comment = this.comment;
     }
 
-    get textarea() {
-      return this.shadowRoot.querySelector('#editTextarea');
+    this.set('comment.message', this._messageText);
+    this.editing = false;
+    this.disabled = true;
+
+    if (!this._messageText) {
+      return this._discardDraft();
     }
 
-    get confirmDeleteOverlay() {
-      if (!this._overlays.confirmDelete) {
-        this._enableOverlay = true;
-        Polymer.dom.flush();
-        this._overlays.confirmDelete = this.shadowRoot
-            .querySelector('#confirmDeleteOverlay');
-      }
-      return this._overlays.confirmDelete;
-    }
+    this._xhrPromise = this._saveDraft(comment).then(response => {
+      this.disabled = false;
+      if (!response.ok) { return response; }
 
-    get confirmDiscardOverlay() {
-      if (!this._overlays.confirmDiscard) {
-        this._enableOverlay = true;
-        Polymer.dom.flush();
-        this._overlays.confirmDiscard = this.shadowRoot
-            .querySelector('#confirmDiscardOverlay');
-      }
-      return this._overlays.confirmDiscard;
-    }
-
-    _computeShowHideIcon(collapsed) {
-      return collapsed ? 'gr-icons:expand-more' : 'gr-icons:expand-less';
-    }
-
-    _calculateActionstoShow(showActions, isRobotComment) {
-      // Polymer 2: check for undefined
-      if ([showActions, isRobotComment].some(arg => arg === undefined)) {
-        return;
-      }
-
-      this._showHumanActions = showActions && !isRobotComment;
-      this._showRobotActions = showActions && isRobotComment;
-    }
-
-    _isRobotComment(comment) {
-      this.isRobotComment = !!comment.robot_id;
-    }
-
-    isOnParent() {
-      return this.side === 'PARENT';
-    }
-
-    _getIsAdmin() {
-      return this.$.restAPI.getIsAdmin();
-    }
-
-    /**
-     * @param {*=} opt_comment
-     */
-    save(opt_comment) {
-      let comment = opt_comment;
-      if (!comment) {
-        comment = this.comment;
-      }
-
-      this.set('comment.message', this._messageText);
-      this.editing = false;
-      this.disabled = true;
-
-      if (!this._messageText) {
-        return this._discardDraft();
-      }
-
-      this._xhrPromise = this._saveDraft(comment).then(response => {
-        this.disabled = false;
-        if (!response.ok) { return response; }
-
-        this._eraseDraftComment();
-        return this.$.restAPI.getResponseObject(response).then(obj => {
-          const resComment = obj;
-          resComment.__draft = true;
-          // Maintain the ephemeral draft ID for identification by other
-          // elements.
-          if (this.comment.__draftID) {
-            resComment.__draftID = this.comment.__draftID;
-          }
-          resComment.__commentSide = this.commentSide;
-          this.comment = resComment;
-          this._fireSave();
-          return obj;
+      this._eraseDraftComment();
+      return this.$.restAPI.getResponseObject(response).then(obj => {
+        const resComment = obj;
+        resComment.__draft = true;
+        // Maintain the ephemeral draft ID for identification by other
+        // elements.
+        if (this.comment.__draftID) {
+          resComment.__draftID = this.comment.__draftID;
+        }
+        resComment.__commentSide = this.commentSide;
+        this.comment = resComment;
+        this._fireSave();
+        return obj;
+      });
+    })
+        .catch(err => {
+          this.disabled = false;
+          throw err;
         });
-      })
-          .catch(err => {
-            this.disabled = false;
-            throw err;
-          });
 
-      return this._xhrPromise;
+    return this._xhrPromise;
+  }
+
+  _eraseDraftComment() {
+    // Prevents a race condition in which removing the draft comment occurs
+    // prior to it being saved.
+    this.cancelDebouncer('store');
+
+    this.$.storage.eraseDraftComment({
+      changeNum: this.changeNum,
+      patchNum: this._getPatchNum(),
+      path: this.comment.path,
+      line: this.comment.line,
+      range: this.comment.range,
+    });
+  }
+
+  _commentChanged(comment) {
+    this.editing = !!comment.__editing;
+    this.resolved = !comment.unresolved;
+    if (this.editing) { // It's a new draft/reply, notify.
+      this._fireUpdate();
+    }
+  }
+
+  _computeHasHumanReply() {
+    if (!this.comment || !this.comments) return;
+    // hide please fix button for robot comment that has human reply
+    this._hasHumanReply = this.comments
+        .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
+          !c.robot_id);
+  }
+
+  /**
+   * @param {!Object=} opt_mixin
+   *
+   * @return {!Object}
+   */
+  _getEventPayload(opt_mixin) {
+    return Object.assign({}, opt_mixin, {
+      comment: this.comment,
+      patchNum: this.patchNum,
+    });
+  }
+
+  _fireSave() {
+    this.fire('comment-save', this._getEventPayload());
+  }
+
+  _fireUpdate() {
+    this.debounce('fire-update', () => {
+      this.fire('comment-update', this._getEventPayload());
+    });
+  }
+
+  _draftChanged(draft) {
+    this.$.container.classList.toggle('draft', draft);
+  }
+
+  _editingChanged(editing, previousValue) {
+    // Polymer 2: observer fires when at least one property is defined.
+    // Do nothing to prevent comment.__editing being overwritten
+    // if previousValue is undefined
+    if (previousValue === undefined) return;
+
+    this.$.container.classList.toggle('editing', editing);
+    if (this.comment && this.comment.id) {
+      this.shadowRoot.querySelector('.cancel').hidden = !editing;
+    }
+    if (this.comment) {
+      this.comment.__editing = this.editing;
+    }
+    if (editing != !!previousValue) {
+      // To prevent event firing on comment creation.
+      this._fireUpdate();
+    }
+    if (editing) {
+      this.async(() => {
+        flush();
+        this.textarea && this.textarea.putCursorAtEnd();
+      }, 1);
+    }
+  }
+
+  _computeDeleteButtonClass(isAdmin, draft) {
+    return isAdmin && !draft ? 'showDeleteButtons' : '';
+  }
+
+  _computeSaveDisabled(draft, comment, resolved) {
+    // If resolved state has changed and a msg exists, save should be enabled.
+    if (!comment || comment.unresolved === resolved && draft) {
+      return false;
+    }
+    return !draft || draft.trim() === '';
+  }
+
+  _handleSaveKey(e) {
+    if (!this._computeSaveDisabled(this._messageText, this.comment,
+        this.resolved)) {
+      e.preventDefault();
+      this._handleSave(e);
+    }
+  }
+
+  _handleEsc(e) {
+    if (!this._messageText.length) {
+      e.preventDefault();
+      this._handleCancel(e);
+    }
+  }
+
+  _handleToggleCollapsed() {
+    this.collapsed = !this.collapsed;
+  }
+
+  _toggleCollapseClass(collapsed) {
+    if (collapsed) {
+      this.$.container.classList.add('collapsed');
+    } else {
+      this.$.container.classList.remove('collapsed');
+    }
+  }
+
+  _commentMessageChanged(message) {
+    this._messageText = message || '';
+  }
+
+  _messageTextChanged(newValue, oldValue) {
+    if (!this.comment || (this.comment && this.comment.id)) {
+      return;
     }
 
-    _eraseDraftComment() {
-      // Prevents a race condition in which removing the draft comment occurs
-      // prior to it being saved.
-      this.cancelDebouncer('store');
-
-      this.$.storage.eraseDraftComment({
+    this.debounce('store', () => {
+      const message = this._messageText;
+      const commentLocation = {
         changeNum: this.changeNum,
         patchNum: this._getPatchNum(),
         path: this.comment.path,
         line: this.comment.line,
         range: this.comment.range,
-      });
-    }
+      };
 
-    _commentChanged(comment) {
-      this.editing = !!comment.__editing;
-      this.resolved = !comment.unresolved;
-      if (this.editing) { // It's a new draft/reply, notify.
-        this._fireUpdate();
-      }
-    }
-
-    _computeHasHumanReply() {
-      if (!this.comment || !this.comments) return;
-      // hide please fix button for robot comment that has human reply
-      this._hasHumanReply = this.comments
-          .some(c => c.in_reply_to && c.in_reply_to === this.comment.id &&
-            !c.robot_id);
-    }
-
-    /**
-     * @param {!Object=} opt_mixin
-     *
-     * @return {!Object}
-     */
-    _getEventPayload(opt_mixin) {
-      return Object.assign({}, opt_mixin, {
-        comment: this.comment,
-        patchNum: this.patchNum,
-      });
-    }
-
-    _fireSave() {
-      this.fire('comment-save', this._getEventPayload());
-    }
-
-    _fireUpdate() {
-      this.debounce('fire-update', () => {
-        this.fire('comment-update', this._getEventPayload());
-      });
-    }
-
-    _draftChanged(draft) {
-      this.$.container.classList.toggle('draft', draft);
-    }
-
-    _editingChanged(editing, previousValue) {
-      // Polymer 2: observer fires when at least one property is defined.
-      // Do nothing to prevent comment.__editing being overwritten
-      // if previousValue is undefined
-      if (previousValue === undefined) return;
-
-      this.$.container.classList.toggle('editing', editing);
-      if (this.comment && this.comment.id) {
-        this.shadowRoot.querySelector('.cancel').hidden = !editing;
-      }
-      if (this.comment) {
-        this.comment.__editing = this.editing;
-      }
-      if (editing != !!previousValue) {
-        // To prevent event firing on comment creation.
-        this._fireUpdate();
-      }
-      if (editing) {
-        this.async(() => {
-          Polymer.dom.flush();
-          this.textarea && this.textarea.putCursorAtEnd();
-        }, 1);
-      }
-    }
-
-    _computeDeleteButtonClass(isAdmin, draft) {
-      return isAdmin && !draft ? 'showDeleteButtons' : '';
-    }
-
-    _computeSaveDisabled(draft, comment, resolved) {
-      // If resolved state has changed and a msg exists, save should be enabled.
-      if (!comment || comment.unresolved === resolved && draft) {
-        return false;
-      }
-      return !draft || draft.trim() === '';
-    }
-
-    _handleSaveKey(e) {
-      if (!this._computeSaveDisabled(this._messageText, this.comment,
-          this.resolved)) {
-        e.preventDefault();
-        this._handleSave(e);
-      }
-    }
-
-    _handleEsc(e) {
-      if (!this._messageText.length) {
-        e.preventDefault();
-        this._handleCancel(e);
-      }
-    }
-
-    _handleToggleCollapsed() {
-      this.collapsed = !this.collapsed;
-    }
-
-    _toggleCollapseClass(collapsed) {
-      if (collapsed) {
-        this.$.container.classList.add('collapsed');
+      if ((!this._messageText || !this._messageText.length) && oldValue) {
+        // If the draft has been modified to be empty, then erase the storage
+        // entry.
+        this.$.storage.eraseDraftComment(commentLocation);
       } else {
-        this.$.container.classList.remove('collapsed');
+        this.$.storage.setDraftComment(commentLocation, message);
       }
+    }, STORAGE_DEBOUNCE_INTERVAL);
+  }
+
+  _handleAnchorClick(e) {
+    e.preventDefault();
+    if (!this.comment.line) {
+      return;
+    }
+    this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
+      bubbles: true,
+      composed: true,
+      detail: {
+        number: this.comment.line || FILE,
+        side: this.side,
+      },
+    }));
+  }
+
+  _handleEdit(e) {
+    e.preventDefault();
+    this._messageText = this.comment.message;
+    this.editing = true;
+    this.$.reporting.recordDraftInteraction();
+  }
+
+  _handleSave(e) {
+    e.preventDefault();
+
+    // Ignore saves started while already saving.
+    if (this.disabled) {
+      return;
+    }
+    const timingLabel = this.comment.id ?
+      REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
+    const timer = this.$.reporting.getTimer(timingLabel);
+    this.set('comment.__editing', false);
+    return this.save().then(() => { timer.end(); });
+  }
+
+  _handleCancel(e) {
+    e.preventDefault();
+
+    if (!this.comment.message ||
+        this.comment.message.trim().length === 0 ||
+        !this.comment.id) {
+      this._fireDiscard();
+      return;
+    }
+    this._messageText = this.comment.message;
+    this.editing = false;
+  }
+
+  _fireDiscard() {
+    this.cancelDebouncer('fire-update');
+    this.fire('comment-discard', this._getEventPayload());
+  }
+
+  _handleFix() {
+    this.dispatchEvent(new CustomEvent('create-fix-comment', {
+      bubbles: true,
+      composed: true,
+      detail: this._getEventPayload(),
+    }));
+  }
+
+  _handleShowFix() {
+    this.dispatchEvent(new CustomEvent('open-fix-preview', {
+      bubbles: true,
+      composed: true,
+      detail: this._getEventPayload(),
+    }));
+  }
+
+  _hasNoFix(comment) {
+    return !comment || !comment.fix_suggestions;
+  }
+
+  _handleDiscard(e) {
+    e.preventDefault();
+    this.$.reporting.recordDraftInteraction();
+
+    if (!this._messageText) {
+      this._discardDraft();
+      return;
     }
 
-    _commentMessageChanged(message) {
-      this._messageText = message || '';
+    this._openOverlay(this.confirmDiscardOverlay).then(() => {
+      this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
+          .resetFocus();
+    });
+  }
+
+  _handleConfirmDiscard(e) {
+    e.preventDefault();
+    const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
+    this._closeConfirmDiscardOverlay();
+    return this._discardDraft().then(() => { timer.end(); });
+  }
+
+  _discardDraft() {
+    if (!this.comment.__draft) {
+      throw Error('Cannot discard a non-draft comment.');
+    }
+    this.discarding = true;
+    this.editing = false;
+    this.disabled = true;
+    this._eraseDraftComment();
+
+    if (!this.comment.id) {
+      this.disabled = false;
+      this._fireDiscard();
+      return;
     }
 
-    _messageTextChanged(newValue, oldValue) {
-      if (!this.comment || (this.comment && this.comment.id)) {
-        return;
+    this._xhrPromise = this._deleteDraft(this.comment).then(response => {
+      this.disabled = false;
+      if (!response.ok) {
+        this.discarding = false;
+        return response;
       }
 
-      this.debounce('store', () => {
-        const message = this._messageText;
-        const commentLocation = {
-          changeNum: this.changeNum,
-          patchNum: this._getPatchNum(),
-          path: this.comment.path,
-          line: this.comment.line,
-          range: this.comment.range,
-        };
+      this._fireDiscard();
+    })
+        .catch(err => {
+          this.disabled = false;
+          throw err;
+        });
 
-        if ((!this._messageText || !this._messageText.length) && oldValue) {
-          // If the draft has been modified to be empty, then erase the storage
-          // entry.
-          this.$.storage.eraseDraftComment(commentLocation);
-        } else {
-          this.$.storage.setDraftComment(commentLocation, message);
-        }
-      }, STORAGE_DEBOUNCE_INTERVAL);
+    return this._xhrPromise;
+  }
+
+  _closeConfirmDiscardOverlay() {
+    this._closeOverlay(this.confirmDiscardOverlay);
+  }
+
+  _getSavingMessage(numPending) {
+    if (numPending === 0) {
+      return SAVED_MESSAGE;
     }
+    return [
+      SAVING_MESSAGE,
+      numPending,
+      numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
+    ].join(' ');
+  }
 
-    _handleAnchorClick(e) {
-      e.preventDefault();
-      if (!this.comment.line) {
-        return;
+  _showStartRequest() {
+    const numPending = ++this._numPendingDraftRequests.number;
+    this._updateRequestToast(numPending);
+  }
+
+  _showEndRequest() {
+    const numPending = --this._numPendingDraftRequests.number;
+    this._updateRequestToast(numPending);
+  }
+
+  _handleFailedDraftRequest() {
+    this._numPendingDraftRequests.number--;
+
+    // Cancel the debouncer so that error toasts from the error-manager will
+    // not be overridden.
+    this.cancelDebouncer('draft-toast');
+  }
+
+  _updateRequestToast(numPending) {
+    const message = this._getSavingMessage(numPending);
+    this.debounce('draft-toast', () => {
+      // Note: the event is fired on the body rather than this element because
+      // this element may not be attached by the time this executes, in which
+      // case the event would not bubble.
+      document.body.dispatchEvent(new CustomEvent(
+          'show-alert', {detail: {message}, bubbles: true, composed: true}));
+    }, TOAST_DEBOUNCE_INTERVAL);
+  }
+
+  _saveDraft(draft) {
+    this._showStartRequest();
+    return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
+        .then(result => {
+          if (result.ok) {
+            this._showEndRequest();
+          } else {
+            this._handleFailedDraftRequest();
+          }
+          return result;
+        });
+  }
+
+  _deleteDraft(draft) {
+    this._showStartRequest();
+    return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
+        draft).then(result => {
+      if (result.ok) {
+        this._showEndRequest();
+      } else {
+        this._handleFailedDraftRequest();
       }
-      this.dispatchEvent(new CustomEvent('comment-anchor-tap', {
-        bubbles: true,
-        composed: true,
-        detail: {
-          number: this.comment.line || FILE,
-          side: this.side,
-        },
-      }));
+      return result;
+    });
+  }
+
+  _getPatchNum() {
+    return this.isOnParent() ? 'PARENT' : this.patchNum;
+  }
+
+  _loadLocalDraft(changeNum, patchNum, comment) {
+    // Polymer 2: check for undefined
+    if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
+      return;
     }
 
-    _handleEdit(e) {
-      e.preventDefault();
-      this._messageText = this.comment.message;
-      this.editing = true;
-      this.$.reporting.recordDraftInteraction();
+    // Only apply local drafts to comments that haven't been saved
+    // remotely, and haven't been given a default message already.
+    //
+    // Don't get local draft if there is another comment that is currently
+    // in an editing state.
+    if (!comment || comment.id || comment.message || comment.__otherEditing) {
+      delete comment.__otherEditing;
+      return;
     }
 
-    _handleSave(e) {
-      e.preventDefault();
+    const draft = this.$.storage.getDraftComment({
+      changeNum,
+      patchNum: this._getPatchNum(),
+      path: comment.path,
+      line: comment.line,
+      range: comment.range,
+    });
 
-      // Ignore saves started while already saving.
-      if (this.disabled) {
-        return;
-      }
-      const timingLabel = this.comment.id ?
-        REPORT_UPDATE_DRAFT : REPORT_CREATE_DRAFT;
-      const timer = this.$.reporting.getTimer(timingLabel);
-      this.set('comment.__editing', false);
-      return this.save().then(() => { timer.end(); });
-    }
-
-    _handleCancel(e) {
-      e.preventDefault();
-
-      if (!this.comment.message ||
-          this.comment.message.trim().length === 0 ||
-          !this.comment.id) {
-        this._fireDiscard();
-        return;
-      }
-      this._messageText = this.comment.message;
-      this.editing = false;
-    }
-
-    _fireDiscard() {
-      this.cancelDebouncer('fire-update');
-      this.fire('comment-discard', this._getEventPayload());
-    }
-
-    _handleFix() {
-      this.dispatchEvent(new CustomEvent('create-fix-comment', {
-        bubbles: true,
-        composed: true,
-        detail: this._getEventPayload(),
-      }));
-    }
-
-    _handleShowFix() {
-      this.dispatchEvent(new CustomEvent('open-fix-preview', {
-        bubbles: true,
-        composed: true,
-        detail: this._getEventPayload(),
-      }));
-    }
-
-    _hasNoFix(comment) {
-      return !comment || !comment.fix_suggestions;
-    }
-
-    _handleDiscard(e) {
-      e.preventDefault();
-      this.$.reporting.recordDraftInteraction();
-
-      if (!this._messageText) {
-        this._discardDraft();
-        return;
-      }
-
-      this._openOverlay(this.confirmDiscardOverlay).then(() => {
-        this.confirmDiscardOverlay.querySelector('#confirmDiscardDialog')
-            .resetFocus();
-      });
-    }
-
-    _handleConfirmDiscard(e) {
-      e.preventDefault();
-      const timer = this.$.reporting.getTimer(REPORT_DISCARD_DRAFT);
-      this._closeConfirmDiscardOverlay();
-      return this._discardDraft().then(() => { timer.end(); });
-    }
-
-    _discardDraft() {
-      if (!this.comment.__draft) {
-        throw Error('Cannot discard a non-draft comment.');
-      }
-      this.discarding = true;
-      this.editing = false;
-      this.disabled = true;
-      this._eraseDraftComment();
-
-      if (!this.comment.id) {
-        this.disabled = false;
-        this._fireDiscard();
-        return;
-      }
-
-      this._xhrPromise = this._deleteDraft(this.comment).then(response => {
-        this.disabled = false;
-        if (!response.ok) {
-          this.discarding = false;
-          return response;
-        }
-
-        this._fireDiscard();
-      })
-          .catch(err => {
-            this.disabled = false;
-            throw err;
-          });
-
-      return this._xhrPromise;
-    }
-
-    _closeConfirmDiscardOverlay() {
-      this._closeOverlay(this.confirmDiscardOverlay);
-    }
-
-    _getSavingMessage(numPending) {
-      if (numPending === 0) {
-        return SAVED_MESSAGE;
-      }
-      return [
-        SAVING_MESSAGE,
-        numPending,
-        numPending === 1 ? DRAFT_SINGULAR : DRAFT_PLURAL,
-      ].join(' ');
-    }
-
-    _showStartRequest() {
-      const numPending = ++this._numPendingDraftRequests.number;
-      this._updateRequestToast(numPending);
-    }
-
-    _showEndRequest() {
-      const numPending = --this._numPendingDraftRequests.number;
-      this._updateRequestToast(numPending);
-    }
-
-    _handleFailedDraftRequest() {
-      this._numPendingDraftRequests.number--;
-
-      // Cancel the debouncer so that error toasts from the error-manager will
-      // not be overridden.
-      this.cancelDebouncer('draft-toast');
-    }
-
-    _updateRequestToast(numPending) {
-      const message = this._getSavingMessage(numPending);
-      this.debounce('draft-toast', () => {
-        // Note: the event is fired on the body rather than this element because
-        // this element may not be attached by the time this executes, in which
-        // case the event would not bubble.
-        document.body.dispatchEvent(new CustomEvent(
-            'show-alert', {detail: {message}, bubbles: true, composed: true}));
-      }, TOAST_DEBOUNCE_INTERVAL);
-    }
-
-    _saveDraft(draft) {
-      this._showStartRequest();
-      return this.$.restAPI.saveDiffDraft(this.changeNum, this.patchNum, draft)
-          .then(result => {
-            if (result.ok) {
-              this._showEndRequest();
-            } else {
-              this._handleFailedDraftRequest();
-            }
-            return result;
-          });
-    }
-
-    _deleteDraft(draft) {
-      this._showStartRequest();
-      return this.$.restAPI.deleteDiffDraft(this.changeNum, this.patchNum,
-          draft).then(result => {
-        if (result.ok) {
-          this._showEndRequest();
-        } else {
-          this._handleFailedDraftRequest();
-        }
-        return result;
-      });
-    }
-
-    _getPatchNum() {
-      return this.isOnParent() ? 'PARENT' : this.patchNum;
-    }
-
-    _loadLocalDraft(changeNum, patchNum, comment) {
-      // Polymer 2: check for undefined
-      if ([changeNum, patchNum, comment].some(arg => arg === undefined)) {
-        return;
-      }
-
-      // Only apply local drafts to comments that haven't been saved
-      // remotely, and haven't been given a default message already.
-      //
-      // Don't get local draft if there is another comment that is currently
-      // in an editing state.
-      if (!comment || comment.id || comment.message || comment.__otherEditing) {
-        delete comment.__otherEditing;
-        return;
-      }
-
-      const draft = this.$.storage.getDraftComment({
-        changeNum,
-        patchNum: this._getPatchNum(),
-        path: comment.path,
-        line: comment.line,
-        range: comment.range,
-      });
-
-      if (draft) {
-        this.set('comment.message', draft.message);
-      }
-    }
-
-    _handleToggleResolved() {
-      this.$.reporting.recordDraftInteraction();
-      this.resolved = !this.resolved;
-      // Modify payload instead of this.comment, as this.comment is passed from
-      // the parent by ref.
-      const payload = this._getEventPayload();
-      payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
-      this.fire('comment-update', payload);
-      if (!this.editing) {
-        // Save the resolved state immediately.
-        this.save(payload.comment);
-      }
-    }
-
-    _handleCommentDelete() {
-      this._openOverlay(this.confirmDeleteOverlay);
-    }
-
-    _handleCancelDeleteComment() {
-      this._closeOverlay(this.confirmDeleteOverlay);
-    }
-
-    _openOverlay(overlay) {
-      Polymer.dom(Gerrit.getRootElement()).appendChild(overlay);
-      return overlay.open();
-    }
-
-    _computeAuthorName(comment) {
-      if (!comment) return '';
-      if (comment.robot_id) {
-        return comment.robot_id;
-      }
-      return comment.author && comment.author.name;
-    }
-
-    _computeHideRunDetails(comment, collapsed) {
-      if (!comment) return true;
-      return !(comment.robot_id && comment.url && !collapsed);
-    }
-
-    _closeOverlay(overlay) {
-      Polymer.dom(Gerrit.getRootElement()).removeChild(overlay);
-      overlay.close();
-    }
-
-    _handleConfirmDeleteComment() {
-      const dialog =
-          this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
-      this.$.restAPI.deleteComment(
-          this.changeNum, this.patchNum, this.comment.id, dialog.message)
-          .then(newComment => {
-            this._handleCancelDeleteComment();
-            this.comment = newComment;
-          });
+    if (draft) {
+      this.set('comment.message', draft.message);
     }
   }
 
-  customElements.define(GrComment.is, GrComment);
-})();
+  _handleToggleResolved() {
+    this.$.reporting.recordDraftInteraction();
+    this.resolved = !this.resolved;
+    // Modify payload instead of this.comment, as this.comment is passed from
+    // the parent by ref.
+    const payload = this._getEventPayload();
+    payload.comment.unresolved = !this.$.resolvedCheckbox.checked;
+    this.fire('comment-update', payload);
+    if (!this.editing) {
+      // Save the resolved state immediately.
+      this.save(payload.comment);
+    }
+  }
+
+  _handleCommentDelete() {
+    this._openOverlay(this.confirmDeleteOverlay);
+  }
+
+  _handleCancelDeleteComment() {
+    this._closeOverlay(this.confirmDeleteOverlay);
+  }
+
+  _openOverlay(overlay) {
+    dom(Gerrit.getRootElement()).appendChild(overlay);
+    return overlay.open();
+  }
+
+  _computeAuthorName(comment) {
+    if (!comment) return '';
+    if (comment.robot_id) {
+      return comment.robot_id;
+    }
+    return comment.author && comment.author.name;
+  }
+
+  _computeHideRunDetails(comment, collapsed) {
+    if (!comment) return true;
+    return !(comment.robot_id && comment.url && !collapsed);
+  }
+
+  _closeOverlay(overlay) {
+    dom(Gerrit.getRootElement()).removeChild(overlay);
+    overlay.close();
+  }
+
+  _handleConfirmDeleteComment() {
+    const dialog =
+        this.confirmDeleteOverlay.querySelector('#confirmDeleteComment');
+    this.$.restAPI.deleteComment(
+        this.changeNum, this.patchNum, this.comment.id, dialog.message)
+        .then(newComment => {
+          this._handleCancelDeleteComment();
+          this.comment = newComment;
+        });
+  }
+}
+
+customElements.define(GrComment.is, GrComment);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
index 18ffc0e..4a0f388 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_html.js
@@ -1,43 +1,22 @@
-<!--
-@license
-Copyright (C) 2015 The Android Open Source Project
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="/bower_components/polymer/polymer.html">
-<link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
-<link rel="import" href="/bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
-<link rel="import" href="../../../behaviors/fire-behavior/fire-behavior.html">
-<link rel="import" href="../../../styles/shared-styles.html">
-<link rel="import" href="../../core/gr-reporting/gr-reporting.html">
-<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
-<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.html">
-<link rel="import" href="../../shared/gr-button/gr-button.html">
-<link rel="import" href="../../shared/gr-dialog/gr-dialog.html">
-<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
-<link rel="import" href="../../shared/gr-formatted-text/gr-formatted-text.html">
-<link rel="import" href="../../shared/gr-icons/gr-icons.html">
-<link rel="import" href="../../shared/gr-overlay/gr-overlay.html">
-<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
-<link rel="import" href="../../shared/gr-storage/gr-storage.html">
-<link rel="import" href="../../shared/gr-textarea/gr-textarea.html">
-<link rel="import" href="../../shared/gr-tooltip-content/gr-tooltip-content.html">
-<link rel="import" href="../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog.html">
-<script src="../../../scripts/rootElement.js"></script>
-
-<dom-module id="gr-comment">
-  <template>
+export const htmlTemplate = html`
     <style include="shared-styles">
       :host {
         display: block;
@@ -257,144 +236,85 @@
         <div class="headerLeft">
           <span class="authorName">[[_computeAuthorName(comment)]]</span>
           <span class="draftLabel">DRAFT</span>
-          <gr-tooltip-content class="draftTooltip"
-              has-tooltip
-              title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key."
-              max-width="20em"
-              show-icon></gr-tooltip-content>
+          <gr-tooltip-content class="draftTooltip" has-tooltip="" title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'A' key." max-width="20em" show-icon=""></gr-tooltip-content>
         </div>
         <div class="headerMiddle">
           <span class="collapsedContent">[[comment.message]]</span>
         </div>
-        <div hidden$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
+        <div hidden\$="[[_computeHideRunDetails(comment, collapsed)]]" class="runIdMessage message">
           <div class="runIdInformation">
-            <a class="robotRunLink" href$="[[comment.url]]">
+            <a class="robotRunLink" href\$="[[comment.url]]">
               <span class="robotRun link">Run Details</span>
             </a>
           </div>
         </div>
-        <gr-button
-            id="deleteBtn"
-            link
-            class$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]"
-            hidden$="[[isRobotComment]]"
-            on-click="_handleCommentDelete">
+        <gr-button id="deleteBtn" link="" class\$="action delete [[_computeDeleteButtonClass(_isAdmin, draft)]]" hidden\$="[[isRobotComment]]" on-click="_handleCommentDelete">
           <iron-icon id="icon" icon="gr-icons:delete"></iron-icon>
         </gr-button>
         <span class="date" on-click="_handleAnchorClick">
-          <gr-date-formatter
-              has-tooltip
-              date-str="[[comment.updated]]"></gr-date-formatter>
+          <gr-date-formatter has-tooltip="" date-str="[[comment.updated]]"></gr-date-formatter>
         </span>
         <div class="show-hide">
           <label class="show-hide">
-            <input type="checkbox" class="show-hide"
-               checked$="[[collapsed]]"
-               on-change="_handleToggleCollapsed">
-            <iron-icon
-                id="icon"
-                icon="[[_computeShowHideIcon(collapsed)]]">
+            <input type="checkbox" class="show-hide" checked\$="[[collapsed]]" on-change="_handleToggleCollapsed">
+            <iron-icon id="icon" icon="[[_computeShowHideIcon(collapsed)]]">
             </iron-icon>
           </label>
         </div>
       </div>
       <div class="body">
         <template is="dom-if" if="[[isRobotComment]]">
-          <div class="robotId" hidden$="[[collapsed]]">
+          <div class="robotId" hidden\$="[[collapsed]]">
             [[comment.author.name]]
           </div>
         </template>
         <template is="dom-if" if="[[editing]]">
-          <gr-textarea
-              id="editTextarea"
-              class="editMessage"
-              autocomplete="on"
-              code
-              disabled="{{disabled}}"
-              rows="4"
-              text="{{_messageText}}"></gr-textarea>
+          <gr-textarea id="editTextarea" class="editMessage" autocomplete="on" code="" disabled="{{disabled}}" rows="4" text="{{_messageText}}"></gr-textarea>
           <template is="dom-if" if="[[_computeVisibilityOfTip(_showRespectfulTip, _respectfulTipDismissed)]]">
             <div class="respectfulReviewTip">
               <div>
-                <gr-tooltip-content
-                  has-tooltip
-                  title="Tips for respectful code reviews.">
+                <gr-tooltip-content has-tooltip="" title="Tips for respectful code reviews.">
                   <iron-icon class="pointer" icon="gr-icons:lightbulb-outline"></iron-icon>
                 </gr-tooltip-content>
                 [[_respectfulReviewTip]]
               </div>
               <div>
-                <a
-                  tabIndex="-1"
-                  on-click="_onRespectfulReadMoreClick"
-                  href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html"
-                  target="_blank">
+                <a tabindex="-1" on-click="_onRespectfulReadMoreClick" href="https://testing.googleblog.com/2019/11/code-health-respectful-reviews-useful.html" target="_blank">
                   Read more
                 </a>
-                <iron-icon
-                  class="close pointer"
-                  on-click="_dismissRespectfulTip"
-                  icon="gr-icons:close"></iron-icon>
+                <iron-icon class="close pointer" on-click="_dismissRespectfulTip" icon="gr-icons:close"></iron-icon>
               </div>
             </div>
           </template>
         </template>
         <!--The message class is needed to ensure selectability from
         gr-diff-selection.-->
-        <gr-formatted-text class="message"
-            content="[[comment.message]]"
-            no-trailing-margin="[[!comment.__draft]]"
-            config="[[projectConfig.commentlinks]]"></gr-formatted-text>
-        <div class="actions humanActions" hidden$="[[!_showHumanActions]]">
+        <gr-formatted-text class="message" content="[[comment.message]]" no-trailing-margin="[[!comment.__draft]]" config="[[projectConfig.commentlinks]]"></gr-formatted-text>
+        <div class="actions humanActions" hidden\$="[[!_showHumanActions]]">
           <div class="action resolve hideOnPublished">
             <label>
-              <input type="checkbox"
-                  id="resolvedCheckbox"
-                  checked="[[resolved]]"
-                  on-change="_handleToggleResolved">
+              <input type="checkbox" id="resolvedCheckbox" checked="[[resolved]]" on-change="_handleToggleResolved">
               Resolved
             </label>
           </div>
           <div class="rightActions">
-            <gr-button
-                link
-                class="action cancel hideOnPublished"
-                on-click="_handleCancel">Cancel</gr-button>
-            <gr-button
-                link
-                class="action discard hideOnPublished"
-                on-click="_handleDiscard">Discard</gr-button>
-            <gr-button
-                link
-                class="action edit hideOnPublished"
-                on-click="_handleEdit">Edit</gr-button>
-            <gr-button
-                link
-                disabled$="[[_computeSaveDisabled(_messageText, comment, resolved)]]"
-                class="action save hideOnPublished"
-                on-click="_handleSave">Save</gr-button>
+            <gr-button link="" class="action cancel hideOnPublished" on-click="_handleCancel">Cancel</gr-button>
+            <gr-button link="" class="action discard hideOnPublished" on-click="_handleDiscard">Discard</gr-button>
+            <gr-button link="" class="action edit hideOnPublished" on-click="_handleEdit">Edit</gr-button>
+            <gr-button link="" disabled\$="[[_computeSaveDisabled(_messageText, comment, resolved)]]" class="action save hideOnPublished" on-click="_handleSave">Save</gr-button>
           </div>
         </div>
-        <div class="robotActions" hidden$="[[!_showRobotActions]]">
+        <div class="robotActions" hidden\$="[[!_showRobotActions]]">
           <template is="dom-if" if="[[isRobotComment]]">
             <gr-endpoint-decorator name="robot-comment-controls">
               <gr-endpoint-param name="comment" value="[[comment]]">
               </gr-endpoint-param>
             </gr-endpoint-decorator>
-            <gr-button
-                link
-                secondary
-                class="action show-fix"
-                hidden$="[[_hasNoFix(comment)]]"
-                on-click="_handleShowFix">
+            <gr-button link="" secondary="" class="action show-fix" hidden\$="[[_hasNoFix(comment)]]" on-click="_handleShowFix">
               Show Fix
             </gr-button>
             <template is="dom-if" if="[[!_hasHumanReply]]">
-              <gr-button
-                  link
-                  class="action fix"
-                  on-click="_handleFix"
-                  disabled="[[robotButtonDisabled]]">
+              <gr-button link="" class="action fix" on-click="_handleFix" disabled="[[robotButtonDisabled]]">
                 Please Fix
               </gr-button>
             </template>
@@ -403,19 +323,12 @@
       </div>
     </div>
     <template is="dom-if" if="[[_enableOverlay]]">
-      <gr-overlay id="confirmDeleteOverlay" with-backdrop>
-        <gr-confirm-delete-comment-dialog id="confirmDeleteComment"
-            on-confirm="_handleConfirmDeleteComment"
-            on-cancel="_handleCancelDeleteComment">
+      <gr-overlay id="confirmDeleteOverlay" with-backdrop="">
+        <gr-confirm-delete-comment-dialog id="confirmDeleteComment" on-confirm="_handleConfirmDeleteComment" on-cancel="_handleCancelDeleteComment">
         </gr-confirm-delete-comment-dialog>
       </gr-overlay>
-      <gr-overlay id="confirmDiscardOverlay" with-backdrop>
-        <gr-dialog
-            id="confirmDiscardDialog"
-            confirm-label="Discard"
-            confirm-on-enter
-            on-confirm="_handleConfirmDiscard"
-            on-cancel="_closeConfirmDiscardOverlay">
+      <gr-overlay id="confirmDiscardOverlay" with-backdrop="">
+        <gr-dialog id="confirmDiscardDialog" confirm-label="Discard" confirm-on-enter="" on-confirm="_handleConfirmDiscard" on-cancel="_closeConfirmDiscardOverlay">
           <div class="header" slot="header">
             Discard comment
           </div>
@@ -428,6 +341,4 @@
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-reporting id="reporting"></gr-reporting>
-  </template>
-  <script src="gr-comment.js"></script>
-</dom-module>
+`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
index 5e9d37a..96d497e 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.html
@@ -19,18 +19,24 @@
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 <title>gr-comment</title>
 
-<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 
-<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
-<script src="/bower_components/web-component-tester/browser.js"></script>
-<script src="../../../test/test-pre-setup.js"></script>
-<link rel="import" href="../../../test/common-test-setup.html"/>
-<script src="/bower_components/page/page.js"></script>
-<script src="../../../scripts/util.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+<script type="module" src="../../../test/test-pre-setup.js"></script>
+<script type="module" src="../../../test/common-test-setup.js"></script>
+<script src="/node_modules/page/page.js"></script>
+<script type="module" src="../../../scripts/util.js"></script>
 
-<link rel="import" href="gr-comment.html">
+<script type="module" src="./gr-comment.js"></script>
 
-<script>void(0);</script>
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment.js';
+void(0);
+</script>
 
 <test-fixture id="basic">
   <template>
@@ -44,1119 +50,1204 @@
   </template>
 </test-fixture>
 
-<script>
-  function isVisible(el) {
-    assert.ok(el);
-    return getComputedStyle(el).getPropertyValue('display') !== 'none';
-  }
+<script type="module">
+import '../../../test/test-pre-setup.js';
+import '../../../test/common-test-setup.js';
+import '../../../scripts/util.js';
+import './gr-comment.js';
+function isVisible(el) {
+  assert.ok(el);
+  return getComputedStyle(el).getPropertyValue('display') !== 'none';
+}
 
-  suite('gr-comment tests', async () => {
-    await readyToTest();
+suite('gr-comment tests', () => {
+  suite('basic tests', () => {
+    let element;
+    let sandbox;
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getAccount() { return Promise.resolve(null); },
+      });
+      element = fixture('basic');
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        id: 'baf0414d_60047215',
+        line: 5,
+        message: 'is this a crossover episode!?',
+        updated: '2015-12-08 19:48:33.843000000',
+      };
+      sandbox = sinon.sandbox.create();
+    });
 
-    suite('basic tests', () => {
-      let element;
-      let sandbox;
+    teardown(() => {
+      sandbox.restore();
+    });
+
+    test('collapsible comments', () => {
+      // When a comment (not draft) is loaded, it should be collapsed
+      assert.isTrue(element.collapsed);
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+
+      // The header middle content is only visible when comments are collapsed.
+      // It shows the message in a condensed way, and limits to a single line.
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is visible');
+
+      // When the header row is clicked, the comment should expand
+      MockInteractions.tap(element.$.header);
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is not visible');
+    });
+
+    test('clicking on date link fires event', () => {
+      element.side = 'PARENT';
+      const stub = sinon.stub();
+      element.addEventListener('comment-anchor-tap', stub);
+      const dateEl = element.shadowRoot
+          .querySelector('.date');
+      assert.ok(dateEl);
+      MockInteractions.tap(dateEl);
+
+      assert.isTrue(stub.called);
+      assert.deepEqual(stub.lastCall.args[0].detail,
+          {side: element.side, number: element.comment.line});
+    });
+
+    test('message is not retrieved from storage when other edits', done => {
+      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+      element.changeNum = 1;
+      element.patchNum = 1;
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        line: 5,
+        __otherEditing: true,
+      };
+      flush(() => {
+        assert.isTrue(loadSpy.called);
+        assert.isFalse(storageStub.called);
+        done();
+      });
+    });
+
+    test('message is retrieved from storage when no other edits', done => {
+      const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
+      const loadSpy = sandbox.spy(element, '_loadLocalDraft');
+
+      element.changeNum = 1;
+      element.patchNum = 1;
+      element.comment = {
+        author: {
+          name: 'Mr. Peanutbutter',
+          email: 'tenn1sballchaser@aol.com',
+        },
+        line: 5,
+      };
+      flush(() => {
+        assert.isTrue(loadSpy.called);
+        assert.isTrue(storageStub.called);
+        done();
+      });
+    });
+
+    test('_getPatchNum', () => {
+      element.side = 'PARENT';
+      element.patchNum = 1;
+      assert.equal(element._getPatchNum(), 'PARENT');
+      element.side = 'REVISION';
+      assert.equal(element._getPatchNum(), 1);
+    });
+
+    test('comment expand and collapse', () => {
+      element.collapsed = true;
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is visible');
+
+      element.collapsed = false;
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is is not visible');
+    });
+
+    suite('while editing', () => {
       setup(() => {
-        stub('gr-rest-api-interface', {
-          getAccount() { return Promise.resolve(null); },
-        });
-        element = fixture('basic');
-        element.comment = {
-          author: {
-            name: 'Mr. Peanutbutter',
-            email: 'tenn1sballchaser@aol.com',
-          },
-          id: 'baf0414d_60047215',
-          line: 5,
-          message: 'is this a crossover episode!?',
-          updated: '2015-12-08 19:48:33.843000000',
-        };
-        sandbox = sinon.sandbox.create();
+        element.editing = true;
+        element._messageText = 'test';
+        sandbox.stub(element, '_handleCancel');
+        sandbox.stub(element, '_handleSave');
+        flushAsynchronousOperations();
       });
 
-      teardown(() => {
-        sandbox.restore();
-      });
-
-      test('collapsible comments', () => {
-        // When a comment (not draft) is loaded, it should be collapsed
-        assert.isTrue(element.collapsed);
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are not visible');
-        assert.isNotOk(element.textarea, 'textarea is not visible');
-
-        // The header middle content is only visible when comments are collapsed.
-        // It shows the message in a condensed way, and limits to a single line.
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is visible');
-
-        // When the header row is clicked, the comment should expand
-        MockInteractions.tap(element.$.header);
-        assert.isFalse(element.collapsed);
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are visible');
-        assert.isNotOk(element.textarea, 'textarea is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is not visible');
-      });
-
-      test('clicking on date link fires event', () => {
-        element.side = 'PARENT';
-        const stub = sinon.stub();
-        element.addEventListener('comment-anchor-tap', stub);
-        const dateEl = element.shadowRoot
-            .querySelector('.date');
-        assert.ok(dateEl);
-        MockInteractions.tap(dateEl);
-
-        assert.isTrue(stub.called);
-        assert.deepEqual(stub.lastCall.args[0].detail,
-            {side: element.side, number: element.comment.line});
-      });
-
-      test('message is not retrieved from storage when other edits', done => {
-        const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
-        const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
-        element.changeNum = 1;
-        element.patchNum = 1;
-        element.comment = {
-          author: {
-            name: 'Mr. Peanutbutter',
-            email: 'tenn1sballchaser@aol.com',
-          },
-          line: 5,
-          __otherEditing: true,
-        };
-        flush(() => {
-          assert.isTrue(loadSpy.called);
-          assert.isFalse(storageStub.called);
-          done();
-        });
-      });
-
-      test('message is retrieved from storage when no other edits', done => {
-        const storageStub = sandbox.stub(element.$.storage, 'getDraftComment');
-        const loadSpy = sandbox.spy(element, '_loadLocalDraft');
-
-        element.changeNum = 1;
-        element.patchNum = 1;
-        element.comment = {
-          author: {
-            name: 'Mr. Peanutbutter',
-            email: 'tenn1sballchaser@aol.com',
-          },
-          line: 5,
-        };
-        flush(() => {
-          assert.isTrue(loadSpy.called);
-          assert.isTrue(storageStub.called);
-          done();
-        });
-      });
-
-      test('_getPatchNum', () => {
-        element.side = 'PARENT';
-        element.patchNum = 1;
-        assert.equal(element._getPatchNum(), 'PARENT');
-        element.side = 'REVISION';
-        assert.equal(element._getPatchNum(), 1);
-      });
-
-      test('comment expand and collapse', () => {
-        element.collapsed = true;
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are not visible');
-        assert.isNotOk(element.textarea, 'textarea is not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is visible');
-
-        element.collapsed = false;
-        assert.isFalse(element.collapsed);
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are visible');
-        assert.isNotOk(element.textarea, 'textarea is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is is not visible');
-      });
-
-      suite('while editing', () => {
+      suite('when text is empty', () => {
         setup(() => {
-          element.editing = true;
-          element._messageText = 'test';
-          sandbox.stub(element, '_handleCancel');
-          sandbox.stub(element, '_handleSave');
-          flushAsynchronousOperations();
+          element._messageText = '';
+          element.comment = {};
         });
 
-        suite('when text is empty', () => {
-          setup(() => {
-            element._messageText = '';
-            element.comment = {};
-          });
-
-          test('esc closes comment when text is empty', () => {
-            MockInteractions.pressAndReleaseKeyOn(
-                element.textarea, 27); // esc
-            assert.isTrue(element._handleCancel.called);
-          });
-
-          test('ctrl+enter does not save', () => {
-            MockInteractions.pressAndReleaseKeyOn(
-                element.textarea, 13, 'ctrl'); // ctrl + enter
-            assert.isFalse(element._handleSave.called);
-          });
-
-          test('meta+enter does not save', () => {
-            MockInteractions.pressAndReleaseKeyOn(
-                element.textarea, 13, 'meta'); // meta + enter
-            assert.isFalse(element._handleSave.called);
-          });
-
-          test('ctrl+s does not save', () => {
-            MockInteractions.pressAndReleaseKeyOn(
-                element.textarea, 83, 'ctrl'); // ctrl + s
-            assert.isFalse(element._handleSave.called);
-          });
-        });
-
-        test('esc does not close comment that has content', () => {
+        test('esc closes comment when text is empty', () => {
           MockInteractions.pressAndReleaseKeyOn(
               element.textarea, 27); // esc
-          assert.isFalse(element._handleCancel.called);
+          assert.isTrue(element._handleCancel.called);
         });
 
-        test('ctrl+enter saves', () => {
+        test('ctrl+enter does not save', () => {
           MockInteractions.pressAndReleaseKeyOn(
               element.textarea, 13, 'ctrl'); // ctrl + enter
-          assert.isTrue(element._handleSave.called);
+          assert.isFalse(element._handleSave.called);
         });
 
-        test('meta+enter saves', () => {
+        test('meta+enter does not save', () => {
           MockInteractions.pressAndReleaseKeyOn(
               element.textarea, 13, 'meta'); // meta + enter
-          assert.isTrue(element._handleSave.called);
+          assert.isFalse(element._handleSave.called);
         });
 
-        test('ctrl+s saves', () => {
+        test('ctrl+s does not save', () => {
           MockInteractions.pressAndReleaseKeyOn(
               element.textarea, 83, 'ctrl'); // ctrl + s
-          assert.isTrue(element._handleSave.called);
-        });
-      });
-      test('delete comment button for non-admins is hidden', () => {
-        element._isAdmin = false;
-        assert.isFalse(element.shadowRoot
-            .querySelector('.action.delete')
-            .classList.contains('showDeleteButtons'));
-      });
-
-      test('delete comment button for admins with draft is hidden', () => {
-        element._isAdmin = false;
-        element.draft = true;
-        assert.isFalse(element.shadowRoot
-            .querySelector('.action.delete')
-            .classList.contains('showDeleteButtons'));
-      });
-
-      test('delete comment', done => {
-        sandbox.stub(
-            element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
-        sandbox.spy(element.confirmDeleteOverlay, 'open');
-        element.changeNum = 42;
-        element.patchNum = 0xDEADBEEF;
-        element._isAdmin = true;
-        assert.isTrue(element.shadowRoot
-            .querySelector('.action.delete')
-            .classList.contains('showDeleteButtons'));
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.action.delete'));
-        flush(() => {
-          element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
-            const dialog =
-                window.confirmDeleteOverlay
-                    .querySelector('#confirmDeleteComment');
-            dialog.message = 'removal reason';
-            element._handleConfirmDeleteComment();
-            assert.isTrue(element.$.restAPI.deleteComment.calledWith(
-                42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
-            done();
-          });
+          assert.isFalse(element._handleSave.called);
         });
       });
 
-      suite('draft update reporting', () => {
-        let endStub;
-        let getTimerStub;
-        let mockEvent;
-
-        setup(() => {
-          mockEvent = {preventDefault() {}};
-          sandbox.stub(element, 'save')
-              .returns(Promise.resolve({}));
-          sandbox.stub(element, '_discardDraft')
-              .returns(Promise.resolve({}));
-          endStub = sinon.stub();
-          getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
-              .returns({end: endStub});
-        });
-
-        test('create', () => {
-          element.comment = {};
-          return element._handleSave(mockEvent).then(() => {
-            assert.isTrue(endStub.calledOnce);
-            assert.isTrue(getTimerStub.calledOnce);
-            assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
-          });
-        });
-
-        test('update', () => {
-          element.comment = {id: 'abc_123'};
-          return element._handleSave(mockEvent).then(() => {
-            assert.isTrue(endStub.calledOnce);
-            assert.isTrue(getTimerStub.calledOnce);
-            assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
-          });
-        });
-
-        test('discard', () => {
-          element.comment = {id: 'abc_123'};
-          sandbox.stub(element, '_closeConfirmDiscardOverlay');
-          return element._handleConfirmDiscard(mockEvent).then(() => {
-            assert.isTrue(endStub.calledOnce);
-            assert.isTrue(getTimerStub.calledOnce);
-            assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
-          });
-        });
+      test('esc does not close comment that has content', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 27); // esc
+        assert.isFalse(element._handleCancel.called);
       });
 
-      test('edit reports interaction', () => {
-        const reportStub = sandbox.stub(element.$.reporting,
-            'recordDraftInteraction');
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.edit'));
-        assert.isTrue(reportStub.calledOnce);
+      test('ctrl+enter saves', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 13, 'ctrl'); // ctrl + enter
+        assert.isTrue(element._handleSave.called);
       });
 
-      test('discard reports interaction', () => {
-        const reportStub = sandbox.stub(element.$.reporting,
-            'recordDraftInteraction');
-        element.draft = true;
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.discard'));
-        assert.isTrue(reportStub.calledOnce);
+      test('meta+enter saves', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 13, 'meta'); // meta + enter
+        assert.isTrue(element._handleSave.called);
+      });
+
+      test('ctrl+s saves', () => {
+        MockInteractions.pressAndReleaseKeyOn(
+            element.textarea, 83, 'ctrl'); // ctrl + s
+        assert.isTrue(element._handleSave.called);
+      });
+    });
+    test('delete comment button for non-admins is hidden', () => {
+      element._isAdmin = false;
+      assert.isFalse(element.shadowRoot
+          .querySelector('.action.delete')
+          .classList.contains('showDeleteButtons'));
+    });
+
+    test('delete comment button for admins with draft is hidden', () => {
+      element._isAdmin = false;
+      element.draft = true;
+      assert.isFalse(element.shadowRoot
+          .querySelector('.action.delete')
+          .classList.contains('showDeleteButtons'));
+    });
+
+    test('delete comment', done => {
+      sandbox.stub(
+          element.$.restAPI, 'deleteComment').returns(Promise.resolve({}));
+      sandbox.spy(element.confirmDeleteOverlay, 'open');
+      element.changeNum = 42;
+      element.patchNum = 0xDEADBEEF;
+      element._isAdmin = true;
+      assert.isTrue(element.shadowRoot
+          .querySelector('.action.delete')
+          .classList.contains('showDeleteButtons'));
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.action.delete'));
+      flush(() => {
+        element.confirmDeleteOverlay.open.lastCall.returnValue.then(() => {
+          const dialog =
+              window.confirmDeleteOverlay
+                  .querySelector('#confirmDeleteComment');
+          dialog.message = 'removal reason';
+          element._handleConfirmDeleteComment();
+          assert.isTrue(element.$.restAPI.deleteComment.calledWith(
+              42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
+          done();
+        });
       });
     });
 
-    suite('gr-comment draft tests', () => {
-      let element;
-      let sandbox;
+    suite('draft update reporting', () => {
+      let endStub;
+      let getTimerStub;
+      let mockEvent;
 
       setup(() => {
-        stub('gr-rest-api-interface', {
-          getAccount() { return Promise.resolve(null); },
-          saveDiffDraft() {
-            return Promise.resolve({
-              ok: true,
-              text() {
-                return Promise.resolve(
-                    ')]}\'\n{' +
-                    '"id": "baf0414d_40572e03",' +
-                    '"path": "/path/to/file",' +
-                    '"line": 5,' +
-                    '"updated": "2015-12-08 21:52:36.177000000",' +
-                    '"message": "saved!"' +
-                  '}'
-                );
-              },
-            });
-          },
-          removeChangeReviewer() {
-            return Promise.resolve({ok: true});
-          },
-        });
-        stub('gr-storage', {
-          getDraftComment() { return null; },
-        });
-        element = fixture('draft');
-        element.changeNum = 42;
-        element.patchNum = 1;
-        element.editing = false;
-        element.comment = {
-          __commentSide: 'right',
-          __draft: true,
-          __draftID: 'temp_draft_id',
-          path: '/path/to/file',
-          line: 5,
-        };
-        element.commentSide = 'right';
-        sandbox = sinon.sandbox.create();
-      });
-
-      teardown(() => {
-        sandbox.restore();
-      });
-
-      test('button visibility states', () => {
-        element.showActions = false;
-        assert.isTrue(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        element.showActions = true;
-        assert.isFalse(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        element.draft = true;
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.edit')), 'edit is visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.discard')), 'discard is visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.save')), 'save is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.cancel')), 'cancel is not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.resolve')), 'resolve is visible');
-        assert.isFalse(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        element.editing = true;
-        flushAsynchronousOperations();
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.edit')), 'edit is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.discard')), 'discard not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.save')), 'save is visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.cancel')), 'cancel is visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.resolve')), 'resolve is visible');
-        assert.isFalse(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        element.draft = false;
-        element.editing = false;
-        flushAsynchronousOperations();
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.edit')), 'edit is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.discard')),
-        'discard is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.save')), 'save is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.cancel')), 'cancel is not visible');
-        assert.isFalse(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        element.comment.id = 'foo';
-        element.draft = true;
-        element.editing = true;
-        flushAsynchronousOperations();
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.cancel')), 'cancel is visible');
-        assert.isFalse(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        // Delete button is not hidden by default
-        assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
-
-        element.isRobotComment = true;
-        element.draft = true;
-        assert.isTrue(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isFalse(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        // It is not expected to see Robot comment drafts, but if they appear,
-        // they will behave the same as non-drafts.
-        element.draft = false;
-        assert.isTrue(element.shadowRoot
-            .querySelector('.humanActions').hasAttribute('hidden'));
-        assert.isFalse(element.shadowRoot
-            .querySelector('.robotActions').hasAttribute('hidden'));
-
-        // A robot comment with run ID should display plain text.
-        element.set(['comment', 'robot_run_id'], 'text');
-        element.editing = false;
-        element.collapsed = false;
-        flushAsynchronousOperations();
-        assert.isTrue(element.shadowRoot
-            .querySelector('.robotRun.link').textContent === 'Run Details');
-
-        // A robot comment with run ID and url should display a link.
-        element.set(['comment', 'url'], '/path/to/run');
-        flushAsynchronousOperations();
-        assert.notEqual(getComputedStyle(element.shadowRoot
-            .querySelector('.robotRun.link')).display,
-        'none');
-
-        // Delete button is hidden for robot comments
-        assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
-      });
-
-      test('collapsible drafts', () => {
-        assert.isTrue(element.collapsed);
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are not visible');
-        assert.isNotOk(element.textarea, 'textarea is not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is visible');
-
-        MockInteractions.tap(element.$.header);
-        assert.isFalse(element.collapsed);
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are visible');
-        assert.isNotOk(element.textarea, 'textarea is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is is not visible');
-
-        // When the edit button is pressed, should still see the actions
-        // and also textarea
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.edit'));
-        flushAsynchronousOperations();
-        assert.isFalse(element.collapsed);
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are visible');
-        assert.isTrue(isVisible(element.textarea), 'textarea is visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is not visible');
-
-        // When toggle again, everything should be hidden except for textarea
-        // and header middle content should be visible
-        MockInteractions.tap(element.$.header);
-        assert.isTrue(element.collapsed);
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are not visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-textarea')),
-        'textarea is not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is visible');
-
-        // When toggle again, textarea should remain open in the state it was
-        // before
-        MockInteractions.tap(element.$.header);
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('gr-formatted-text')),
-        'gr-formatted-text is not visible');
-        assert.isTrue(isVisible(element.shadowRoot
-            .querySelector('.actions')),
-        'actions are visible');
-        assert.isTrue(isVisible(element.textarea), 'textarea is visible');
-        assert.isFalse(isVisible(element.shadowRoot
-            .querySelector('.collapsedContent')),
-        'header middle content is not visible');
-      });
-
-      test('robot comment layout', () => {
-        const comment = Object.assign({
-          robot_id: 'happy_robot_id',
-          url: '/robot/comment',
-          author: {
-            name: 'Happy Robot',
-          },
-        }, element.comment);
-        element.comment = comment;
-        element.collapsed = false;
-        flushAsynchronousOperations();
-
-        let runIdMessage;
-        runIdMessage = element.shadowRoot
-            .querySelector('.runIdMessage');
-        assert.isFalse(runIdMessage.hidden);
-
-        const runDetailsLink = element.shadowRoot
-            .querySelector('.robotRunLink');
-        assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
-
-        const robotServiceName = element.shadowRoot
-            .querySelector('.authorName');
-        assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
-
-        const authorName = element.shadowRoot
-            .querySelector('.robotId');
-        assert.isTrue(authorName.innerText === 'Happy Robot');
-
-        element.collapsed = true;
-        flushAsynchronousOperations();
-        runIdMessage = element.shadowRoot
-            .querySelector('.runIdMessage');
-        assert.isTrue(runIdMessage.hidden);
-      });
-
-      test('draft creation/cancellation', done => {
-        assert.isFalse(element.editing);
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.edit'));
-        assert.isTrue(element.editing);
-
-        element._messageText = '';
-        const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-
-        // Save should be disabled on an empty message.
-        let disabled = element.shadowRoot
-            .querySelector('.save').hasAttribute('disabled');
-        assert.isTrue(disabled, 'save button should be disabled.');
-        element._messageText = '     ';
-        disabled = element.shadowRoot
-            .querySelector('.save').hasAttribute('disabled');
-        assert.isTrue(disabled, 'save button should be disabled.');
-
-        const updateStub = sinon.stub();
-        element.addEventListener('comment-update', updateStub);
-
-        let numDiscardEvents = 0;
-        element.addEventListener('comment-discard', e => {
-          numDiscardEvents++;
-          assert.isFalse(eraseMessageDraftSpy.called);
-          if (numDiscardEvents === 2) {
-            assert.isFalse(updateStub.called);
-            done();
-          }
-        });
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.cancel'));
-        element.flushDebouncer('fire-update');
-        element._messageText = '';
-        flushAsynchronousOperations();
-        MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
-      });
-
-      test('draft discard removes message from storage', done => {
-        element._messageText = '';
-        const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
-        sandbox.stub(element, '_closeConfirmDiscardOverlay');
-
-        element.addEventListener('comment-discard', e => {
-          assert.isTrue(eraseMessageDraftSpy.called);
-          done();
-        });
-        element._handleConfirmDiscard({preventDefault: sinon.stub()});
-      });
-
-      test('storage is cleared only after save success', () => {
-        element._messageText = 'test';
-        const eraseStub = sandbox.stub(element, '_eraseDraftComment');
-        sandbox.stub(element.$.restAPI, 'getResponseObject')
+        mockEvent = {preventDefault() {}};
+        sandbox.stub(element, 'save')
             .returns(Promise.resolve({}));
+        sandbox.stub(element, '_discardDraft')
+            .returns(Promise.resolve({}));
+        endStub = sinon.stub();
+        getTimerStub = sandbox.stub(element.$.reporting, 'getTimer')
+            .returns({end: endStub});
+      });
 
-        sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+      test('create', () => {
+        element.comment = {};
+        return element._handleSave(mockEvent).then(() => {
+          assert.isTrue(endStub.calledOnce);
+          assert.isTrue(getTimerStub.calledOnce);
+          assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
+        });
+      });
 
-        const savePromise = element.save();
-        assert.isFalse(eraseStub.called);
-        return savePromise.then(() => {
-          assert.isFalse(eraseStub.called);
+      test('update', () => {
+        element.comment = {id: 'abc_123'};
+        return element._handleSave(mockEvent).then(() => {
+          assert.isTrue(endStub.calledOnce);
+          assert.isTrue(getTimerStub.calledOnce);
+          assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
+        });
+      });
 
-          element._saveDraft.restore();
-          sandbox.stub(element, '_saveDraft')
-              .returns(Promise.resolve({ok: true}));
-          return element.save().then(() => {
-            assert.isTrue(eraseStub.called);
+      test('discard', () => {
+        element.comment = {id: 'abc_123'};
+        sandbox.stub(element, '_closeConfirmDiscardOverlay');
+        return element._handleConfirmDiscard(mockEvent).then(() => {
+          assert.isTrue(endStub.calledOnce);
+          assert.isTrue(getTimerStub.calledOnce);
+          assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
+        });
+      });
+    });
+
+    test('edit reports interaction', () => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.edit'));
+      assert.isTrue(reportStub.calledOnce);
+    });
+
+    test('discard reports interaction', () => {
+      const reportStub = sandbox.stub(element.$.reporting,
+          'recordDraftInteraction');
+      element.draft = true;
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.discard'));
+      assert.isTrue(reportStub.calledOnce);
+    });
+  });
+
+  suite('gr-comment draft tests', () => {
+    let element;
+    let sandbox;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getAccount() { return Promise.resolve(null); },
+        saveDiffDraft() {
+          return Promise.resolve({
+            ok: true,
+            text() {
+              return Promise.resolve(
+                  ')]}\'\n{' +
+                  '"id": "baf0414d_40572e03",' +
+                  '"path": "/path/to/file",' +
+                  '"line": 5,' +
+                  '"updated": "2015-12-08 21:52:36.177000000",' +
+                  '"message": "saved!"' +
+                '}'
+              );
+            },
           });
-        });
+        },
+        removeChangeReviewer() {
+          return Promise.resolve({ok: true});
+        },
       });
-
-      test('_computeSaveDisabled', () => {
-        const comment = {unresolved: true};
-        const msgComment = {message: 'test', unresolved: true};
-        assert.equal(element._computeSaveDisabled('', comment, false), true);
-        assert.equal(element._computeSaveDisabled('test', comment, false), false);
-        assert.equal(element._computeSaveDisabled('', msgComment, false), true);
-        assert.equal(
-            element._computeSaveDisabled('test', msgComment, false), false);
-        assert.equal(
-            element._computeSaveDisabled('test2', msgComment, false), false);
-        assert.equal(element._computeSaveDisabled('test', comment, true), false);
-        assert.equal(element._computeSaveDisabled('', comment, true), true);
-        assert.equal(element._computeSaveDisabled('', comment, false), true);
+      stub('gr-storage', {
+        getDraftComment() { return null; },
       });
+      element = fixture('draft');
+      element.changeNum = 42;
+      element.patchNum = 1;
+      element.editing = false;
+      element.comment = {
+        __commentSide: 'right',
+        __draft: true,
+        __draftID: 'temp_draft_id',
+        path: '/path/to/file',
+        line: 5,
+      };
+      element.commentSide = 'right';
+      sandbox = sinon.sandbox.create();
+    });
 
-      suite('confirm discard', () => {
-        let discardStub;
-        let overlayStub;
-        let mockEvent;
+    teardown(() => {
+      sandbox.restore();
+    });
 
-        setup(() => {
-          discardStub = sandbox.stub(element, '_discardDraft');
-          overlayStub = sandbox.stub(element, '_openOverlay')
-              .returns(Promise.resolve());
-          mockEvent = {preventDefault: sinon.stub()};
-        });
+    test('button visibility states', () => {
+      element.showActions = false;
+      assert.isTrue(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
 
-        test('confirms discard of comments with message text', () => {
-          element._messageText = 'test';
-          element._handleDiscard(mockEvent);
-          assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
-          assert.isFalse(discardStub.called);
-        });
+      element.showActions = true;
+      assert.isFalse(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
 
-        test('no confirmation for comments without message text', () => {
-          element._messageText = '';
-          element._handleDiscard(mockEvent);
-          assert.isFalse(overlayStub.called);
-          assert.isTrue(discardStub.calledOnce);
-        });
-      });
+      element.draft = true;
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.edit')), 'edit is visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.discard')), 'discard is visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.save')), 'save is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.cancel')), 'cancel is not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.resolve')), 'resolve is visible');
+      assert.isFalse(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
 
-      test('ctrl+s saves comment', done => {
-        const stub = sinon.stub(element, 'save', () => {
-          assert.isTrue(stub.called);
-          stub.restore();
+      element.editing = true;
+      flushAsynchronousOperations();
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.edit')), 'edit is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.discard')), 'discard not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.save')), 'save is visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.cancel')), 'cancel is visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.resolve')), 'resolve is visible');
+      assert.isFalse(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
+
+      element.draft = false;
+      element.editing = false;
+      flushAsynchronousOperations();
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.edit')), 'edit is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.discard')),
+      'discard is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.save')), 'save is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.cancel')), 'cancel is not visible');
+      assert.isFalse(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
+
+      element.comment.id = 'foo';
+      element.draft = true;
+      element.editing = true;
+      flushAsynchronousOperations();
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.cancel')), 'cancel is visible');
+      assert.isFalse(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
+
+      // Delete button is not hidden by default
+      assert.isFalse(element.shadowRoot.querySelector('#deleteBtn').hidden);
+
+      element.isRobotComment = true;
+      element.draft = true;
+      assert.isTrue(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isFalse(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
+
+      // It is not expected to see Robot comment drafts, but if they appear,
+      // they will behave the same as non-drafts.
+      element.draft = false;
+      assert.isTrue(element.shadowRoot
+          .querySelector('.humanActions').hasAttribute('hidden'));
+      assert.isFalse(element.shadowRoot
+          .querySelector('.robotActions').hasAttribute('hidden'));
+
+      // A robot comment with run ID should display plain text.
+      element.set(['comment', 'robot_run_id'], 'text');
+      element.editing = false;
+      element.collapsed = false;
+      flushAsynchronousOperations();
+      assert.isTrue(element.shadowRoot
+          .querySelector('.robotRun.link').textContent === 'Run Details');
+
+      // A robot comment with run ID and url should display a link.
+      element.set(['comment', 'url'], '/path/to/run');
+      flushAsynchronousOperations();
+      assert.notEqual(getComputedStyle(element.shadowRoot
+          .querySelector('.robotRun.link')).display,
+      'none');
+
+      // Delete button is hidden for robot comments
+      assert.isTrue(element.shadowRoot.querySelector('#deleteBtn').hidden);
+    });
+
+    test('collapsible drafts', () => {
+      assert.isTrue(element.collapsed);
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are not visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is visible');
+
+      MockInteractions.tap(element.$.header);
+      assert.isFalse(element.collapsed);
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are visible');
+      assert.isNotOk(element.textarea, 'textarea is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is is not visible');
+
+      // When the edit button is pressed, should still see the actions
+      // and also textarea
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.edit'));
+      flushAsynchronousOperations();
+      assert.isFalse(element.collapsed);
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are visible');
+      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is not visible');
+
+      // When toggle again, everything should be hidden except for textarea
+      // and header middle content should be visible
+      MockInteractions.tap(element.$.header);
+      assert.isTrue(element.collapsed);
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are not visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-textarea')),
+      'textarea is not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is visible');
+
+      // When toggle again, textarea should remain open in the state it was
+      // before
+      MockInteractions.tap(element.$.header);
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('gr-formatted-text')),
+      'gr-formatted-text is not visible');
+      assert.isTrue(isVisible(element.shadowRoot
+          .querySelector('.actions')),
+      'actions are visible');
+      assert.isTrue(isVisible(element.textarea), 'textarea is visible');
+      assert.isFalse(isVisible(element.shadowRoot
+          .querySelector('.collapsedContent')),
+      'header middle content is not visible');
+    });
+
+    test('robot comment layout', () => {
+      const comment = Object.assign({
+        robot_id: 'happy_robot_id',
+        url: '/robot/comment',
+        author: {
+          name: 'Happy Robot',
+        },
+      }, element.comment);
+      element.comment = comment;
+      element.collapsed = false;
+      flushAsynchronousOperations();
+
+      let runIdMessage;
+      runIdMessage = element.shadowRoot
+          .querySelector('.runIdMessage');
+      assert.isFalse(runIdMessage.hidden);
+
+      const runDetailsLink = element.shadowRoot
+          .querySelector('.robotRunLink');
+      assert.isTrue(runDetailsLink.href.indexOf(element.comment.url) !== -1);
+
+      const robotServiceName = element.shadowRoot
+          .querySelector('.authorName');
+      assert.isTrue(robotServiceName.textContent === 'happy_robot_id');
+
+      const authorName = element.shadowRoot
+          .querySelector('.robotId');
+      assert.isTrue(authorName.innerText === 'Happy Robot');
+
+      element.collapsed = true;
+      flushAsynchronousOperations();
+      runIdMessage = element.shadowRoot
+          .querySelector('.runIdMessage');
+      assert.isTrue(runIdMessage.hidden);
+    });
+
+    test('draft creation/cancellation', done => {
+      assert.isFalse(element.editing);
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.edit'));
+      assert.isTrue(element.editing);
+
+      element._messageText = '';
+      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+
+      // Save should be disabled on an empty message.
+      let disabled = element.shadowRoot
+          .querySelector('.save').hasAttribute('disabled');
+      assert.isTrue(disabled, 'save button should be disabled.');
+      element._messageText = '     ';
+      disabled = element.shadowRoot
+          .querySelector('.save').hasAttribute('disabled');
+      assert.isTrue(disabled, 'save button should be disabled.');
+
+      const updateStub = sinon.stub();
+      element.addEventListener('comment-update', updateStub);
+
+      let numDiscardEvents = 0;
+      element.addEventListener('comment-discard', e => {
+        numDiscardEvents++;
+        assert.isFalse(eraseMessageDraftSpy.called);
+        if (numDiscardEvents === 2) {
+          assert.isFalse(updateStub.called);
           done();
-          return Promise.resolve();
+        }
+      });
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.cancel'));
+      element.flushDebouncer('fire-update');
+      element._messageText = '';
+      flushAsynchronousOperations();
+      MockInteractions.pressAndReleaseKeyOn(element.textarea, 27); // esc
+    });
+
+    test('draft discard removes message from storage', done => {
+      element._messageText = '';
+      const eraseMessageDraftSpy = sandbox.spy(element, '_eraseDraftComment');
+      sandbox.stub(element, '_closeConfirmDiscardOverlay');
+
+      element.addEventListener('comment-discard', e => {
+        assert.isTrue(eraseMessageDraftSpy.called);
+        done();
+      });
+      element._handleConfirmDiscard({preventDefault: sinon.stub()});
+    });
+
+    test('storage is cleared only after save success', () => {
+      element._messageText = 'test';
+      const eraseStub = sandbox.stub(element, '_eraseDraftComment');
+      sandbox.stub(element.$.restAPI, 'getResponseObject')
+          .returns(Promise.resolve({}));
+
+      sandbox.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
+
+      const savePromise = element.save();
+      assert.isFalse(eraseStub.called);
+      return savePromise.then(() => {
+        assert.isFalse(eraseStub.called);
+
+        element._saveDraft.restore();
+        sandbox.stub(element, '_saveDraft')
+            .returns(Promise.resolve({ok: true}));
+        return element.save().then(() => {
+          assert.isTrue(eraseStub.called);
         });
-        element._messageText = 'is that the horse from horsing around??';
-        element.editing = true;
-        flushAsynchronousOperations();
-        MockInteractions.pressAndReleaseKeyOn(
-            element.textarea.$.textarea.textarea,
-            83, 'ctrl'); // 'ctrl + s'
+      });
+    });
+
+    test('_computeSaveDisabled', () => {
+      const comment = {unresolved: true};
+      const msgComment = {message: 'test', unresolved: true};
+      assert.equal(element._computeSaveDisabled('', comment, false), true);
+      assert.equal(element._computeSaveDisabled('test', comment, false), false);
+      assert.equal(element._computeSaveDisabled('', msgComment, false), true);
+      assert.equal(
+          element._computeSaveDisabled('test', msgComment, false), false);
+      assert.equal(
+          element._computeSaveDisabled('test2', msgComment, false), false);
+      assert.equal(element._computeSaveDisabled('test', comment, true), false);
+      assert.equal(element._computeSaveDisabled('', comment, true), true);
+      assert.equal(element._computeSaveDisabled('', comment, false), true);
+    });
+
+    suite('confirm discard', () => {
+      let discardStub;
+      let overlayStub;
+      let mockEvent;
+
+      setup(() => {
+        discardStub = sandbox.stub(element, '_discardDraft');
+        overlayStub = sandbox.stub(element, '_openOverlay')
+            .returns(Promise.resolve());
+        mockEvent = {preventDefault: sinon.stub()};
       });
 
-      test('draft saving/editing', done => {
-        const fireStub = sinon.stub(element, 'fire');
-        const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+      test('confirms discard of comments with message text', () => {
+        element._messageText = 'test';
+        element._handleDiscard(mockEvent);
+        assert.isTrue(overlayStub.calledWith(element.confirmDiscardOverlay));
+        assert.isFalse(discardStub.called);
+      });
 
-        element.draft = true;
+      test('no confirmation for comments without message text', () => {
+        element._messageText = '';
+        element._handleDiscard(mockEvent);
+        assert.isFalse(overlayStub.called);
+        assert.isTrue(discardStub.calledOnce);
+      });
+    });
+
+    test('ctrl+s saves comment', done => {
+      const stub = sinon.stub(element, 'save', () => {
+        assert.isTrue(stub.called);
+        stub.restore();
+        done();
+        return Promise.resolve();
+      });
+      element._messageText = 'is that the horse from horsing around??';
+      element.editing = true;
+      flushAsynchronousOperations();
+      MockInteractions.pressAndReleaseKeyOn(
+          element.textarea.$.textarea.textarea,
+          83, 'ctrl'); // 'ctrl + s'
+    });
+
+    test('draft saving/editing', done => {
+      const fireStub = sinon.stub(element, 'fire');
+      const cancelDebounce = sandbox.stub(element, 'cancelDebouncer');
+
+      element.draft = true;
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.edit'));
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+      assert(fireStub.calledWith('comment-update'),
+          'comment-update should be sent');
+      assert.isTrue(fireStub.calledOnce);
+
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+      assert.isTrue(fireStub.calledOnce,
+          'No events should fire for text editing');
+
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.save'));
+
+      assert.isTrue(element.disabled,
+          'Element should be disabled when creating draft.');
+
+      element._xhrPromise.then(draft => {
+        assert(fireStub.calledWith('comment-save'),
+            'comment-save should be sent');
+        assert(cancelDebounce.calledWith('store'));
+
+        assert.deepEqual(fireStub.lastCall.args[1], {
+          comment: {
+            __commentSide: 'right',
+            __draft: true,
+            __draftID: 'temp_draft_id',
+            id: 'baf0414d_40572e03',
+            line: 5,
+            message: 'saved!',
+            path: '/path/to/file',
+            updated: '2015-12-08 21:52:36.177000000',
+          },
+          patchNum: 1,
+        });
+        assert.isFalse(element.disabled,
+            'Element should be enabled when done creating draft.');
+        assert.equal(draft.message, 'saved!');
+        assert.isFalse(element.editing);
+      }).then(() => {
         MockInteractions.tap(element.shadowRoot
             .querySelector('.edit'));
-        element._messageText = 'good news, everyone!';
-        element.flushDebouncer('fire-update');
-        element.flushDebouncer('store');
-        assert(fireStub.calledWith('comment-update'),
-            'comment-update should be sent');
-        assert.isTrue(fireStub.calledOnce);
-
-        element._messageText = 'good news, everyone!';
-        element.flushDebouncer('fire-update');
-        element.flushDebouncer('store');
-        assert.isTrue(fireStub.calledOnce,
-            'No events should fire for text editing');
-
+        element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
+            'a world where humans are killed on sight.';
         MockInteractions.tap(element.shadowRoot
             .querySelector('.save'));
-
         assert.isTrue(element.disabled,
-            'Element should be disabled when creating draft.');
+            'Element should be disabled when updating draft.');
 
         element._xhrPromise.then(draft => {
-          assert(fireStub.calledWith('comment-save'),
-              'comment-save should be sent');
-          assert(cancelDebounce.calledWith('store'));
-
-          assert.deepEqual(fireStub.lastCall.args[1], {
-            comment: {
-              __commentSide: 'right',
-              __draft: true,
-              __draftID: 'temp_draft_id',
-              id: 'baf0414d_40572e03',
-              line: 5,
-              message: 'saved!',
-              path: '/path/to/file',
-              updated: '2015-12-08 21:52:36.177000000',
-            },
-            patchNum: 1,
-          });
           assert.isFalse(element.disabled,
-              'Element should be enabled when done creating draft.');
+              'Element should be enabled when done updating draft.');
           assert.equal(draft.message, 'saved!');
           assert.isFalse(element.editing);
-        }).then(() => {
-          MockInteractions.tap(element.shadowRoot
-              .querySelector('.edit'));
-          element._messageText = 'You’ll be delivering a package to Chapek 9, ' +
-              'a world where humans are killed on sight.';
-          MockInteractions.tap(element.shadowRoot
-              .querySelector('.save'));
-          assert.isTrue(element.disabled,
-              'Element should be disabled when updating draft.');
-
-          element._xhrPromise.then(draft => {
-            assert.isFalse(element.disabled,
-                'Element should be enabled when done updating draft.');
-            assert.equal(draft.message, 'saved!');
-            assert.isFalse(element.editing);
-            fireStub.restore();
-            done();
-          });
-        });
-      });
-
-      test('draft prevent save when disabled', () => {
-        const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
-        element.showActions = true;
-        element.draft = true;
-        MockInteractions.tap(element.$.header);
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.edit'));
-        element._messageText = 'good news, everyone!';
-        element.flushDebouncer('fire-update');
-        element.flushDebouncer('store');
-
-        element.disabled = true;
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.save'));
-        assert.isFalse(saveStub.called);
-
-        element.disabled = false;
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.save'));
-        assert.isTrue(saveStub.calledOnce);
-      });
-
-      test('proper event fires on resolve, comment is not saved', done => {
-        const save = sandbox.stub(element, 'save');
-        element.addEventListener('comment-update', e => {
-          assert.isTrue(e.detail.comment.unresolved);
-          assert.isFalse(save.called);
+          fireStub.restore();
           done();
         });
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.resolve input'));
-      });
-
-      test('resolved comment state indicated by checkbox', () => {
-        sandbox.stub(element, 'save');
-        element.comment = {unresolved: false};
-        assert.isTrue(element.shadowRoot
-            .querySelector('.resolve input').checked);
-        element.comment = {unresolved: true};
-        assert.isFalse(element.shadowRoot
-            .querySelector('.resolve input').checked);
-      });
-
-      test('resolved checkbox saves with tap when !editing', () => {
-        element.editing = false;
-        const save = sandbox.stub(element, 'save');
-
-        element.comment = {unresolved: false};
-        assert.isTrue(element.shadowRoot
-            .querySelector('.resolve input').checked);
-        element.comment = {unresolved: true};
-        assert.isFalse(element.shadowRoot
-            .querySelector('.resolve input').checked);
-        assert.isFalse(save.called);
-        MockInteractions.tap(element.$.resolvedCheckbox);
-        assert.isTrue(element.shadowRoot
-            .querySelector('.resolve input').checked);
-        assert.isTrue(save.called);
-      });
-
-      suite('draft saving messages', () => {
-        test('_getSavingMessage', () => {
-          assert.equal(element._getSavingMessage(0), 'All changes saved');
-          assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
-          assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
-          assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
-        });
-
-        test('_show{Start,End}Request', () => {
-          const updateStub = sandbox.stub(element, '_updateRequestToast');
-          element._numPendingDraftRequests.number = 1;
-
-          element._showStartRequest();
-          assert.isTrue(updateStub.calledOnce);
-          assert.equal(updateStub.lastCall.args[0], 2);
-          assert.equal(element._numPendingDraftRequests.number, 2);
-
-          element._showEndRequest();
-          assert.isTrue(updateStub.calledTwice);
-          assert.equal(updateStub.lastCall.args[0], 1);
-          assert.equal(element._numPendingDraftRequests.number, 1);
-
-          element._showEndRequest();
-          assert.isTrue(updateStub.calledThrice);
-          assert.equal(updateStub.lastCall.args[0], 0);
-          assert.equal(element._numPendingDraftRequests.number, 0);
-        });
-      });
-
-      test('cancelling an unsaved draft discards, persists in storage', () => {
-        const discardSpy = sandbox.spy(element, '_fireDiscard');
-        const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
-        const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
-        element._messageText = 'test text';
-        flushAsynchronousOperations();
-        element.flushDebouncer('store');
-
-        assert.isTrue(storeStub.called);
-        assert.equal(storeStub.lastCall.args[1], 'test text');
-        element._handleCancel({preventDefault: () => {}});
-        assert.isTrue(discardSpy.called);
-        assert.isFalse(eraseStub.called);
-      });
-
-      test('cancelling edit on a saved draft does not store', () => {
-        element.comment.id = 'foo';
-        const discardSpy = sandbox.spy(element, '_fireDiscard');
-        const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
-        element._messageText = 'test text';
-        flushAsynchronousOperations();
-        element.flushDebouncer('store');
-
-        assert.isFalse(storeStub.called);
-        element._handleCancel({preventDefault: () => {}});
-        assert.isTrue(discardSpy.called);
-      });
-
-      test('deleting text from saved draft and saving deletes the draft', () => {
-        element.comment = {id: 'foo', message: 'test'};
-        element._messageText = '';
-        const discardStub = sandbox.stub(element, '_discardDraft');
-
-        element.save();
-        assert.isTrue(discardStub.called);
-      });
-
-      test('_handleFix fires create-fix event', done => {
-        element.addEventListener('create-fix-comment', e => {
-          assert.deepEqual(e.detail, element._getEventPayload());
-          done();
-        });
-        element.isRobotComment = true;
-        element.comments = [element.comment];
-        flushAsynchronousOperations();
-
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.fix'));
-      });
-
-      test('do not show Please Fix button if human reply exists', () => {
-        element.comments = [
-          {
-            robot_id: 'happy_robot_id',
-            robot_run_id: '5838406743490560',
-            fix_suggestions: [
-              {
-                fix_id: '478ff847_3bf47aaf',
-                description: 'Make the smiley happier by giving it a nose.',
-                replacements: [
-                  {
-                    path: 'Documentation/config-gerrit.txt',
-                    range: {
-                      start_line: 10,
-                      start_character: 7,
-                      end_line: 10,
-                      end_character: 9,
-                    },
-                    replacement: ':-)',
-                  },
-                ],
-              },
-            ],
-            author: {
-              _account_id: 1030912,
-              name: 'Alice Kober-Sotzek',
-              email: 'aliceks@google.com',
-              avatars: [
-                {
-                  url: '/s32-p/photo.jpg',
-                  height: 32,
-                },
-                {
-                  url: '/AaAdOFzPlFI/s56-p/photo.jpg',
-                  height: 56,
-                },
-                {
-                  url: '/AaAdOFzPlFI/s100-p/photo.jpg',
-                  height: 100,
-                },
-                {
-                  url: '/AaAdOFzPlFI/s120-p/photo.jpg',
-                  height: 120,
-                },
-              ],
-            },
-            patch_set: 1,
-            id: 'eb0d03fd_5e95904f',
-            line: 10,
-            updated: '2017-04-04 15:36:17.000000000',
-            message: 'This is a robot comment with a fix.',
-            unresolved: false,
-            __commentSide: 'right',
-            collapsed: false,
-          },
-          {
-            __draft: true,
-            __draftID: '0.wbrfbwj89sa',
-            __date: '2019-12-04T13:41:03.689Z',
-            path: 'Documentation/config-gerrit.txt',
-            patchNum: 1,
-            side: 'REVISION',
-            __commentSide: 'right',
-            line: 10,
-            in_reply_to: 'eb0d03fd_5e95904f',
-            message: '> This is a robot comment with a fix.\n\nPlease fix.',
-            unresolved: true,
-          },
-        ];
-        element.comment = element.comments[0];
-        flushAsynchronousOperations();
-        assert.isNull(element.shadowRoot
-            .querySelector('robotActions gr-button'));
-      });
-
-      test('show Please Fix if no human reply', () => {
-        element.comments = [
-          {
-            robot_id: 'happy_robot_id',
-            robot_run_id: '5838406743490560',
-            fix_suggestions: [
-              {
-                fix_id: '478ff847_3bf47aaf',
-                description: 'Make the smiley happier by giving it a nose.',
-                replacements: [
-                  {
-                    path: 'Documentation/config-gerrit.txt',
-                    range: {
-                      start_line: 10,
-                      start_character: 7,
-                      end_line: 10,
-                      end_character: 9,
-                    },
-                    replacement: ':-)',
-                  },
-                ],
-              },
-            ],
-            author: {
-              _account_id: 1030912,
-              name: 'Alice Kober-Sotzek',
-              email: 'aliceks@google.com',
-              avatars: [
-                {
-                  url: '/s32-p/photo.jpg',
-                  height: 32,
-                },
-                {
-                  url: '/AaAdOFzPlFI/s56-p/photo.jpg',
-                  height: 56,
-                },
-                {
-                  url: '/AaAdOFzPlFI/s100-p/photo.jpg',
-                  height: 100,
-                },
-                {
-                  url: '/AaAdOFzPlFI/s120-p/photo.jpg',
-                  height: 120,
-                },
-              ],
-            },
-            patch_set: 1,
-            id: 'eb0d03fd_5e95904f',
-            line: 10,
-            updated: '2017-04-04 15:36:17.000000000',
-            message: 'This is a robot comment with a fix.',
-            unresolved: false,
-            __commentSide: 'right',
-            collapsed: false,
-          },
-        ];
-        element.comment = element.comments[0];
-        flushAsynchronousOperations();
-        assert.isNotNull(element.shadowRoot
-            .querySelector('.robotActions gr-button'));
-      });
-
-      test('_handleShowFix fires open-fix-preview event', done => {
-        element.addEventListener('open-fix-preview', e => {
-          assert.deepEqual(e.detail, element._getEventPayload());
-          done();
-        });
-        element.comment = {fix_suggestions: [{}]};
-        element.isRobotComment = true;
-        flushAsynchronousOperations();
-
-        MockInteractions.tap(element.shadowRoot
-            .querySelector('.show-fix'));
       });
     });
 
-    suite('respectful tips', () => {
-      let element;
-      let sandbox;
-      let clock;
-      setup(() => {
-        stub('gr-rest-api-interface', {
-          getAccount() { return Promise.resolve(null); },
-        });
-        clock = sinon.useFakeTimers();
-        sandbox = sinon.sandbox.create();
+    test('draft prevent save when disabled', () => {
+      const saveStub = sandbox.stub(element, 'save').returns(Promise.resolve());
+      element.showActions = true;
+      element.draft = true;
+      MockInteractions.tap(element.$.header);
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.edit'));
+      element._messageText = 'good news, everyone!';
+      element.flushDebouncer('fire-update');
+      element.flushDebouncer('store');
+
+      element.disabled = true;
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.save'));
+      assert.isFalse(saveStub.called);
+
+      element.disabled = false;
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.save'));
+      assert.isTrue(saveStub.calledOnce);
+    });
+
+    test('proper event fires on resolve, comment is not saved', done => {
+      const save = sandbox.stub(element, 'save');
+      element.addEventListener('comment-update', e => {
+        assert.isTrue(e.detail.comment.unresolved);
+        assert.isFalse(save.called);
+        done();
+      });
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.resolve input'));
+    });
+
+    test('resolved comment state indicated by checkbox', () => {
+      sandbox.stub(element, 'save');
+      element.comment = {unresolved: false};
+      assert.isTrue(element.shadowRoot
+          .querySelector('.resolve input').checked);
+      element.comment = {unresolved: true};
+      assert.isFalse(element.shadowRoot
+          .querySelector('.resolve input').checked);
+    });
+
+    test('resolved checkbox saves with tap when !editing', () => {
+      element.editing = false;
+      const save = sandbox.stub(element, 'save');
+
+      element.comment = {unresolved: false};
+      assert.isTrue(element.shadowRoot
+          .querySelector('.resolve input').checked);
+      element.comment = {unresolved: true};
+      assert.isFalse(element.shadowRoot
+          .querySelector('.resolve input').checked);
+      assert.isFalse(save.called);
+      MockInteractions.tap(element.$.resolvedCheckbox);
+      assert.isTrue(element.shadowRoot
+          .querySelector('.resolve input').checked);
+      assert.isTrue(save.called);
+    });
+
+    suite('draft saving messages', () => {
+      test('_getSavingMessage', () => {
+        assert.equal(element._getSavingMessage(0), 'All changes saved');
+        assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
+        assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
+        assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
       });
 
-      teardown(() => {
-        clock.restore();
-        sandbox.restore();
-      });
+      test('_show{Start,End}Request', () => {
+        const updateStub = sandbox.stub(element, '_updateRequestToast');
+        element._numPendingDraftRequests.number = 1;
 
-      test('show tip when no cached record', done => {
-        // fake stub for storage
-        const respectfulGetStub = sinon.stub();
-        const respectfulSetStub = sinon.stub();
-        stub('gr-storage', {
-          getRespectfulTipVisibility() { return respectfulGetStub(); },
-          setRespectfulTipVisibility() { return respectfulSetStub(); },
-        });
-        respectfulGetStub.returns(null);
-        element = fixture('draft');
-        // fake random
-        element.getRandomNum = () => 0;
-        element.comment = {__editing: true};
+        element._showStartRequest();
+        assert.isTrue(updateStub.calledOnce);
+        assert.equal(updateStub.lastCall.args[0], 2);
+        assert.equal(element._numPendingDraftRequests.number, 2);
+
+        element._showEndRequest();
+        assert.isTrue(updateStub.calledTwice);
+        assert.equal(updateStub.lastCall.args[0], 1);
+        assert.equal(element._numPendingDraftRequests.number, 1);
+
+        element._showEndRequest();
+        assert.isTrue(updateStub.calledThrice);
+        assert.equal(updateStub.lastCall.args[0], 0);
+        assert.equal(element._numPendingDraftRequests.number, 0);
+      });
+    });
+
+    test('cancelling an unsaved draft discards, persists in storage', () => {
+      const discardSpy = sandbox.spy(element, '_fireDiscard');
+      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+      const eraseStub = sandbox.stub(element.$.storage, 'eraseDraftComment');
+      element._messageText = 'test text';
+      flushAsynchronousOperations();
+      element.flushDebouncer('store');
+
+      assert.isTrue(storeStub.called);
+      assert.equal(storeStub.lastCall.args[1], 'test text');
+      element._handleCancel({preventDefault: () => {}});
+      assert.isTrue(discardSpy.called);
+      assert.isFalse(eraseStub.called);
+    });
+
+    test('cancelling edit on a saved draft does not store', () => {
+      element.comment.id = 'foo';
+      const discardSpy = sandbox.spy(element, '_fireDiscard');
+      const storeStub = sandbox.stub(element.$.storage, 'setDraftComment');
+      element._messageText = 'test text';
+      flushAsynchronousOperations();
+      element.flushDebouncer('store');
+
+      assert.isFalse(storeStub.called);
+      element._handleCancel({preventDefault: () => {}});
+      assert.isTrue(discardSpy.called);
+    });
+
+    test('deleting text from saved draft and saving deletes the draft', () => {
+      element.comment = {id: 'foo', message: 'test'};
+      element._messageText = '';
+      const discardStub = sandbox.stub(element, '_discardDraft');
+
+      element.save();
+      assert.isTrue(discardStub.called);
+    });
+
+    test('_handleFix fires create-fix event', done => {
+      element.addEventListener('create-fix-comment', e => {
+        assert.deepEqual(e.detail, element._getEventPayload());
+        done();
+      });
+      element.isRobotComment = true;
+      element.comments = [element.comment];
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.fix'));
+    });
+
+    test('do not show Please Fix button if human reply exists', () => {
+      element.comments = [
+        {
+          robot_id: 'happy_robot_id',
+          robot_run_id: '5838406743490560',
+          fix_suggestions: [
+            {
+              fix_id: '478ff847_3bf47aaf',
+              description: 'Make the smiley happier by giving it a nose.',
+              replacements: [
+                {
+                  path: 'Documentation/config-gerrit.txt',
+                  range: {
+                    start_line: 10,
+                    start_character: 7,
+                    end_line: 10,
+                    end_character: 9,
+                  },
+                  replacement: ':-)',
+                },
+              ],
+            },
+          ],
+          author: {
+            _account_id: 1030912,
+            name: 'Alice Kober-Sotzek',
+            email: 'aliceks@google.com',
+            avatars: [
+              {
+                url: '/s32-p/photo.jpg',
+                height: 32,
+              },
+              {
+                url: '/AaAdOFzPlFI/s56-p/photo.jpg',
+                height: 56,
+              },
+              {
+                url: '/AaAdOFzPlFI/s100-p/photo.jpg',
+                height: 100,
+              },
+              {
+                url: '/AaAdOFzPlFI/s120-p/photo.jpg',
+                height: 120,
+              },
+            ],
+          },
+          patch_set: 1,
+          id: 'eb0d03fd_5e95904f',
+          line: 10,
+          updated: '2017-04-04 15:36:17.000000000',
+          message: 'This is a robot comment with a fix.',
+          unresolved: false,
+          __commentSide: 'right',
+          collapsed: false,
+        },
+        {
+          __draft: true,
+          __draftID: '0.wbrfbwj89sa',
+          __date: '2019-12-04T13:41:03.689Z',
+          path: 'Documentation/config-gerrit.txt',
+          patchNum: 1,
+          side: 'REVISION',
+          __commentSide: 'right',
+          line: 10,
+          in_reply_to: 'eb0d03fd_5e95904f',
+          message: '> This is a robot comment with a fix.\n\nPlease fix.',
+          unresolved: true,
+        },
+      ];
+      element.comment = element.comments[0];
+      flushAsynchronousOperations();
+      assert.isNull(element.shadowRoot
+          .querySelector('robotActions gr-button'));
+    });
+
+    test('show Please Fix if no human reply', () => {
+      element.comments = [
+        {
+          robot_id: 'happy_robot_id',
+          robot_run_id: '5838406743490560',
+          fix_suggestions: [
+            {
+              fix_id: '478ff847_3bf47aaf',
+              description: 'Make the smiley happier by giving it a nose.',
+              replacements: [
+                {
+                  path: 'Documentation/config-gerrit.txt',
+                  range: {
+                    start_line: 10,
+                    start_character: 7,
+                    end_line: 10,
+                    end_character: 9,
+                  },
+                  replacement: ':-)',
+                },
+              ],
+            },
+          ],
+          author: {
+            _account_id: 1030912,
+            name: 'Alice Kober-Sotzek',
+            email: 'aliceks@google.com',
+            avatars: [
+              {
+                url: '/s32-p/photo.jpg',
+                height: 32,
+              },
+              {
+                url: '/AaAdOFzPlFI/s56-p/photo.jpg',
+                height: 56,
+              },
+              {
+                url: '/AaAdOFzPlFI/s100-p/photo.jpg',
+                height: 100,
+              },
+              {
+                url: '/AaAdOFzPlFI/s120-p/photo.jpg',
+                height: 120,
+              },
+            ],
+          },
+          patch_set: 1,
+          id: 'eb0d03fd_5e95904f',
+          line: 10,
+          updated: '2017-04-04 15:36:17.000000000',
+          message: 'This is a robot comment with a fix.',
+          unresolved: false,
+          __commentSide: 'right',
+          collapsed: false,
+        },
+      ];
+      element.comment = element.comments[0];
+      flushAsynchronousOperations();
+      assert.isNotNull(element.shadowRoot
+          .querySelector('.robotActions gr-button'));
+    });
+
+    test('_handleShowFix fires open-fix-preview event', done => {
+      element.addEventListener('open-fix-preview', e => {
+        assert.deepEqual(e.detail, element._getEventPayload());
+        done();
+      });
+      element.comment = {fix_suggestions: [{}]};
+      element.isRobotComment = true;
+      flushAsynchronousOperations();
+
+      MockInteractions.tap(element.shadowRoot
+          .querySelector('.show-fix'));
+    });
+  });
+
+  suite('respectful tips', () => {
+    let element;
+    let sandbox;
+    let clock;
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getAccount() { return Promise.resolve(null); },
+      });
+      clock = sinon.useFakeTimers();
+      sandbox = sinon.sandbox.create();
+    });
+
+    teardown(() => {
+      clock.restore();
+      sandbox.restore();
+    });
+
+    test('show tip when no cached record', done => {
+      // fake stub for storage
+      const respectfulGetStub = sinon.stub();
+      const respectfulSetStub = sinon.stub();
+      stub('gr-storage', {
+        getRespectfulTipVisibility() { return respectfulGetStub(); },
+        setRespectfulTipVisibility() { return respectfulSetStub(); },
+      });
+      respectfulGetStub.returns(null);
+      element = fixture('draft');
+      // fake random
+      element.getRandomNum = () => 0;
+      element.comment = {__editing: true};
+      flush(() => {
+        assert.isTrue(respectfulGetStub.called);
+        assert.isTrue(respectfulSetStub.called);
+        assert.isTrue(
+            !!element.shadowRoot.querySelector('.respectfulReviewTip')
+        );
+        done();
+      });
+    });
+
+    test('add 3 day delays once dismissed', done => {
+      // fake stub for storage
+      const respectfulGetStub = sinon.stub();
+      const respectfulSetStub = sinon.stub();
+      stub('gr-storage', {
+        getRespectfulTipVisibility() { return respectfulGetStub(); },
+        setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
+      });
+      respectfulGetStub.returns(null);
+      element = fixture('draft');
+      // fake random
+      element.getRandomNum = () => 0;
+      element.comment = {__editing: true};
+      flush(() => {
+        assert.isTrue(respectfulGetStub.called);
+        assert.isTrue(respectfulSetStub.called);
+        assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
+        assert.isTrue(
+            !!element.shadowRoot.querySelector('.respectfulReviewTip')
+        );
+
+        MockInteractions.tap(element.shadowRoot
+            .querySelector('.respectfulReviewTip .close'));
+        flushAsynchronousOperations();
+        assert.isTrue(respectfulSetStub.lastCall.args[0] === 3);
+        done();
+      });
+    });
+
+    test('do not show tip when fall out of probability', done => {
+      // fake stub for storage
+      const respectfulGetStub = sinon.stub();
+      const respectfulSetStub = sinon.stub();
+      stub('gr-storage', {
+        getRespectfulTipVisibility() { return respectfulGetStub(); },
+        setRespectfulTipVisibility() { return respectfulSetStub(); },
+      });
+      respectfulGetStub.returns(null);
+      element = fixture('draft');
+      // fake random
+      element.getRandomNum = () => 3;
+      element.comment = {__editing: true};
+      flush(() => {
+        assert.isTrue(respectfulGetStub.called);
+        assert.isFalse(respectfulSetStub.called);
+        assert.isFalse(
+            !!element.shadowRoot.querySelector('.respectfulReviewTip')
+        );
+        done();
+      });
+    });
+
+    test('show tip when editing changed to true', done => {
+      // fake stub for storage
+      const respectfulGetStub = sinon.stub();
+      const respectfulSetStub = sinon.stub();
+      stub('gr-storage', {
+        getRespectfulTipVisibility() { return respectfulGetStub(); },
+        setRespectfulTipVisibility() { return respectfulSetStub(); },
+      });
+      respectfulGetStub.returns(null);
+      element = fixture('draft');
+      // fake random
+      element.getRandomNum = () => 0;
+      element.comment = {__editing: false};
+      flush(() => {
+        assert.isFalse(respectfulGetStub.called);
+        assert.isFalse(respectfulSetStub.called);
+        assert.isFalse(
+            !!element.shadowRoot.querySelector('.respectfulReviewTip')
+        );
+
+        element.editing = true;
         flush(() => {
           assert.isTrue(respectfulGetStub.called);
           assert.isTrue(respectfulSetStub.called);
@@ -1166,113 +1257,30 @@
           done();
         });
       });
+    });
 
-      test('add 3 day delays once dismissed', done => {
-        // fake stub for storage
-        const respectfulGetStub = sinon.stub();
-        const respectfulSetStub = sinon.stub();
-        stub('gr-storage', {
-          getRespectfulTipVisibility() { return respectfulGetStub(); },
-          setRespectfulTipVisibility(days) { return respectfulSetStub(days); },
-        });
-        respectfulGetStub.returns(null);
-        element = fixture('draft');
-        // fake random
-        element.getRandomNum = () => 0;
-        element.comment = {__editing: true};
-        flush(() => {
-          assert.isTrue(respectfulGetStub.called);
-          assert.isTrue(respectfulSetStub.called);
-          assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
-          assert.isTrue(
-              !!element.shadowRoot.querySelector('.respectfulReviewTip')
-          );
-
-          MockInteractions.tap(element.shadowRoot
-              .querySelector('.respectfulReviewTip .close'));
-          flushAsynchronousOperations();
-          assert.isTrue(respectfulSetStub.lastCall.args[0] === 3);
-          done();
-        });
+    test('no tip when cached record', done => {
+      // fake stub for storage
+      const respectfulGetStub = sinon.stub();
+      const respectfulSetStub = sinon.stub();
+      stub('gr-storage', {
+        getRespectfulTipVisibility() { return respectfulGetStub(); },
+        setRespectfulTipVisibility() { return respectfulSetStub(); },
       });
-
-      test('do not show tip when fall out of probability', done => {
-        // fake stub for storage
-        const respectfulGetStub = sinon.stub();
-        const respectfulSetStub = sinon.stub();
-        stub('gr-storage', {
-          getRespectfulTipVisibility() { return respectfulGetStub(); },
-          setRespectfulTipVisibility() { return respectfulSetStub(); },
-        });
-        respectfulGetStub.returns(null);
-        element = fixture('draft');
-        // fake random
-        element.getRandomNum = () => 3;
-        element.comment = {__editing: true};
-        flush(() => {
-          assert.isTrue(respectfulGetStub.called);
-          assert.isFalse(respectfulSetStub.called);
-          assert.isFalse(
-              !!element.shadowRoot.querySelector('.respectfulReviewTip')
-          );
-          done();
-        });
-      });
-
-      test('show tip when editing changed to true', done => {
-        // fake stub for storage
-        const respectfulGetStub = sinon.stub();
-        const respectfulSetStub = sinon.stub();
-        stub('gr-storage', {
-          getRespectfulTipVisibility() { return respectfulGetStub(); },
-          setRespectfulTipVisibility() { return respectfulSetStub(); },
-        });
-        respectfulGetStub.returns(null);
-        element = fixture('draft');
-        // fake random
-        element.getRandomNum = () => 0;
-        element.comment = {__editing: false};
-        flush(() => {
-          assert.isFalse(respectfulGetStub.called);
-          assert.isFalse(respectfulSetStub.called);
-          assert.isFalse(
-              !!element.shadowRoot.querySelector('.respectfulReviewTip')
-          );
-
-          element.editing = true;
-          flush(() => {
-            assert.isTrue(respectfulGetStub.called);
-            assert.isTrue(respectfulSetStub.called);
-            assert.isTrue(
-                !!element.shadowRoot.querySelector('.respectfulReviewTip')
-            );
-            done();
-          });
-        });
-      });
-
-      test('no tip when cached record', done => {
-        // fake stub for storage
-        const respectfulGetStub = sinon.stub();
-        const respectfulSetStub = sinon.stub();
-        stub('gr-storage', {
-          getRespectfulTipVisibility() { return respectfulGetStub(); },
-          setRespectfulTipVisibility() { return respectfulSetStub(); },
-        });
-        respectfulGetStub.returns({});
-        element = fixture('draft');
-        // fake random
-        element.getRandomNum = () => 0;
-        element.comment = {__editing: true};
-        flush(() => {
-          assert.isTrue(respectfulGetStub.called);
-          assert.isFalse(respectfulSetStub.called);
-          assert.isFalse(
-              !!element.shadowRoot.querySelector('.respectfulReviewTip')
-          );
-          done();
-        });
+      respectfulGetStub.returns({});
+      element = fixture('draft');
+      // fake random
+      element.getRandomNum = () => 0;
+      element.comment = {__editing: true};
+      flush(() => {
+        assert.isTrue(respectfulGetStub.called);
+        assert.isFalse(respectfulSetStub.called);
+        assert.isFalse(
+            !!element.shadowRoot.querySelector('.respectfulReviewTip')
+        );
+        done();
       });
     });
   });
+});
 </script>