Merge changes Ie43a85d1,Idec8057a,Ie5f86a27,Iedf92e10,I0ad4960f
* changes: Pick change 263458 into experimental version of ChangeLog Pick change 259816 into experimental version of ChangeLog Simplify experimental gr-messages-list Remove the messageControlsContainer from Change Log Add an experimental version of the Change Log
This commit is contained in:
@@ -43,6 +43,7 @@ import '../gr-file-list-header/gr-file-list-header.js';
|
||||
import '../gr-file-list/gr-file-list.js';
|
||||
import '../gr-included-in-dialog/gr-included-in-dialog.js';
|
||||
import '../gr-messages-list/gr-messages-list.js';
|
||||
import '../gr-messages-list/gr-messages-list-experimental.js';
|
||||
import '../gr-related-changes-list/gr-related-changes-list.js';
|
||||
import '../../diff/gr-apply-fix-dialog/gr-apply-fix-dialog.js';
|
||||
import '../gr-reply-dialog/gr-reply-dialog.js';
|
||||
@@ -67,6 +68,7 @@ import {RevisionInfo} from '../../shared/revision-info/revision-info.js';
|
||||
|
||||
import {PrimaryTabs, SecondaryTabs} from '../../../constants/constants.js';
|
||||
import {NO_ROBOT_COMMENTS_THREADS_MSG} from '../../../constants/messages.js';
|
||||
import {appContext} from '../../../services/app-context.js';
|
||||
|
||||
const CHANGE_ID_ERROR = {
|
||||
MISMATCH: 'mismatch',
|
||||
@@ -444,6 +446,11 @@ class GrChangeView extends mixinBehaviors( [
|
||||
};
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.flagsService = appContext.flagsService;
|
||||
}
|
||||
|
||||
/** @override */
|
||||
created() {
|
||||
super.created();
|
||||
@@ -531,8 +538,14 @@ class GrChangeView extends mixinBehaviors( [
|
||||
}
|
||||
}
|
||||
|
||||
_isChangeLogExperimentEnabled() {
|
||||
return this.flagsService.isEnabled('UiFeature__cleaner_changelog');
|
||||
}
|
||||
|
||||
get messagesList() {
|
||||
return this.shadowRoot.querySelector('gr-messages-list');
|
||||
const tagName = this._isChangeLogExperimentEnabled()
|
||||
? 'gr-messages-list-experimental' : 'gr-messages-list';
|
||||
return this.shadowRoot.querySelector(tagName);
|
||||
}
|
||||
|
||||
get threadList() {
|
||||
|
||||
@@ -216,7 +216,8 @@ export const htmlTemplate = html`
|
||||
--paper-tab-ink: var(--link-color);
|
||||
}
|
||||
gr-thread-list,
|
||||
gr-messages-list {
|
||||
gr-messages-list,
|
||||
gr-messages-list-experimental {
|
||||
display: block;
|
||||
}
|
||||
gr-thread-list {
|
||||
@@ -500,7 +501,12 @@ export const htmlTemplate = html`
|
||||
</paper-tabs>
|
||||
<section class="changeLog">
|
||||
<template is="dom-if" if="[[_isTabActive(_constants.SecondaryTabs.CHANGE_LOG, _activeTabs)]]">
|
||||
<gr-messages-list class="hideOnMobileOverlay" change-num="[[_changeNum]]" labels="[[_change.labels]]" messages="[[_change.messages]]" reviewer-updates="[[_change.reviewer_updates]]" change-comments="[[_changeComments]]" project-name="[[_change.project]]" show-reply-buttons="[[_loggedIn]]" on-message-anchor-tap="_handleMessageAnchorTap" on-reply="_handleMessageReply"></gr-messages-list>
|
||||
<template is="dom-if" if="[[!_isChangeLogExperimentEnabled()]]">
|
||||
<gr-messages-list class="hideOnMobileOverlay" change-num="[[_changeNum]]" labels="[[_change.labels]]" messages="[[_change.messages]]" reviewer-updates="[[_change.reviewer_updates]]" change-comments="[[_changeComments]]" project-name="[[_change.project]]" show-reply-buttons="[[_loggedIn]]" on-message-anchor-tap="_handleMessageAnchorTap" on-reply="_handleMessageReply"></gr-messages-list>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_isChangeLogExperimentEnabled()]]">
|
||||
<gr-messages-list-experimental class="hideOnMobileOverlay" change-num="[[_changeNum]]" labels="[[_change.labels]]" messages="[[_change.messages]]" reviewer-updates="[[_change.reviewer_updates]]" change-comments="[[_changeComments]]" project-name="[[_change.project]]" show-reply-buttons="[[_loggedIn]]" on-message-anchor-tap="_handleMessageAnchorTap" on-reply="_handleMessageReply"></gr-messages-list-experimental>
|
||||
</template>
|
||||
</template>
|
||||
<template is="dom-if" if="[[_isTabActive(_constants.SecondaryTabs.COMMENT_THREADS, _activeTabs)]]">
|
||||
<gr-thread-list threads="[[_commentThreads]]" change="[[_change]]" change-num="[[_changeNum]]" logged-in="[[_loggedIn]]" only-show-robot-comments-with-human-reply="" on-thread-list-modified="_handleReloadDiffComments"></gr-thread-list>
|
||||
|
||||
@@ -287,8 +287,8 @@ class GrMessage extends GestureEventListeners(
|
||||
return hideAutomated && isAutomated;
|
||||
}
|
||||
|
||||
_computeIsReviewerUpdate(event) {
|
||||
return event.type === 'REVIEWER_UPDATE';
|
||||
_computeIsReviewerUpdate(message) {
|
||||
return message.type === 'REVIEWER_UPDATE';
|
||||
}
|
||||
|
||||
_getScores(message, labelExtremes) {
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* @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 '../../../scripts/bundled-polymer.js';
|
||||
|
||||
import '@polymer/paper-toggle-button/paper-toggle-button.js';
|
||||
import '../../core/gr-reporting/gr-reporting.js';
|
||||
import '../../shared/gr-button/gr-button.js';
|
||||
import '../gr-message/gr-message.js';
|
||||
import '../../../styles/shared-styles.js';
|
||||
import {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-messages-list-experimental_html.js';
|
||||
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
|
||||
import {util} from '../../../scripts/util.js';
|
||||
|
||||
/**
|
||||
* The content of the enum is also used in the UI for the button text.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
const ExpandAllState = {
|
||||
EXPAND_ALL: 'Expand All',
|
||||
COLLAPSE_ALL: 'Collapse All',
|
||||
};
|
||||
|
||||
/**
|
||||
* @extends Polymer.Element
|
||||
*/
|
||||
class GrMessagesListExperimental extends mixinBehaviors( [
|
||||
KeyboardShortcutBehavior,
|
||||
], GestureEventListeners(
|
||||
LegacyElementMixin(
|
||||
PolymerElement))) {
|
||||
static get template() { return htmlTemplate; }
|
||||
|
||||
static get is() { return 'gr-messages-list-experimental'; }
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
changeNum: Number,
|
||||
/**
|
||||
* These are just the change messages. They are combined with reviewer
|
||||
* updates below. So _combinedMessages is the more important property.
|
||||
*/
|
||||
messages: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
/**
|
||||
* These are just the reviewer updates. They are combined with change
|
||||
* messages above. So _combinedMessages is the more important property.
|
||||
*/
|
||||
reviewerUpdates: {
|
||||
type: Array,
|
||||
value() { return []; },
|
||||
},
|
||||
changeComments: Object,
|
||||
projectName: String,
|
||||
showReplyButtons: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
},
|
||||
labels: Object,
|
||||
|
||||
/**
|
||||
* Keeps track of the state of the "Expand All" toggle button. Note that
|
||||
* you can individually expand/collapse some messages without affecting
|
||||
* the toggle button's state.
|
||||
*
|
||||
* @type {ExpandAllState}
|
||||
*/
|
||||
_expandAllState: {
|
||||
type: String,
|
||||
value: ExpandAllState.EXPAND_ALL,
|
||||
},
|
||||
_expandAllTitle: {
|
||||
type: String,
|
||||
computed: '_computeExpandAllTitle(_expandAllState)',
|
||||
},
|
||||
|
||||
_hideAutomated: {
|
||||
type: Boolean,
|
||||
value: false,
|
||||
observer: '_hideAutomatedChanged',
|
||||
},
|
||||
/**
|
||||
* The merged array of change messages and reviewer updates.
|
||||
*/
|
||||
_combinedMessages: {
|
||||
type: Array,
|
||||
computed: '_computeCombinedMessages(messages, reviewerUpdates)',
|
||||
observer: '_combinedMessagesChanged',
|
||||
},
|
||||
|
||||
_labelExtremes: {
|
||||
type: Object,
|
||||
computed: '_computeLabelExtremes(labels.*)',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
scrollToMessage(messageID) {
|
||||
const selector = `[data-message-id="${messageID}"]`;
|
||||
const el = this.shadowRoot.querySelector(selector);
|
||||
|
||||
if (!el && !this._hideAutomated) {
|
||||
console.warn(`Failed to scroll to message: ${messageID}`);
|
||||
return;
|
||||
}
|
||||
if (!el) {
|
||||
this._hideAutomated = false;
|
||||
setTimeout(() => this.scrollToMessage(messageID));
|
||||
return;
|
||||
}
|
||||
|
||||
el.set('message.expanded', true);
|
||||
let top = el.offsetTop;
|
||||
for (let offsetParent = el.offsetParent;
|
||||
offsetParent;
|
||||
offsetParent = offsetParent.offsetParent) {
|
||||
top += offsetParent.offsetTop;
|
||||
}
|
||||
window.scrollTo(0, top);
|
||||
this._highlightEl(el);
|
||||
}
|
||||
|
||||
_isAutomated(message) {
|
||||
const isReviewerUpdate =
|
||||
!!(message.reviewer || message.type === 'REVIEWER_UPDATE');
|
||||
const isAutoGenerated =
|
||||
!!(message.tag && message.tag.startsWith('autogenerated'));
|
||||
return isReviewerUpdate || isAutoGenerated;
|
||||
}
|
||||
|
||||
_hideAutomatedChanged(hideAutomated) {
|
||||
// We have to call render() such that the dom-repeat filter picks up the
|
||||
// change.
|
||||
this.$.messageRepeat.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for the dom-repeat of combinedMessages.
|
||||
*/
|
||||
_isMessageVisible(message) {
|
||||
return !(this._hideAutomated && this._isAutomated(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges change messages and reviewer updates into one array.
|
||||
*/
|
||||
_computeCombinedMessages(messages, reviewerUpdates) {
|
||||
messages = messages || [];
|
||||
reviewerUpdates = reviewerUpdates || [];
|
||||
let mi = 0;
|
||||
let ri = 0;
|
||||
let combinedMessages = [];
|
||||
let mDate;
|
||||
let rDate;
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
messages[i]._index = i;
|
||||
}
|
||||
|
||||
while (mi < messages.length || ri < reviewerUpdates.length) {
|
||||
if (mi >= messages.length) {
|
||||
combinedMessages = combinedMessages.concat(reviewerUpdates.slice(ri));
|
||||
break;
|
||||
}
|
||||
if (ri >= reviewerUpdates.length) {
|
||||
combinedMessages = combinedMessages.concat(messages.slice(mi));
|
||||
break;
|
||||
}
|
||||
mDate = mDate || util.parseDate(messages[mi].date);
|
||||
rDate = rDate || util.parseDate(reviewerUpdates[ri].date);
|
||||
if (rDate < mDate) {
|
||||
combinedMessages.push(reviewerUpdates[ri++]);
|
||||
rDate = null;
|
||||
} else {
|
||||
combinedMessages.push(messages[mi++]);
|
||||
mDate = null;
|
||||
}
|
||||
}
|
||||
combinedMessages.forEach(m => {
|
||||
if (m.expanded === undefined) {
|
||||
m.expanded = false;
|
||||
}
|
||||
});
|
||||
return combinedMessages;
|
||||
}
|
||||
|
||||
_updateExpandedStateOfAllMessages(exp) {
|
||||
if (this._combinedMessages) {
|
||||
for (let i = 0; i < this._combinedMessages.length; i++) {
|
||||
this._combinedMessages[i].expanded = exp;
|
||||
this.notifyPath(`_combinedMessages.${i}.expanded`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_computeExpandAllTitle(_expandAllState) {
|
||||
if (_expandAllState === ExpandAllState.COLLAPSED_ALL) {
|
||||
return this.createTitle(
|
||||
this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
|
||||
}
|
||||
if (_expandAllState === ExpandAllState.EXPAND_ALL) {
|
||||
return this.createTitle(
|
||||
this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
_highlightEl(el) {
|
||||
const highlightedEls =
|
||||
dom(this.root).querySelectorAll('.highlighted');
|
||||
for (const highlighedEl of highlightedEls) {
|
||||
highlighedEl.classList.remove('highlighted');
|
||||
}
|
||||
function handleAnimationEnd() {
|
||||
el.removeEventListener('animationend', handleAnimationEnd);
|
||||
el.classList.remove('highlighted');
|
||||
}
|
||||
el.addEventListener('animationend', handleAnimationEnd);
|
||||
el.classList.add('highlighted');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {boolean} expand
|
||||
*/
|
||||
handleExpandCollapse(expand) {
|
||||
this._expandAllState = expand ? ExpandAllState.COLLAPSE_ALL
|
||||
: ExpandAllState.EXPAND_ALL;
|
||||
this._updateExpandedStateOfAllMessages(expand);
|
||||
}
|
||||
|
||||
_handleExpandCollapseTap(e) {
|
||||
e.preventDefault();
|
||||
this.handleExpandCollapse(
|
||||
this._expandAllState === ExpandAllState.EXPAND_ALL);
|
||||
}
|
||||
|
||||
_handleAnchorClick(e) {
|
||||
this.scrollToMessage(e.detail.id);
|
||||
}
|
||||
|
||||
_hasAutomatedMessages(messages) {
|
||||
if (!messages) { return false; }
|
||||
for (const message of messages) {
|
||||
if (this._isAutomated(message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes message author's file comments for change's message.
|
||||
* Method uses this.messages to find next message and relies on messages
|
||||
* to be sorted by date field descending.
|
||||
*
|
||||
* @param {!Object} changeComments changeComment object, which includes
|
||||
* a method to get all published comments (including robot comments),
|
||||
* which returns a Hash of arrays of comments, filename as key.
|
||||
* @param {!Object} message
|
||||
* @return {!Object} Hash of arrays of comments, filename as key.
|
||||
*/
|
||||
_computeCommentsForMessage(changeComments, message) {
|
||||
if ([changeComments, message].some(arg => arg === undefined)) {
|
||||
return [];
|
||||
}
|
||||
const comments = changeComments.getAllPublishedComments();
|
||||
if (message._index === undefined || !comments || !this.messages) {
|
||||
return [];
|
||||
}
|
||||
const messages = this.messages || [];
|
||||
const index = message._index;
|
||||
const authorId = message.author && message.author._account_id;
|
||||
const mDate = util.parseDate(message.date).getTime();
|
||||
// NB: Messages array has oldest messages first.
|
||||
let nextMDate;
|
||||
if (index > 0) {
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
if (messages[i] && messages[i].author &&
|
||||
messages[i].author._account_id === authorId) {
|
||||
nextMDate = util.parseDate(messages[i].date).getTime();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const msgComments = {};
|
||||
for (const file in comments) {
|
||||
if (!comments.hasOwnProperty(file)) { continue; }
|
||||
const fileComments = comments[file];
|
||||
for (let i = 0; i < fileComments.length; i++) {
|
||||
if (fileComments[i].author &&
|
||||
fileComments[i].author._account_id !== authorId) {
|
||||
continue;
|
||||
}
|
||||
const cDate = util.parseDate(fileComments[i].updated).getTime();
|
||||
if (cDate <= mDate) {
|
||||
if (nextMDate && cDate <= nextMDate) {
|
||||
continue;
|
||||
}
|
||||
msgComments[file] = msgComments[file] || [];
|
||||
msgComments[file].push(fileComments[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return msgComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is for reporting stats only.
|
||||
*/
|
||||
_combinedMessagesChanged(combinedMessages) {
|
||||
if (combinedMessages) {
|
||||
if (combinedMessages.length === 0) return;
|
||||
const tags = combinedMessages.map(
|
||||
message => message.tag || message.type ||
|
||||
(message.comments ? 'comments' : 'none'));
|
||||
const tagsCounted = tags.reduce((acc, val) => {
|
||||
acc[val] = (acc[val] || 0) + 1;
|
||||
return acc;
|
||||
}, {all: combinedMessages.length});
|
||||
this.$.reporting.reportInteraction('messages-count', tagsCounted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a mapping from label name to objects representing the minimum and
|
||||
* maximum possible values for that label.
|
||||
*/
|
||||
_computeLabelExtremes(labelRecord) {
|
||||
const extremes = {};
|
||||
const labels = labelRecord.base;
|
||||
if (!labels) { return extremes; }
|
||||
for (const key of Object.keys(labels)) {
|
||||
if (!labels[key] || !labels[key].values) { continue; }
|
||||
const values = Object.keys(labels[key].values)
|
||||
.map(v => parseInt(v, 10));
|
||||
values.sort((a, b) => a - b);
|
||||
if (!values.length) { continue; }
|
||||
extremes[key] = {min: values[0], max: values[values.length - 1]};
|
||||
}
|
||||
return extremes;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define(GrMessagesListExperimental.is,
|
||||
GrMessagesListExperimental);
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @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';
|
||||
|
||||
export const htmlTemplate = html`
|
||||
<style include="shared-styles">
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.header {
|
||||
align-items: center;
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-s) var(--spacing-l);
|
||||
}
|
||||
.highlighted {
|
||||
animation: 3s fadeOut;
|
||||
}
|
||||
@keyframes fadeOut {
|
||||
0% { background-color: var(--emphasis-color); }
|
||||
100% { background-color: var(--view-background-color); }
|
||||
}
|
||||
.container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
gr-message:not(:last-of-type) {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
gr-message:nth-child(2n) {
|
||||
background-color: var(--background-color-secondary);
|
||||
}
|
||||
gr-message:nth-child(2n+1) {
|
||||
background-color: var(--background-color-tertiary);
|
||||
}
|
||||
</style>
|
||||
<div class="header">
|
||||
<span id="automatedMessageToggleContainer" class="container" hidden\$="[[!_hasAutomatedMessages(messages)]]">
|
||||
<paper-toggle-button id="automatedMessageToggle" checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
|
||||
<span class="transparent separator"></span>
|
||||
</span>
|
||||
<gr-button id="collapse-messages" link="" title="[[_expandAllTitle]]" on-click="_handleExpandCollapseTap">
|
||||
[[_expandAllState]]
|
||||
</gr-button>
|
||||
</div>
|
||||
<template id="messageRepeat" is="dom-repeat" items="[[_combinedMessages]]" as="message" filter="_isMessageVisible">
|
||||
<gr-message change-num="[[changeNum]]"
|
||||
message="[[message]]"
|
||||
comments="[[_computeCommentsForMessage(changeComments, message)]]"
|
||||
project-name="[[projectName]]"
|
||||
show-reply-button="[[showReplyButtons]]"
|
||||
on-message-anchor-tap="_handleAnchorClick"
|
||||
label-extremes="[[_labelExtremes]]"
|
||||
data-message-id\$="[[message.id]]"></gr-message>
|
||||
</template>
|
||||
<gr-reporting id="reporting" category="message-list"></gr-reporting>
|
||||
`;
|
||||
@@ -0,0 +1,430 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
@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.
|
||||
-->
|
||||
|
||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||||
<title>gr-messages-list-experimental</title>
|
||||
|
||||
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
|
||||
|
||||
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
|
||||
<script src="/components/wct-browser-legacy/browser.js"></script>
|
||||
|
||||
<dom-module id="comment-api-mock">
|
||||
<template>
|
||||
<gr-messages-list-experimental
|
||||
id="messagesList"
|
||||
change-comments="[[_changeComments]]"></gr-messages-list-experimental>
|
||||
<gr-comment-api id="commentAPI"></gr-comment-api>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
||||
<test-fixture id="basic">
|
||||
<template>
|
||||
<comment-api-mock>
|
||||
<gr-messages-list-experimental></gr-messages-list-experimental>
|
||||
</comment-api-mock>
|
||||
</template>
|
||||
</test-fixture>
|
||||
|
||||
<script type="module">
|
||||
import '../../../test/common-test-setup.js';
|
||||
import '../../diff/gr-comment-api/gr-comment-api.js';
|
||||
import './gr-messages-list-experimental.js';
|
||||
import '../../diff/gr-comment-api/gr-comment-api-mock_test.js';
|
||||
import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
|
||||
const randomMessage = function(opt_params) {
|
||||
const params = opt_params || {};
|
||||
const author1 = {
|
||||
_account_id: 1115495,
|
||||
name: 'Andrew Bonventre',
|
||||
email: 'andybons@chromium.org',
|
||||
};
|
||||
return {
|
||||
id: params.id || Math.random().toString(),
|
||||
date: params.date || '2016-01-12 20:28:33.038000',
|
||||
message: params.message || Math.random().toString(),
|
||||
_revision_number: params._revision_number || 1,
|
||||
author: params.author || author1,
|
||||
};
|
||||
};
|
||||
|
||||
const randomAutomated = function(opt_params) {
|
||||
return Object.assign({tag: 'autogenerated:gerrit:replace'},
|
||||
randomMessage(opt_params));
|
||||
};
|
||||
|
||||
suite('gr-messages-list-experimental tests', () => {
|
||||
let element;
|
||||
let messages;
|
||||
let sandbox;
|
||||
let commentApiWrapper;
|
||||
|
||||
const getMessages = function() {
|
||||
return dom(element.root).querySelectorAll('gr-message');
|
||||
};
|
||||
|
||||
const author = {
|
||||
_account_id: 42,
|
||||
name: 'Marvin the Paranoid Android',
|
||||
email: 'marvin@sirius.org',
|
||||
};
|
||||
|
||||
const comments = {
|
||||
file1: [
|
||||
{
|
||||
message: 'message text',
|
||||
updated: '2016-09-27 00:18:03.000000000',
|
||||
in_reply_to: '6505d749_f0bec0aa',
|
||||
line: 62,
|
||||
id: '6505d749_10ed44b2',
|
||||
patch_set: 2,
|
||||
author: {
|
||||
email: 'some@email.com',
|
||||
_account_id: 123,
|
||||
},
|
||||
},
|
||||
{
|
||||
message: 'message text',
|
||||
updated: '2016-09-27 00:18:03.000000000',
|
||||
in_reply_to: 'c5912363_6b820105',
|
||||
line: 42,
|
||||
id: '450a935e_0f1c05db',
|
||||
patch_set: 2,
|
||||
author,
|
||||
},
|
||||
{
|
||||
message: 'message text',
|
||||
updated: '2016-09-27 00:18:03.000000000',
|
||||
in_reply_to: '6505d749_f0bec0aa',
|
||||
line: 62,
|
||||
id: '6505d749_10ed44b2',
|
||||
patch_set: 2,
|
||||
author,
|
||||
},
|
||||
],
|
||||
file2: [
|
||||
{
|
||||
message: 'message text',
|
||||
updated: '2016-09-27 00:18:03.000000000',
|
||||
in_reply_to: 'c5912363_4b7d450a',
|
||||
line: 132,
|
||||
id: '450a935e_4f260d25',
|
||||
patch_set: 2,
|
||||
author,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
suite('basic tests', () => {
|
||||
setup(() => {
|
||||
stub('gr-rest-api-interface', {
|
||||
getConfig() { return Promise.resolve({}); },
|
||||
getLoggedIn() { return Promise.resolve(false); },
|
||||
getDiffComments() { return Promise.resolve(comments); },
|
||||
getDiffRobotComments() { return Promise.resolve({}); },
|
||||
getDiffDrafts() { return Promise.resolve({}); },
|
||||
});
|
||||
sandbox = sinon.sandbox.create();
|
||||
messages = _.times(3, randomMessage);
|
||||
// Element must be wrapped in an element with direct access to the
|
||||
// comment API.
|
||||
commentApiWrapper = fixture('basic');
|
||||
element = commentApiWrapper.$.messagesList;
|
||||
element.messages = messages;
|
||||
|
||||
// Stub methods on the changeComments object after changeComments has
|
||||
// been initialized.
|
||||
return commentApiWrapper.loadComments();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('expand/collapse all', () => {
|
||||
let allMessageEls = getMessages();
|
||||
for (const message of allMessageEls) {
|
||||
message._expanded = false;
|
||||
}
|
||||
MockInteractions.tap(allMessageEls[1]);
|
||||
assert.isTrue(allMessageEls[1]._expanded);
|
||||
|
||||
MockInteractions.tap(element.shadowRoot
|
||||
.querySelector('#collapse-messages'));
|
||||
allMessageEls = getMessages();
|
||||
for (const message of allMessageEls) {
|
||||
assert.isTrue(message._expanded);
|
||||
}
|
||||
|
||||
MockInteractions.tap(element.shadowRoot
|
||||
.querySelector('#collapse-messages'));
|
||||
allMessageEls = getMessages();
|
||||
for (const message of allMessageEls) {
|
||||
assert.isFalse(message._expanded);
|
||||
}
|
||||
});
|
||||
|
||||
test('expand/collapse from external keypress', () => {
|
||||
// Start with one expanded message. -> not all collapsed
|
||||
element.scrollToMessage(messages[1].id);
|
||||
assert.isFalse([...getMessages()].filter(m => m._expanded).length == 0);
|
||||
|
||||
// Press 'z' -> all collapsed
|
||||
element.handleExpandCollapse(false);
|
||||
assert.isTrue([...getMessages()].filter(m => m._expanded).length == 0);
|
||||
|
||||
// Press 'x' -> all expanded
|
||||
element.handleExpandCollapse(true);
|
||||
assert.isTrue([...getMessages()].filter(m => !m._expanded).length == 0);
|
||||
|
||||
// Press 'z' -> all collapsed
|
||||
element.handleExpandCollapse(false);
|
||||
assert.isTrue([...getMessages()].filter(m => m._expanded).length == 0);
|
||||
});
|
||||
|
||||
test('hide messages does not appear when no automated messages', () => {
|
||||
assert.isOk(element.shadowRoot
|
||||
.querySelector('#automatedMessageToggleContainer[hidden]'));
|
||||
});
|
||||
|
||||
test('scroll to message', () => {
|
||||
const allMessageEls = getMessages();
|
||||
for (const message of allMessageEls) {
|
||||
message.set('message.expanded', false);
|
||||
}
|
||||
|
||||
const scrollToStub = sandbox.stub(window, 'scrollTo');
|
||||
const highlightStub = sandbox.stub(element, '_highlightEl');
|
||||
|
||||
element.scrollToMessage('invalid');
|
||||
|
||||
for (const message of allMessageEls) {
|
||||
assert.isFalse(message._expanded,
|
||||
'expected gr-message to not be expanded');
|
||||
}
|
||||
|
||||
const messageID = messages[1].id;
|
||||
element.scrollToMessage(messageID);
|
||||
assert.isTrue(
|
||||
element.shadowRoot
|
||||
.querySelector('[data-message-id="' + messageID + '"]')
|
||||
._expanded);
|
||||
|
||||
assert.isTrue(scrollToStub.calledOnce);
|
||||
assert.isTrue(highlightStub.calledOnce);
|
||||
});
|
||||
|
||||
test('scroll to message offscreen', () => {
|
||||
const scrollToStub = sandbox.stub(window, 'scrollTo');
|
||||
const highlightStub = sandbox.stub(element, '_highlightEl');
|
||||
element.messages = _.times(25, randomMessage);
|
||||
flushAsynchronousOperations();
|
||||
assert.isFalse(scrollToStub.called);
|
||||
assert.isFalse(highlightStub.called);
|
||||
|
||||
const messageID = element.messages[1].id;
|
||||
element.scrollToMessage(messageID);
|
||||
assert.isTrue(scrollToStub.calledOnce);
|
||||
assert.isTrue(highlightStub.calledOnce);
|
||||
assert.isTrue(
|
||||
element.shadowRoot
|
||||
.querySelector('[data-message-id="' + messageID + '"]')
|
||||
._expanded);
|
||||
});
|
||||
|
||||
test('messages', () => {
|
||||
const messages = [].concat(
|
||||
randomMessage(),
|
||||
{
|
||||
_index: 5,
|
||||
_revision_number: 4,
|
||||
message: 'Uploaded patch set 4.',
|
||||
date: '2016-09-28 13:36:33.000000000',
|
||||
author,
|
||||
id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
|
||||
},
|
||||
{
|
||||
_index: 6,
|
||||
_revision_number: 4,
|
||||
message: 'Patch Set 4:\n\n(6 comments)',
|
||||
date: '2016-09-28 13:36:33.000000000',
|
||||
author,
|
||||
id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5',
|
||||
}
|
||||
);
|
||||
element.messages = messages;
|
||||
const isAuthor = function(author, message) {
|
||||
return message.author._account_id === author._account_id;
|
||||
};
|
||||
const isMarvin = isAuthor.bind(null, author);
|
||||
flushAsynchronousOperations();
|
||||
const messageElements = getMessages();
|
||||
assert.equal(messageElements.length, messages.length);
|
||||
assert.deepEqual(messageElements[1].message, messages[1]);
|
||||
assert.deepEqual(messageElements[2].message, messages[2]);
|
||||
assert.deepEqual(messageElements[1].comments.file1,
|
||||
comments.file1.filter(isMarvin));
|
||||
assert.deepEqual(messageElements[1].comments.file2,
|
||||
comments.file2.filter(isMarvin));
|
||||
assert.deepEqual(messageElements[2].comments, {});
|
||||
});
|
||||
|
||||
test('messages without author do not throw', () => {
|
||||
const messages = [{
|
||||
_index: 5,
|
||||
_revision_number: 4,
|
||||
message: 'Uploaded patch set 4.',
|
||||
date: '2016-09-28 13:36:33.000000000',
|
||||
id: '8c19ccc949c6d482b061be6a28e10782abf0e7af',
|
||||
}];
|
||||
element.messages = messages;
|
||||
flushAsynchronousOperations();
|
||||
const messageEls = getMessages();
|
||||
assert.equal(messageEls.length, 1);
|
||||
assert.equal(messageEls[0].message.message, messages[0].message);
|
||||
});
|
||||
});
|
||||
|
||||
suite('gr-messages-list-experimental automate tests', () => {
|
||||
let element;
|
||||
let messages;
|
||||
let sandbox;
|
||||
let commentApiWrapper;
|
||||
|
||||
const getMessages = function() {
|
||||
return dom(element.root).querySelectorAll('gr-message');
|
||||
};
|
||||
const getHiddenMessages = function() {
|
||||
return dom(element.root).querySelectorAll('gr-message[hidden]');
|
||||
};
|
||||
|
||||
const randomMessageReviewer = {
|
||||
reviewer: {},
|
||||
date: '2016-01-13 20:30:33.038000',
|
||||
};
|
||||
|
||||
setup(() => {
|
||||
stub('gr-rest-api-interface', {
|
||||
getConfig() { return Promise.resolve({}); },
|
||||
getLoggedIn() { return Promise.resolve(false); },
|
||||
getDiffComments() { return Promise.resolve({}); },
|
||||
getDiffRobotComments() { return Promise.resolve({}); },
|
||||
getDiffDrafts() { return Promise.resolve({}); },
|
||||
});
|
||||
|
||||
sandbox = sinon.sandbox.create();
|
||||
messages = _.times(2, randomAutomated);
|
||||
messages.push(randomMessageReviewer);
|
||||
|
||||
// Element must be wrapped in an element with direct access to the
|
||||
// comment API.
|
||||
commentApiWrapper = fixture('basic');
|
||||
element = commentApiWrapper.$.messagesList;
|
||||
sandbox.spy(commentApiWrapper.$.commentAPI, 'loadAll');
|
||||
element.messages = messages;
|
||||
|
||||
// Stub methods on the changeComments object after changeComments has
|
||||
// been initialized.
|
||||
return commentApiWrapper.loadComments();
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
test('hide autogenerated button is not hidden', () => {
|
||||
assert.isNotOk(element.shadowRoot
|
||||
.querySelector('#automatedMessageToggle[hidden]'));
|
||||
});
|
||||
|
||||
test('autogenerated messages are not hidden initially', () => {
|
||||
const allHiddenMessageEls = getHiddenMessages();
|
||||
|
||||
// There are no hidden messages.
|
||||
assert.isFalse(!!allHiddenMessageEls.length);
|
||||
});
|
||||
|
||||
test('autogenerated messages hidden after comments only toggle', () => {
|
||||
let allHiddenMessageEls = getHiddenMessages();
|
||||
|
||||
element._hideAutomated = false;
|
||||
MockInteractions.tap(element.$.automatedMessageToggle);
|
||||
flushAsynchronousOperations();
|
||||
const allMessageEls = getMessages();
|
||||
allHiddenMessageEls = getHiddenMessages();
|
||||
|
||||
// Autogenerated messages are now hidden.
|
||||
assert.equal(allHiddenMessageEls.length, allMessageEls.length);
|
||||
});
|
||||
|
||||
test('autogenerated messages not hidden after comments only toggle',
|
||||
() => {
|
||||
let allHiddenMessageEls = getHiddenMessages();
|
||||
|
||||
element._hideAutomated = true;
|
||||
MockInteractions.tap(element.$.automatedMessageToggle);
|
||||
allHiddenMessageEls = getHiddenMessages();
|
||||
|
||||
// Autogenerated messages are now hidden.
|
||||
assert.isFalse(!!allHiddenMessageEls.length);
|
||||
});
|
||||
|
||||
test('_computeLabelExtremes', () => {
|
||||
const computeSpy = sandbox.spy(element, '_computeLabelExtremes');
|
||||
|
||||
element.labels = null;
|
||||
assert.isTrue(computeSpy.calledOnce);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue, {});
|
||||
|
||||
element.labels = {};
|
||||
assert.isTrue(computeSpy.calledTwice);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue, {});
|
||||
|
||||
element.labels = {'my-label': {}};
|
||||
assert.isTrue(computeSpy.calledThrice);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue, {});
|
||||
|
||||
element.labels = {'my-label': {values: {}}};
|
||||
assert.equal(computeSpy.callCount, 4);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue, {});
|
||||
|
||||
element.labels = {'my-label': {values: {'-12': {}}}};
|
||||
assert.equal(computeSpy.callCount, 5);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue,
|
||||
{'my-label': {min: -12, max: -12}});
|
||||
|
||||
element.labels = {
|
||||
'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
|
||||
};
|
||||
assert.equal(computeSpy.callCount, 6);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue,
|
||||
{'my-label': {min: -2, max: 2}});
|
||||
|
||||
element.labels = {
|
||||
'my-label': {values: {'-12': {}}},
|
||||
'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
|
||||
};
|
||||
assert.equal(computeSpy.callCount, 7);
|
||||
assert.deepEqual(computeSpy.lastCall.returnValue, {
|
||||
'my-label': {min: -12, max: -12},
|
||||
'other-label': {min: -1, max: 1},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -80,6 +80,7 @@ const elements = [
|
||||
'change/gr-label-scores/gr-label-scores_test.html',
|
||||
'change/gr-message/gr-message_test.html',
|
||||
'change/gr-messages-list/gr-messages-list_test.html',
|
||||
'change/gr-messages-list/gr-messages-list-experimental_test.html',
|
||||
'change/gr-related-changes-list/gr-related-changes-list_test.html',
|
||||
'change/gr-reply-dialog/gr-reply-dialog-it_test.html',
|
||||
'change/gr-reply-dialog/gr-reply-dialog_test.html',
|
||||
|
||||
Reference in New Issue
Block a user