Move comment and thread elements to shared
Even before the refactorings, the comment and thread elements were also used from views outside of diff/. Now they are now completely independent from gr-diff and descendants, and can move into their own folder and drop the -diff from their name. The dependency left from diff/gr-diff-host onto comment[-thread], makes sense since gr-diff-host is the Gerrit wrapper for gr-diff with gr-comment[-thread]. Change-Id: I5076428da980198989edc605e5edc0e4d66529dd
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
<!--
|
||||
@license
|
||||
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.
|
||||
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/gr-path-list-behavior/gr-path-list-behavior.html">
|
||||
<link rel="import" href="../../../styles/shared-styles.html">
|
||||
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
|
||||
<link rel="import" href="../../core/gr-reporting/gr-reporting.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="../gr-comment/gr-comment.html">
|
||||
|
||||
<dom-module id="gr-comment-thread">
|
||||
<template>
|
||||
<style include="shared-styles">
|
||||
gr-button {
|
||||
margin-left: .5em;
|
||||
}
|
||||
#actions {
|
||||
margin-left: auto;
|
||||
padding: .5em .7em;
|
||||
}
|
||||
#container {
|
||||
background-color: var(--comment-background-color);
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--comment-text-color);
|
||||
display: block;
|
||||
margin-bottom: 1px;
|
||||
white-space: normal;
|
||||
}
|
||||
#container.unresolved {
|
||||
background-color: var(--unresolved-comment-background-color);
|
||||
}
|
||||
#commentInfoContainer {
|
||||
border-top: 1px dotted var(--border-color);
|
||||
display: flex;
|
||||
}
|
||||
#unresolvedLabel {
|
||||
font-family: var(--font-family);
|
||||
margin: auto 0;
|
||||
padding: .5em .7em;
|
||||
}
|
||||
.pathInfo {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.descriptionText {
|
||||
margin-left: .5rem;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
<template is="dom-if" if="[[showFilePath]]">
|
||||
<div class="pathInfo">
|
||||
<a href$="[[_getDiffUrlForComment(projectName, changeNum, path, patchNum)]]">[[_computeDisplayPath(path)]]</a>
|
||||
<span class="descriptionText">Patchset [[patchNum]]</span>
|
||||
</div>
|
||||
</template>
|
||||
<div id="container" class$="[[_computeHostClass(unresolved)]]">
|
||||
<template id="commentList" is="dom-repeat" items="[[_orderedComments]]"
|
||||
as="comment">
|
||||
<gr-comment
|
||||
comment="{{comment}}"
|
||||
robot-button-disabled="[[_hideActions(_showActions, _lastComment)]]"
|
||||
change-num="[[changeNum]]"
|
||||
patch-num="[[patchNum]]"
|
||||
draft="[[_isDraft(comment)]]"
|
||||
show-actions="[[_showActions]]"
|
||||
comment-side="[[comment.__commentSide]]"
|
||||
side="[[comment.side]]"
|
||||
root-id="[[rootId]]"
|
||||
project-config="[[_projectConfig]]"
|
||||
on-create-fix-comment="_handleCommentFix"
|
||||
on-comment-discard="_handleCommentDiscard"
|
||||
on-comment-save="_handleCommentSavedOrDiscarded"></gr-comment>
|
||||
</template>
|
||||
<div id="commentInfoContainer"
|
||||
hidden$="[[_hideActions(_showActions, _lastComment)]]">
|
||||
<span id="unresolvedLabel" hidden$="[[!unresolved]]">Unresolved</span>
|
||||
<div id="actions">
|
||||
<gr-button
|
||||
id="replyBtn"
|
||||
link
|
||||
secondary
|
||||
class="action reply"
|
||||
on-tap="_handleCommentReply">Reply</gr-button>
|
||||
<gr-button
|
||||
id="quoteBtn"
|
||||
link
|
||||
secondary
|
||||
class="action quote"
|
||||
on-tap="_handleCommentQuote">Quote</gr-button>
|
||||
<gr-button
|
||||
id="ackBtn"
|
||||
link
|
||||
secondary
|
||||
class="action ack"
|
||||
on-tap="_handleCommentAck">Ack</gr-button>
|
||||
<gr-button
|
||||
id="doneBtn"
|
||||
link
|
||||
secondary
|
||||
class="action done"
|
||||
on-tap="_handleCommentDone">Done</gr-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<gr-reporting id="reporting"></gr-reporting>
|
||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||
<gr-storage id="storage"></gr-storage>
|
||||
</template>
|
||||
<script src="gr-comment-thread.js"></script>
|
||||
</dom-module>
|
||||
@@ -0,0 +1,493 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright (C) 2016 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.
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const UNRESOLVED_EXPAND_COUNT = 5;
|
||||
const NEWLINE_PATTERN = /\n/g;
|
||||
|
||||
Polymer({
|
||||
is: 'gr-comment-thread',
|
||||
|
||||
/**
|
||||
* Fired when the thread should be discarded.
|
||||
*
|
||||
* @event thread-discard
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fired when a comment in the thread is permanently modified.
|
||||
*
|
||||
* @event thread-changed
|
||||
*/
|
||||
|
||||
/**
|
||||
* gr-comment-thread exposes the following attributes that allow a
|
||||
* diff widget like gr-diff to show the thread in the right location:
|
||||
*
|
||||
* line-num:
|
||||
* 1-based line number or undefined if it refers to the entire file.
|
||||
*
|
||||
* comment-side:
|
||||
* "left" or "right". These indicate which of the two diffed versions
|
||||
* the comment relates to. In the case of unified diff, the left
|
||||
* version is the one whose line number column is further to the left.
|
||||
*
|
||||
* range:
|
||||
* The range of text that the comment refers to (start_line,
|
||||
* start_character, end_line, end_character), serialized as JSON. If
|
||||
* set, range's end_line will have the same value as line-num. Line
|
||||
* numbers are 1-based, char numbers are 0-based. The start position
|
||||
* (start_line, start_character) is inclusive, and the end position
|
||||
* (end_line, end_character) is exclusive.
|
||||
*/
|
||||
properties: {
|
||||
changeNum: String,
|
||||
comments: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
/**
|
||||
* @type {?{start_line: number, start_character: number, end_line: number,
|
||||
* end_character: number}}
|
||||
*/
|
||||
range: {
|
||||
type: Object,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
keyEventTarget: {
|
||||
type: Object,
|
||||
value() { return document.body; },
|
||||
},
|
||||
commentSide: {
|
||||
type: String,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
patchNum: String,
|
||||
path: String,
|
||||
projectName: {
|
||||
type: String,
|
||||
observer: '_projectNameChanged',
|
||||
},
|
||||
hasDraft: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
isOnParent: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
parentIndex: {
|
||||
type: Number,
|
||||
value: null,
|
||||
},
|
||||
rootId: {
|
||||
type: String,
|
||||
notify: true,
|
||||
computed: '_computeRootId(comments.*)',
|
||||
},
|
||||
/**
|
||||
* If this is true, the comment thread also needs to have the change and
|
||||
* line properties property set
|
||||
*/
|
||||
showFilePath: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
/** Necessary only if showFilePath is true or when used with gr-diff */
|
||||
lineNum: {
|
||||
type: Number,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
unresolved: {
|
||||
type: Boolean,
|
||||
notify: true,
|
||||
reflectToAttribute: true,
|
||||
},
|
||||
_showActions: Boolean,
|
||||
_lastComment: Object,
|
||||
_orderedComments: Array,
|
||||
_projectConfig: Object,
|
||||
},
|
||||
|
||||
behaviors: [
|
||||
Gerrit.KeyboardShortcutBehavior,
|
||||
Gerrit.PathListBehavior,
|
||||
],
|
||||
|
||||
listeners: {
|
||||
'comment-update': '_handleCommentUpdate',
|
||||
},
|
||||
|
||||
observers: [
|
||||
'_commentsChanged(comments.*)',
|
||||
],
|
||||
|
||||
keyBindings: {
|
||||
'e shift+e': '_handleEKey',
|
||||
},
|
||||
|
||||
attached() {
|
||||
this._getLoggedIn().then(loggedIn => {
|
||||
this._showActions = loggedIn;
|
||||
});
|
||||
this._setInitialExpandedState();
|
||||
},
|
||||
|
||||
addOrEditDraft(opt_lineNum, opt_range) {
|
||||
const lastComment = this.comments[this.comments.length - 1] || {};
|
||||
if (lastComment.__draft) {
|
||||
const commentEl = this._commentElWithDraftID(
|
||||
lastComment.id || lastComment.__draftID);
|
||||
commentEl.editing = true;
|
||||
|
||||
// If the comment was collapsed, re-open it to make it clear which
|
||||
// actions are available.
|
||||
commentEl.collapsed = false;
|
||||
} else {
|
||||
const range = opt_range ? opt_range :
|
||||
lastComment ? lastComment.range : undefined;
|
||||
const unresolved = lastComment ? lastComment.unresolved : undefined;
|
||||
this.addDraft(opt_lineNum, range, unresolved);
|
||||
}
|
||||
},
|
||||
|
||||
addDraft(opt_lineNum, opt_range, opt_unresolved) {
|
||||
const draft = this._newDraft(opt_lineNum, opt_range);
|
||||
draft.__editing = true;
|
||||
draft.unresolved = opt_unresolved === false ? opt_unresolved : true;
|
||||
this.push('comments', draft);
|
||||
},
|
||||
|
||||
fireRemoveSelf() {
|
||||
this.dispatchEvent(new CustomEvent('thread-discard',
|
||||
{detail: {rootId: this.rootId}, bubbles: false}));
|
||||
},
|
||||
|
||||
_getDiffUrlForComment(projectName, changeNum, path, patchNum) {
|
||||
return Gerrit.Nav.getUrlForDiffById(changeNum,
|
||||
projectName, path, patchNum,
|
||||
null, this.lineNum);
|
||||
},
|
||||
|
||||
_computeDisplayPath(path) {
|
||||
const lineString = this.lineNum ? `#${this.lineNum}` : '';
|
||||
return this.computeDisplayPath(path) + lineString;
|
||||
},
|
||||
|
||||
_getLoggedIn() {
|
||||
return this.$.restAPI.getLoggedIn();
|
||||
},
|
||||
|
||||
_commentsChanged() {
|
||||
this._orderedComments = this._sortedComments(this.comments);
|
||||
this.updateThreadProperties();
|
||||
},
|
||||
|
||||
updateThreadProperties() {
|
||||
if (this._orderedComments.length) {
|
||||
this._lastComment = this._getLastComment();
|
||||
this.unresolved = this._lastComment.unresolved;
|
||||
this.hasDraft = this._lastComment.__draft;
|
||||
}
|
||||
},
|
||||
|
||||
_hideActions(_showActions, _lastComment) {
|
||||
return !_showActions || !_lastComment || !!_lastComment.__draft;
|
||||
},
|
||||
|
||||
_getLastComment() {
|
||||
return this._orderedComments[this._orderedComments.length - 1] || {};
|
||||
},
|
||||
|
||||
_handleEKey(e) {
|
||||
if (this.shouldSuppressKeyboardShortcut(e)) { return; }
|
||||
|
||||
// Don’t preventDefault in this case because it will render the event
|
||||
// useless for other handlers (other gr-comment-thread elements).
|
||||
if (e.detail.keyboardEvent.shiftKey) {
|
||||
this._expandCollapseComments(true);
|
||||
} else {
|
||||
if (this.modifierPressed(e)) { return; }
|
||||
this._expandCollapseComments(false);
|
||||
}
|
||||
},
|
||||
|
||||
_expandCollapseComments(actionIsCollapse) {
|
||||
const comments =
|
||||
Polymer.dom(this.root).querySelectorAll('gr-comment');
|
||||
for (const comment of comments) {
|
||||
comment.collapsed = actionIsCollapse;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the initial state of the comment thread.
|
||||
* Expands the thread if one of the following is true:
|
||||
* - last {UNRESOLVED_EXPAND_COUNT} comments expanded by default if the
|
||||
* thread is unresolved,
|
||||
* - it's a robot comment.
|
||||
*/
|
||||
_setInitialExpandedState() {
|
||||
if (this._orderedComments) {
|
||||
for (let i = 0; i < this._orderedComments.length; i++) {
|
||||
const comment = this._orderedComments[i];
|
||||
const isRobotComment = !!comment.robot_id;
|
||||
// False if it's an unresolved comment under UNRESOLVED_EXPAND_COUNT.
|
||||
const resolvedThread = !this.unresolved ||
|
||||
this._orderedComments.length - i - 1 >= UNRESOLVED_EXPAND_COUNT;
|
||||
comment.collapsed = !isRobotComment && resolvedThread;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_sortedComments(comments) {
|
||||
return comments.slice().sort((c1, c2) => {
|
||||
const c1Date = c1.__date || util.parseDate(c1.updated);
|
||||
const c2Date = c2.__date || util.parseDate(c2.updated);
|
||||
const dateCompare = c1Date - c2Date;
|
||||
// Ensure drafts are at the end. There should only be one but in edge
|
||||
// cases could be more. In the unlikely event two drafts are being
|
||||
// compared, use the typical date compare.
|
||||
if (c2.__draft && !c1.__draft ) { return -1; }
|
||||
if (c1.__draft && !c2.__draft ) { return 1; }
|
||||
if (dateCompare === 0 && (!c1.id || !c1.id.localeCompare)) { return 0; }
|
||||
// If same date, fall back to sorting by id.
|
||||
return dateCompare ? dateCompare : c1.id.localeCompare(c2.id);
|
||||
});
|
||||
},
|
||||
|
||||
_createReplyComment(parent, content, opt_isEditing,
|
||||
opt_unresolved) {
|
||||
this.$.reporting.recordDraftInteraction();
|
||||
const reply = this._newReply(
|
||||
this._orderedComments[this._orderedComments.length - 1].id,
|
||||
parent.line,
|
||||
content,
|
||||
opt_unresolved,
|
||||
parent.range);
|
||||
|
||||
// If there is currently a comment in an editing state, add an attribute
|
||||
// so that the gr-comment knows not to populate the draft text.
|
||||
for (let i = 0; i < this.comments.length; i++) {
|
||||
if (this.comments[i].__editing) {
|
||||
reply.__otherEditing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (opt_isEditing) {
|
||||
reply.__editing = true;
|
||||
}
|
||||
|
||||
this.push('comments', reply);
|
||||
|
||||
if (!opt_isEditing) {
|
||||
// Allow the reply to render in the dom-repeat.
|
||||
this.async(() => {
|
||||
const commentEl = this._commentElWithDraftID(reply.__draftID);
|
||||
commentEl.save();
|
||||
}, 1);
|
||||
}
|
||||
},
|
||||
|
||||
_isDraft(comment) {
|
||||
return !!comment.__draft;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {boolean=} opt_quote
|
||||
*/
|
||||
_processCommentReply(opt_quote) {
|
||||
const comment = this._lastComment;
|
||||
let quoteStr;
|
||||
if (opt_quote) {
|
||||
const msg = comment.message;
|
||||
quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
|
||||
}
|
||||
this._createReplyComment(comment, quoteStr, true, comment.unresolved);
|
||||
},
|
||||
|
||||
_handleCommentReply(e) {
|
||||
this._processCommentReply();
|
||||
},
|
||||
|
||||
_handleCommentQuote(e) {
|
||||
this._processCommentReply(true);
|
||||
},
|
||||
|
||||
_handleCommentAck(e) {
|
||||
const comment = this._lastComment;
|
||||
this._createReplyComment(comment, 'Ack', false, false);
|
||||
},
|
||||
|
||||
_handleCommentDone(e) {
|
||||
const comment = this._lastComment;
|
||||
this._createReplyComment(comment, 'Done', false, false);
|
||||
},
|
||||
|
||||
_handleCommentFix(e) {
|
||||
const comment = e.detail.comment;
|
||||
const msg = comment.message;
|
||||
const quoteStr = '> ' + msg.replace(NEWLINE_PATTERN, '\n> ') + '\n\n';
|
||||
const response = quoteStr + 'Please Fix';
|
||||
this._createReplyComment(comment, response, false, true);
|
||||
},
|
||||
|
||||
_commentElWithDraftID(id) {
|
||||
const els = Polymer.dom(this.root).querySelectorAll('gr-comment');
|
||||
for (const el of els) {
|
||||
if (el.comment.id === id || el.comment.__draftID === id) {
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
_newReply(inReplyTo, opt_lineNum, opt_message, opt_unresolved,
|
||||
opt_range) {
|
||||
const d = this._newDraft(opt_lineNum);
|
||||
d.in_reply_to = inReplyTo;
|
||||
d.range = opt_range;
|
||||
if (opt_message != null) {
|
||||
d.message = opt_message;
|
||||
}
|
||||
if (opt_unresolved !== undefined) {
|
||||
d.unresolved = opt_unresolved;
|
||||
}
|
||||
return d;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {number=} opt_lineNum
|
||||
* @param {!Object=} opt_range
|
||||
*/
|
||||
_newDraft(opt_lineNum, opt_range) {
|
||||
const d = {
|
||||
__draft: true,
|
||||
__draftID: Math.random().toString(36),
|
||||
__date: new Date(),
|
||||
path: this.path,
|
||||
patchNum: this.patchNum,
|
||||
side: this._getSide(this.isOnParent),
|
||||
__commentSide: this.commentSide,
|
||||
};
|
||||
if (opt_lineNum) {
|
||||
d.line = opt_lineNum;
|
||||
}
|
||||
if (opt_range) {
|
||||
d.range = opt_range;
|
||||
}
|
||||
if (this.parentIndex) {
|
||||
d.parent = this.parentIndex;
|
||||
}
|
||||
return d;
|
||||
},
|
||||
|
||||
_getSide(isOnParent) {
|
||||
if (isOnParent) { return 'PARENT'; }
|
||||
return 'REVISION';
|
||||
},
|
||||
|
||||
_computeRootId(comments) {
|
||||
// Keep the root ID even if the comment was removed, so that notification
|
||||
// to sync will know which thread to remove.
|
||||
if (!comments.base.length) { return this.rootId; }
|
||||
const rootComment = comments.base[0];
|
||||
return rootComment.id || rootComment.__draftID;
|
||||
},
|
||||
|
||||
_handleCommentDiscard(e) {
|
||||
const diffCommentEl = Polymer.dom(e).rootTarget;
|
||||
const comment = diffCommentEl.comment;
|
||||
const idx = this._indexOf(comment, this.comments);
|
||||
if (idx == -1) {
|
||||
throw Error('Cannot find comment ' +
|
||||
JSON.stringify(diffCommentEl.comment));
|
||||
}
|
||||
this.splice('comments', idx, 1);
|
||||
if (this.comments.length === 0) {
|
||||
this.fireRemoveSelf();
|
||||
}
|
||||
this._handleCommentSavedOrDiscarded(e);
|
||||
|
||||
// Check to see if there are any other open comments getting edited and
|
||||
// set the local storage value to its message value.
|
||||
for (const changeComment of this.comments) {
|
||||
if (changeComment.__editing) {
|
||||
const commentLocation = {
|
||||
changeNum: this.changeNum,
|
||||
patchNum: this.patchNum,
|
||||
path: changeComment.path,
|
||||
line: changeComment.line,
|
||||
};
|
||||
return this.$.storage.setDraftComment(commentLocation,
|
||||
changeComment.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_handleCommentSavedOrDiscarded(e) {
|
||||
this.dispatchEvent(new CustomEvent('thread-changed',
|
||||
{detail: {rootId: this.rootId, path: this.path},
|
||||
bubbles: false}));
|
||||
},
|
||||
|
||||
_handleCommentUpdate(e) {
|
||||
const comment = e.detail.comment;
|
||||
const index = this._indexOf(comment, this.comments);
|
||||
if (index === -1) {
|
||||
// This should never happen: comment belongs to another thread.
|
||||
console.warn('Comment update for another comment thread.');
|
||||
return;
|
||||
}
|
||||
this.set(['comments', index], comment);
|
||||
// Because of the way we pass these comment objects around by-ref, in
|
||||
// combination with the fact that Polymer does dirty checking in
|
||||
// observers, the this.set() call above will not cause a thread update in
|
||||
// some situations.
|
||||
this.updateThreadProperties();
|
||||
},
|
||||
|
||||
_indexOf(comment, arr) {
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
const c = arr[i];
|
||||
if ((c.__draftID != null && c.__draftID == comment.__draftID) ||
|
||||
(c.id != null && c.id == comment.id)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
},
|
||||
|
||||
_computeHostClass(unresolved) {
|
||||
return unresolved ? 'unresolved' : '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Load the project config when a project name has been provided.
|
||||
* @param {string} name The project name.
|
||||
*/
|
||||
_projectNameChanged(name) {
|
||||
if (!name) { return; }
|
||||
this.$.restAPI.getProjectConfig(name).then(config => {
|
||||
this._projectConfig = config;
|
||||
});
|
||||
},
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,751 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
@license
|
||||
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.
|
||||
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.
|
||||
-->
|
||||
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>gr-comment-thread</title>
|
||||
|
||||
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||||
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||||
<script src="../../../scripts/util.js"></script>
|
||||
|
||||
<link rel="import" href="gr-comment-thread.html">
|
||||
|
||||
<script>void(0);</script>
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<gr-comment-thread></gr-comment-thread>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<test-fixture id="withComment">
|
||||
<template>
|
||||
<gr-comment-thread></gr-comment-thread>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script>
|
||||
suite('gr-comment-thread tests', () => {
|
||||
let element;
|
||||
let sandbox;
|
||||
|
||||
setup(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
stub('gr-rest-api-interface', {
|
||||
getLoggedIn() { return Promise.resolve(false); },
|
||||
});
|
||||
sandbox = sinon.sandbox.create();
|
||||
element = fixture('basic');
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('comments are sorted correctly', () => {
|
||||
const comments = [
|
||||
{
|
||||
message: 'i like you, too',
|
||||
in_reply_to: 'sallys_confession',
|
||||
__date: new Date('2015-12-25'),
|
||||
}, {
|
||||
id: 'sallys_confession',
|
||||
message: 'i like you, jack',
|
||||
updated: '2015-12-24 15:00:20.396000000',
|
||||
}, {
|
||||
id: 'sally_to_dr_finklestein',
|
||||
message: 'i’m running away',
|
||||
updated: '2015-10-31 09:00:20.396000000',
|
||||
}, {
|
||||
id: 'sallys_defiance',
|
||||
in_reply_to: 'sally_to_dr_finklestein',
|
||||
message: 'i will poison you so i can get away',
|
||||
updated: '2015-10-31 15:00:20.396000000',
|
||||
}, {
|
||||
id: 'dr_finklesteins_response',
|
||||
in_reply_to: 'sally_to_dr_finklestein',
|
||||
message: 'no i will pull a thread and your arm will fall off',
|
||||
updated: '2015-10-31 11:00:20.396000000',
|
||||
}, {
|
||||
id: 'sallys_mission',
|
||||
message: 'i have to find santa',
|
||||
updated: '2015-12-24 15:00:20.396000000',
|
||||
},
|
||||
];
|
||||
const results = element._sortedComments(comments);
|
||||
assert.deepEqual(results, [
|
||||
{
|
||||
id: 'sally_to_dr_finklestein',
|
||||
message: 'i’m running away',
|
||||
updated: '2015-10-31 09:00:20.396000000',
|
||||
}, {
|
||||
id: 'dr_finklesteins_response',
|
||||
in_reply_to: 'sally_to_dr_finklestein',
|
||||
message: 'no i will pull a thread and your arm will fall off',
|
||||
updated: '2015-10-31 11:00:20.396000000',
|
||||
}, {
|
||||
id: 'sallys_defiance',
|
||||
in_reply_to: 'sally_to_dr_finklestein',
|
||||
message: 'i will poison you so i can get away',
|
||||
updated: '2015-10-31 15:00:20.396000000',
|
||||
}, {
|
||||
id: 'sallys_confession',
|
||||
message: 'i like you, jack',
|
||||
updated: '2015-12-24 15:00:20.396000000',
|
||||
}, {
|
||||
id: 'sallys_mission',
|
||||
message: 'i have to find santa',
|
||||
updated: '2015-12-24 15:00:20.396000000',
|
||||
}, {
|
||||
message: 'i like you, too',
|
||||
in_reply_to: 'sallys_confession',
|
||||
__date: new Date('2015-12-25'),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('addOrEditDraft w/ edit draft', () => {
|
||||
element.comments = [{
|
||||
id: 'jacks_reply',
|
||||
message: 'i like you, too',
|
||||
in_reply_to: 'sallys_confession',
|
||||
updated: '2015-12-25 15:00:20.396000000',
|
||||
__draft: true,
|
||||
}];
|
||||
const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
|
||||
() => { return {}; });
|
||||
const addDraftStub = sandbox.stub(element, 'addDraft');
|
||||
|
||||
element.addOrEditDraft(123);
|
||||
|
||||
assert.isTrue(commentElStub.called);
|
||||
assert.isFalse(addDraftStub.called);
|
||||
});
|
||||
|
||||
test('addOrEditDraft w/o edit draft', () => {
|
||||
element.comments = [];
|
||||
const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
|
||||
() => { return {}; });
|
||||
const addDraftStub = sandbox.stub(element, 'addDraft');
|
||||
|
||||
element.addOrEditDraft(123);
|
||||
|
||||
assert.isFalse(commentElStub.called);
|
||||
assert.isTrue(addDraftStub.called);
|
||||
});
|
||||
|
||||
test('_hideActions', () => {
|
||||
let showActions = true;
|
||||
const lastComment = {};
|
||||
assert.equal(element._hideActions(showActions, lastComment), false);
|
||||
showActions = false;
|
||||
assert.equal(element._hideActions(showActions, lastComment), true);
|
||||
showActions = true;
|
||||
lastComment.__draft = true;
|
||||
assert.equal(element._hideActions(showActions, lastComment), true);
|
||||
});
|
||||
|
||||
test('setting project name loads the project config', done => {
|
||||
const projectName = 'foo/bar/baz';
|
||||
const getProjectStub = sandbox.stub(element.$.restAPI, 'getProjectConfig')
|
||||
.returns(Promise.resolve({}));
|
||||
element.projectName = projectName;
|
||||
flush(() => {
|
||||
assert.isTrue(getProjectStub.calledWithExactly(projectName));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('optionally show file path', () => {
|
||||
// Path info doesn't exist when showFilePath is false. Because it's in a
|
||||
// dom-if it is not yet in the dom.
|
||||
assert.isNotOk(element.$$('.pathInfo'));
|
||||
|
||||
sandbox.stub(Gerrit.Nav, 'getUrlForDiffById');
|
||||
element.changeNum = 123;
|
||||
element.projectName = 'test project';
|
||||
element.path = 'path/to/file';
|
||||
element.patchNum = 3;
|
||||
element.lineNum = 5;
|
||||
element.showFilePath = true;
|
||||
flushAsynchronousOperations();
|
||||
assert.isOk(element.$$('.pathInfo'));
|
||||
assert.notEqual(getComputedStyle(element.$$('.pathInfo')).display,
|
||||
'none');
|
||||
assert.isTrue(Gerrit.Nav.getUrlForDiffById.lastCall.calledWithExactly(
|
||||
element.changeNum, element.projectName, element.path,
|
||||
element.patchNum, null, element.lineNum));
|
||||
});
|
||||
|
||||
test('_computeDisplayPath', () => {
|
||||
const path = 'path/to/file';
|
||||
assert.equal(element._computeDisplayPath(path), 'path/to/file');
|
||||
|
||||
element.lineNum = 5;
|
||||
assert.equal(element._computeDisplayPath(path), 'path/to/file#5');
|
||||
});
|
||||
});
|
||||
|
||||
suite('comment action tests', () => {
|
||||
let element;
|
||||
let sandbox;
|
||||
|
||||
setup(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
stub('gr-rest-api-interface', {
|
||||
getLoggedIn() { return Promise.resolve(false); },
|
||||
saveDiffDraft() {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
text() {
|
||||
return Promise.resolve(')]}\'\n' +
|
||||
JSON.stringify({
|
||||
id: '7afa4931_de3d65bd',
|
||||
path: '/path/to/file.txt',
|
||||
line: 5,
|
||||
in_reply_to: 'baf0414d_60047215',
|
||||
updated: '2015-12-21 02:01:10.850000000',
|
||||
message: 'Done',
|
||||
}));
|
||||
},
|
||||
});
|
||||
},
|
||||
deleteDiffDraft() { return Promise.resolve({ok: true}); },
|
||||
});
|
||||
element = fixture('withComment');
|
||||
element.comments = [{
|
||||
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',
|
||||
path: '/path/to/file.txt',
|
||||
}];
|
||||
flushAsynchronousOperations();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('reply', () => {
|
||||
const commentEl = element.$$('gr-comment');
|
||||
const reportStub = sandbox.stub(element.$.reporting,
|
||||
'recordDraftInteraction');
|
||||
assert.ok(commentEl);
|
||||
|
||||
const replyBtn = element.$.replyBtn;
|
||||
MockInteractions.tap(replyBtn);
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const drafts = element._orderedComments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 1);
|
||||
assert.notOk(drafts[0].message, 'message should be empty');
|
||||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||||
assert.isTrue(reportStub.calledOnce);
|
||||
});
|
||||
|
||||
test('quote reply', () => {
|
||||
const commentEl = element.$$('gr-comment');
|
||||
const reportStub = sandbox.stub(element.$.reporting,
|
||||
'recordDraftInteraction');
|
||||
assert.ok(commentEl);
|
||||
|
||||
const quoteBtn = element.$.quoteBtn;
|
||||
MockInteractions.tap(quoteBtn);
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const drafts = element._orderedComments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 1);
|
||||
assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
|
||||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||||
assert.isTrue(reportStub.calledOnce);
|
||||
});
|
||||
|
||||
test('quote reply multiline', () => {
|
||||
const reportStub = sandbox.stub(element.$.reporting,
|
||||
'recordDraftInteraction');
|
||||
element.comments = [{
|
||||
author: {
|
||||
name: 'Mr. Peanutbutter',
|
||||
email: 'tenn1sballchaser@aol.com',
|
||||
},
|
||||
id: 'baf0414d_60047215',
|
||||
line: 5,
|
||||
message: 'is this a crossover episode!?\nIt might be!',
|
||||
updated: '2015-12-08 19:48:33.843000000',
|
||||
}];
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const commentEl = element.$$('gr-comment');
|
||||
assert.ok(commentEl);
|
||||
|
||||
const quoteBtn = element.$.quoteBtn;
|
||||
MockInteractions.tap(quoteBtn);
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const drafts = element._orderedComments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 1);
|
||||
assert.equal(drafts[0].message,
|
||||
'> is this a crossover episode!?\n> It might be!\n\n');
|
||||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||||
assert.isTrue(reportStub.calledOnce);
|
||||
});
|
||||
|
||||
test('ack', done => {
|
||||
const reportStub = sandbox.stub(element.$.reporting,
|
||||
'recordDraftInteraction');
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
|
||||
const commentEl = element.$$('gr-comment');
|
||||
assert.ok(commentEl);
|
||||
|
||||
const ackBtn = element.$.ackBtn;
|
||||
MockInteractions.tap(ackBtn);
|
||||
flush(() => {
|
||||
const drafts = element.comments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 1);
|
||||
assert.equal(drafts[0].message, 'Ack');
|
||||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||||
assert.equal(drafts[0].unresolved, false);
|
||||
assert.isTrue(reportStub.calledOnce);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('done', done => {
|
||||
const reportStub = sandbox.stub(element.$.reporting,
|
||||
'recordDraftInteraction');
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
const commentEl = element.$$('gr-comment');
|
||||
assert.ok(commentEl);
|
||||
|
||||
const doneBtn = element.$.doneBtn;
|
||||
MockInteractions.tap(doneBtn);
|
||||
flush(() => {
|
||||
const drafts = element.comments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 1);
|
||||
assert.equal(drafts[0].message, 'Done');
|
||||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||||
assert.isFalse(drafts[0].unresolved);
|
||||
assert.isTrue(reportStub.calledOnce);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('save', done => {
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
element.path = '/path/to/file.txt';
|
||||
const commentEl = element.$$('gr-comment');
|
||||
assert.ok(commentEl);
|
||||
|
||||
const saveOrDiscardStub = sandbox.stub();
|
||||
element.addEventListener('thread-changed', saveOrDiscardStub);
|
||||
element.$$('gr-comment')._fireSave();
|
||||
|
||||
flush(() => {
|
||||
assert.isTrue(saveOrDiscardStub.called);
|
||||
assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
|
||||
'baf0414d_60047215');
|
||||
assert.equal(element.rootId, 'baf0414d_60047215');
|
||||
assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
|
||||
'/path/to/file.txt');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('please fix', done => {
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
const commentEl = element.$$('gr-comment');
|
||||
assert.ok(commentEl);
|
||||
commentEl.addEventListener('create-fix-comment', () => {
|
||||
const drafts = element._orderedComments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 1);
|
||||
assert.equal(
|
||||
drafts[0].message, '> is this a crossover episode!?\n\nPlease Fix');
|
||||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||||
assert.isTrue(drafts[0].unresolved);
|
||||
done();
|
||||
});
|
||||
commentEl.fire('create-fix-comment', {comment: commentEl.comment},
|
||||
{bubbles: false});
|
||||
});
|
||||
|
||||
test('discard', done => {
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
element.path = '/path/to/file.txt';
|
||||
element.push('comments', element._newReply(
|
||||
element.comments[0].id,
|
||||
element.comments[0].line,
|
||||
element.comments[0].path,
|
||||
'it’s pronouced jiff, not giff'));
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const saveOrDiscardStub = sandbox.stub();
|
||||
element.addEventListener('thread-changed', saveOrDiscardStub);
|
||||
const draftEl =
|
||||
Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
|
||||
assert.ok(draftEl);
|
||||
draftEl.addEventListener('comment-discard', () => {
|
||||
const drafts = element.comments.filter(c => {
|
||||
return c.__draft == true;
|
||||
});
|
||||
assert.equal(drafts.length, 0);
|
||||
assert.isTrue(saveOrDiscardStub.called);
|
||||
assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
|
||||
element.rootId);
|
||||
assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
|
||||
element.path);
|
||||
done();
|
||||
});
|
||||
draftEl.fire('comment-discard', {comment: draftEl.comment},
|
||||
{bubbles: false});
|
||||
});
|
||||
|
||||
test('discard with a single comment still fires event with previous rootId',
|
||||
done => {
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
element.path = '/path/to/file.txt';
|
||||
element.comments = [];
|
||||
element.addOrEditDraft('1');
|
||||
flushAsynchronousOperations();
|
||||
const rootId = element.rootId;
|
||||
assert.isOk(rootId);
|
||||
|
||||
const saveOrDiscardStub = sandbox.stub();
|
||||
element.addEventListener('thread-changed', saveOrDiscardStub);
|
||||
const draftEl =
|
||||
Polymer.dom(element.root).querySelectorAll('gr-comment')[0];
|
||||
assert.ok(draftEl);
|
||||
draftEl.addEventListener('comment-discard', () => {
|
||||
assert.equal(element.comments.length, 0);
|
||||
assert.isTrue(saveOrDiscardStub.called);
|
||||
assert.equal(saveOrDiscardStub.lastCall.args[0].detail.rootId,
|
||||
rootId);
|
||||
assert.equal(saveOrDiscardStub.lastCall.args[0].detail.path,
|
||||
element.path);
|
||||
done();
|
||||
});
|
||||
draftEl.fire('comment-discard', {comment: draftEl.comment},
|
||||
{bubbles: false});
|
||||
});
|
||||
|
||||
test('first editing comment does not add __otherEditing attribute', () => {
|
||||
element.comments = [{
|
||||
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',
|
||||
__draft: true,
|
||||
}];
|
||||
|
||||
const replyBtn = element.$.replyBtn;
|
||||
MockInteractions.tap(replyBtn);
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const editing = element._orderedComments.filter(c => {
|
||||
return c.__editing == true;
|
||||
});
|
||||
assert.equal(editing.length, 1);
|
||||
assert.equal(!!editing[0].__otherEditing, false);
|
||||
});
|
||||
|
||||
test('When not editing other comments, local storage not set' +
|
||||
' after discard', done => {
|
||||
element.changeNum = '42';
|
||||
element.patchNum = '1';
|
||||
element.comments = [{
|
||||
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:31.843000000',
|
||||
},
|
||||
{
|
||||
author: {
|
||||
name: 'Mr. Peanutbutter',
|
||||
email: 'tenn1sballchaser@aol.com',
|
||||
},
|
||||
__draftID: '1',
|
||||
in_reply_to: 'baf0414d_60047215',
|
||||
line: 5,
|
||||
message: 'yes',
|
||||
updated: '2015-12-08 19:48:32.843000000',
|
||||
__draft: true,
|
||||
__editing: true,
|
||||
},
|
||||
{
|
||||
author: {
|
||||
name: 'Mr. Peanutbutter',
|
||||
email: 'tenn1sballchaser@aol.com',
|
||||
},
|
||||
__draftID: '2',
|
||||
in_reply_to: 'baf0414d_60047215',
|
||||
line: 5,
|
||||
message: 'no',
|
||||
updated: '2015-12-08 19:48:33.843000000',
|
||||
__draft: true,
|
||||
}];
|
||||
const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
|
||||
flushAsynchronousOperations();
|
||||
|
||||
const draftEl =
|
||||
Polymer.dom(element.root).querySelectorAll('gr-comment')[1];
|
||||
assert.ok(draftEl);
|
||||
draftEl.addEventListener('comment-discard', () => {
|
||||
assert.isFalse(storageStub.called);
|
||||
storageStub.restore();
|
||||
done();
|
||||
});
|
||||
draftEl.fire('comment-discard', {comment: draftEl.comment},
|
||||
{bubbles: false});
|
||||
});
|
||||
|
||||
test('comment-update', () => {
|
||||
const commentEl = element.$$('gr-comment');
|
||||
const updatedComment = {
|
||||
id: element.comments[0].id,
|
||||
foo: 'bar',
|
||||
};
|
||||
commentEl.fire('comment-update', {comment: updatedComment});
|
||||
assert.strictEqual(element.comments[0], updatedComment);
|
||||
});
|
||||
|
||||
suite('jack and sally comment data test consolidation', () => {
|
||||
setup(() => {
|
||||
element.comments = [
|
||||
{
|
||||
id: 'jacks_reply',
|
||||
message: 'i like you, too',
|
||||
in_reply_to: 'sallys_confession',
|
||||
updated: '2015-12-25 15:00:20.396000000',
|
||||
unresolved: false,
|
||||
}, {
|
||||
id: 'sallys_confession',
|
||||
in_reply_to: 'nonexistent_comment',
|
||||
message: 'i like you, jack',
|
||||
updated: '2015-12-24 15:00:20.396000000',
|
||||
}, {
|
||||
id: 'sally_to_dr_finklestein',
|
||||
in_reply_to: 'nonexistent_comment',
|
||||
message: 'i’m running away',
|
||||
updated: '2015-10-31 09:00:20.396000000',
|
||||
}, {
|
||||
id: 'sallys_defiance',
|
||||
message: 'i will poison you so i can get away',
|
||||
updated: '2015-10-31 15:00:20.396000000',
|
||||
}];
|
||||
});
|
||||
|
||||
test('orphan replies', () => {
|
||||
assert.equal(4, element._orderedComments.length);
|
||||
});
|
||||
|
||||
test('keyboard shortcuts', () => {
|
||||
const expandCollapseStub =
|
||||
sinon.stub(element, '_expandCollapseComments');
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
|
||||
assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
|
||||
|
||||
MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
|
||||
assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
|
||||
});
|
||||
|
||||
test('comment in_reply_to is either null or most recent comment', () => {
|
||||
element._createReplyComment(element.comments[3], 'dummy', true);
|
||||
flushAsynchronousOperations();
|
||||
assert.equal(element._orderedComments.length, 5);
|
||||
assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
|
||||
});
|
||||
|
||||
test('resolvable comments', () => {
|
||||
assert.isFalse(element.unresolved);
|
||||
element._createReplyComment(element.comments[3], 'dummy', true, true);
|
||||
flushAsynchronousOperations();
|
||||
assert.isTrue(element.unresolved);
|
||||
});
|
||||
|
||||
test('_setInitialExpandedState', () => {
|
||||
element.unresolved = true;
|
||||
element._setInitialExpandedState();
|
||||
for (let i = 0; i < element.comments.length; i++) {
|
||||
assert.isFalse(element.comments[i].collapsed);
|
||||
}
|
||||
element.unresolved = false;
|
||||
element._setInitialExpandedState();
|
||||
for (let i = 0; i < element.comments.length; i++) {
|
||||
assert.isTrue(element.comments[i].collapsed);
|
||||
}
|
||||
for (let i = 0; i < element.comments.length; i++) {
|
||||
element.comments[i].robot_id = 123;
|
||||
}
|
||||
element._setInitialExpandedState();
|
||||
for (let i = 0; i < element.comments.length; i++) {
|
||||
assert.isFalse(element.comments[i].collapsed);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('_computeHostClass', () => {
|
||||
assert.equal(element._computeHostClass(true), 'unresolved');
|
||||
assert.equal(element._computeHostClass(false), '');
|
||||
});
|
||||
|
||||
test('addDraft sets unresolved state correctly', () => {
|
||||
let unresolved = true;
|
||||
element.comments = [];
|
||||
element.addDraft(null, null, unresolved);
|
||||
assert.equal(element.comments[0].unresolved, true);
|
||||
|
||||
unresolved = false; // comment should get added as actually resolved.
|
||||
element.comments = [];
|
||||
element.addDraft(null, null, unresolved);
|
||||
assert.equal(element.comments[0].unresolved, false);
|
||||
|
||||
element.comments = [];
|
||||
element.addDraft();
|
||||
assert.equal(element.comments[0].unresolved, true);
|
||||
});
|
||||
|
||||
test('_newDraft', () => {
|
||||
element.commentSide = 'left';
|
||||
element.patchNum = 3;
|
||||
const draft = element._newDraft();
|
||||
assert.equal(draft.__commentSide, 'left');
|
||||
assert.equal(draft.patchNum, 3);
|
||||
});
|
||||
|
||||
test('new comment gets created', () => {
|
||||
element.comments = [];
|
||||
element.addOrEditDraft(1);
|
||||
assert.equal(element.comments.length, 1);
|
||||
// Mock a submitted comment.
|
||||
element.comments[0].id = element.comments[0].__draftID;
|
||||
element.comments[0].__draft = false;
|
||||
element.addOrEditDraft(1);
|
||||
assert.equal(element.comments.length, 2);
|
||||
});
|
||||
|
||||
test('unresolved label', () => {
|
||||
element.unresolved = false;
|
||||
assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
|
||||
element.unresolved = true;
|
||||
assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
|
||||
});
|
||||
|
||||
test('draft comments are at the end of orderedComments', () => {
|
||||
element.comments = [{
|
||||
author: {
|
||||
name: 'Mr. Peanutbutter',
|
||||
email: 'tenn1sballchaser@aol.com',
|
||||
},
|
||||
id: 2,
|
||||
line: 5,
|
||||
message: 'Earlier draft',
|
||||
updated: '2015-12-08 19:48:33.843000000',
|
||||
__draft: true,
|
||||
},
|
||||
{
|
||||
author: {
|
||||
name: 'Mr. Peanutbutter2',
|
||||
email: 'tenn1sballchaser@aol.com',
|
||||
},
|
||||
id: 1,
|
||||
line: 5,
|
||||
message: 'This comment was left last but is not a draft',
|
||||
updated: '2015-12-10 19:48:33.843000000',
|
||||
},
|
||||
{
|
||||
author: {
|
||||
name: 'Mr. Peanutbutter2',
|
||||
email: 'tenn1sballchaser@aol.com',
|
||||
},
|
||||
id: 3,
|
||||
line: 5,
|
||||
message: 'Later draft',
|
||||
updated: '2015-12-09 19:48:33.843000000',
|
||||
__draft: true,
|
||||
}];
|
||||
assert.equal(element._orderedComments[0].id, '1');
|
||||
assert.equal(element._orderedComments[1].id, '2');
|
||||
assert.equal(element._orderedComments[2].id, '3');
|
||||
});
|
||||
|
||||
test('reflects lineNum and commentSide to attributes', () => {
|
||||
element.lineNum = 7;
|
||||
element.commentSide = 'left';
|
||||
|
||||
assert.equal(element.getAttribute('line-num'), '7');
|
||||
assert.equal(element.getAttribute('comment-side'), 'left');
|
||||
});
|
||||
|
||||
test('reflects range to JSON serialized attribute if set', () => {
|
||||
element.range = {
|
||||
start_line: 4,
|
||||
end_line: 5,
|
||||
start_character: 6,
|
||||
end_character: 7,
|
||||
};
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(element.getAttribute('range')),
|
||||
{start_line: 4, end_line: 5, start_character: 6, end_character: 7});
|
||||
});
|
||||
|
||||
test('removes range attribute if range is unset', () => {
|
||||
element.range = {
|
||||
start_line: 4,
|
||||
end_line: 5,
|
||||
start_character: 6,
|
||||
end_character: 7,
|
||||
};
|
||||
element.range = undefined;
|
||||
|
||||
assert.notOk(element.hasAttribute('range'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user