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>