blob: df04f68f98d34f26b59fe8bc33128bde2dcca950 [file] [log] [blame]
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-messages-list';
import {CombinedMessage, GrMessagesList, TEST_ONLY} from './gr-messages-list';
import {MessageTag} from '../../../constants/constants';
import {
query,
queryAll,
queryAndAssert,
stubRestApi,
} from '../../../test/test-utils';
import {GrMessage} from '../gr-message/gr-message';
import {
AccountId,
ChangeMessageId,
ChangeMessageInfo,
EmailAddress,
LabelNameToInfoMap,
NumericChangeId,
PatchSetNum,
ReviewInputTag,
RevisionPatchSetNum,
Timestamp,
UrlEncodedCommentId,
} from '../../../types/common';
import {assertIsDefined, uuid} from '../../../utils/common-util';
import {html} from 'lit';
import {fixture, assert} from '@open-wc/testing';
import {GrButton} from '../../shared/gr-button/gr-button';
import {PaperToggleButtonElement} from '@polymer/paper-toggle-button';
import {testResolver} from '../../../test/common-test-setup';
import {commentsModelToken} from '../../../models/comments/comments-model';
const author = {
_account_id: 42 as AccountId,
name: 'Marvin the Paranoid Android',
email: 'marvin@sirius.org' as EmailAddress,
};
const createComment = function () {
return {
id: '1a2b3c4d' as UrlEncodedCommentId,
message: 'some random test text',
change_message_id: '8a7b6c5d',
updated: '2016-01-01 01:02:03.000000000' as Timestamp,
line: 1,
patch_set: 1 as RevisionPatchSetNum,
author,
};
};
const randomMessage = function (params?: ChangeMessageInfo) {
params = params || ({} as ChangeMessageInfo);
const author1 = {
_account_id: 1115495 as AccountId,
name: 'Andrew Bonventre',
email: 'andybons@chromium.org' as EmailAddress,
};
return {
id: (params.id || uuid()) as ChangeMessageId,
date: (params.date || '2016-01-12 20:28:33.038000') as Timestamp,
message: params.message || uuid(),
_revision_number: (params._revision_number || 1) as PatchSetNum,
author: params.author || author1,
tag: params.tag,
};
};
function generateRandomMessages(count: number) {
return Array.from({length: count})
.fill(undefined)
.map(() => randomMessage()) as ChangeMessageInfo[];
}
suite('gr-messages-list tests', () => {
let element: GrMessagesList;
let messages: ChangeMessageInfo[];
const getMessages = function () {
return queryAll<GrMessage>(element, 'gr-message');
};
const MESSAGE_ID_0 = '1234ccc949c6d482b061be6a28e10782abf0e7af';
const MESSAGE_ID_1 = '8c19ccc949c6d482b061be6a28e10782abf0e7af';
const MESSAGE_ID_2 = 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5';
const comments = {
file1: [
{
...createComment(),
change_message_id: MESSAGE_ID_0,
in_reply_to: '6505d749_f0bec0aa' as UrlEncodedCommentId,
author: {
email: 'some@email.com' as EmailAddress,
_account_id: 123 as AccountId,
},
},
{
...createComment(),
id: '2b3c4d5e' as UrlEncodedCommentId,
change_message_id: MESSAGE_ID_1,
in_reply_to: 'c5912363_6b820105' as UrlEncodedCommentId,
},
{
...createComment(),
id: '2b3c4d5e' as UrlEncodedCommentId,
change_message_id: MESSAGE_ID_1,
in_reply_to: '6505d749_f0bec0aa' as UrlEncodedCommentId,
},
{
...createComment(),
id: '34ed05d749_10ed44b2' as UrlEncodedCommentId,
change_message_id: MESSAGE_ID_2,
},
],
file2: [
{
...createComment(),
change_message_id: MESSAGE_ID_1,
in_reply_to: 'c5912363_4b7d450a' as UrlEncodedCommentId,
id: '450a935e_4f260d25' as UrlEncodedCommentId,
},
],
};
suite('basic tests', () => {
setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getDiffComments').returns(Promise.resolve(comments));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
messages = generateRandomMessages(3);
element = await fixture<GrMessagesList>(
html`<gr-messages-list></gr-messages-list>`
);
await testResolver(commentsModelToken).reloadComments(
0 as NumericChangeId
);
element.messages = messages;
await element.updateComplete;
});
test('render', () => {
assert.shadowDom.equal(
element,
/* HTML */ `
<div class="header">
<div class="container" id="showAllActivityToggleContainer"></div>
<gr-button
aria-disabled="false"
id="collapse-messages"
link=""
role="button"
tabindex="0"
title="Expand all messages (shortcut: x)"
>
Expand All
</gr-button>
</div>
<gr-message data-message-id="${messages[0].id}"> </gr-message>
<gr-message data-message-id="${messages[1].id}"> </gr-message>
<gr-message data-message-id="${messages[2].id}"> </gr-message>
`
);
});
test('expand/collapse all', async () => {
let allMessageEls = getMessages();
for (const message of allMessageEls) {
assertIsDefined(message.message);
message.message = {...message.message, expanded: false};
await message.updateComplete;
}
allMessageEls[1].click();
await element.updateComplete;
assert.isTrue(allMessageEls[1].message?.expanded);
queryAndAssert<GrButton>(element, '#collapse-messages').click();
await element.updateComplete;
allMessageEls = getMessages();
for (const message of allMessageEls) {
assert.isTrue(message.message?.expanded);
}
queryAndAssert<GrButton>(element, '#collapse-messages').click();
await element.updateComplete;
allMessageEls = getMessages();
for (const message of allMessageEls) {
assert.isFalse(message.message?.expanded);
}
});
test('expand/collapse from external keypress', async () => {
// Start with one expanded message. -> not all collapsed
await element.scrollToMessage(messages[1].id);
assert.isFalse(
[...getMessages()].filter(m => m.message?.expanded).length === 0
);
// Press 'z' -> all collapsed
element.handleExpandCollapse(false);
assert.isTrue(
[...getMessages()].filter(m => m.message?.expanded).length === 0
);
// Press 'x' -> all expanded
element.handleExpandCollapse(true);
assert.isTrue(
[...getMessages()].filter(m => !m.message?.expanded).length === 0
);
// Press 'z' -> all collapsed
element.handleExpandCollapse(false);
assert.isTrue(
[...getMessages()].filter(m => m.message?.expanded).length === 0
);
});
test('showAllActivity does not appear when all msgs are important', () => {
assert.isOk(query(element, '#showAllActivityToggleContainer'));
assert.isNotOk(query(element, '.showAllActivityToggle'));
});
test('scroll to message', async () => {
const allMessageEls = getMessages();
for (const message of allMessageEls) {
assertIsDefined(message.message);
message.message = {...message.message, expanded: false};
}
const highlightStub = sinon.stub(element, 'highlightEl');
await element.scrollToMessage('invalid');
for (const message of allMessageEls) {
assertIsDefined(message.message);
assert.isFalse(
message.message.expanded,
'expected gr-message to not be expanded'
);
}
const messageID = messages[1].id;
const selector = `[data-message-id="${messageID}"]`;
const el = queryAndAssert<GrMessage>(element, selector);
const scrollToStub = sinon.stub(el, 'scrollIntoView');
await element.scrollToMessage(messageID);
assert.isTrue(
queryAndAssert<GrMessage>(element, `[data-message-id="${messageID}"]`)
.message?.expanded
);
assert.isTrue(scrollToStub.calledOnce);
assert.isTrue(highlightStub.calledOnce);
});
test('scroll to message offscreen', async () => {
const highlightStub = sinon.stub(element, 'highlightEl');
element.messages = generateRandomMessages(25);
await element.updateComplete;
assert.isFalse(highlightStub.called);
const messageID = element.messages[1].id;
const selector = `[data-message-id="${messageID}"]`;
const el = queryAndAssert<GrMessage>(element, selector);
const scrollToStub = sinon.stub(el, 'scrollIntoView');
assert.isFalse(scrollToStub.called);
await element.scrollToMessage(messageID);
assert.isTrue(scrollToStub.calledOnce);
assert.isTrue(highlightStub.calledOnce);
assert.isTrue(
queryAndAssert<GrMessage>(element, `[data-message-id="${messageID}"]`)
.message?.expanded
);
});
test('associating messages with comments', async () => {
// Have to type as any otherwise fails with
// Argument of type 'ChangeMessageInfo[]' is not assignable to
// parameter of type 'ConcatArray<never>'.
const messages = ([] as any).concat(
randomMessage(),
{
_index: 5,
_revision_number: 4 as PatchSetNum,
message: 'Uploaded patch set 4.',
date: '2016-09-28 13:36:33.000000000' as Timestamp,
author,
id: '8c19ccc949c6d482b061be6a28e10782abf0e7af' as ChangeMessageId,
} as CombinedMessage,
{
_index: 6,
_revision_number: 4 as PatchSetNum,
message: 'Patch Set 4:\n\n(6 comments)',
date: '2016-09-28 13:36:33.000000000' as Timestamp,
author,
id: 'e7bfdbc842f6b6d8064bc68e0f52b673f40c0ca5' as ChangeMessageId,
} as CombinedMessage
);
element.messages = messages;
await element.updateComplete;
const messageElements = getMessages();
assert.equal(messageElements.length, messages.length);
assert.deepEqual(messageElements[1].message, messages[1]);
assert.deepEqual(messageElements[2].message, messages[2]);
});
test('threads', async () => {
const messages = [
{
_index: 5,
_revision_number: 4 as PatchSetNum,
message: 'Uploaded patch set 4.',
date: '2016-09-28 13:36:33.000000000' as Timestamp,
author,
id: '8c19ccc949c6d482b061be6a28e10782abf0e7af' as ChangeMessageId,
},
];
element.messages = messages;
await element.updateComplete;
const messageElements = getMessages();
// threads
assertIsDefined(messageElements[0].message, 'message');
assert.equal(messageElements[0].message.commentThreads.length, 3);
// first thread contains 1 comment
assert.equal(
messageElements[0].message.commentThreads[0].comments.length,
1
);
});
test('updateTag human message', () => {
const m = randomMessage();
assert.equal(TEST_ONLY.computeTag(m), undefined);
});
test('updateTag nothing to change', () => {
const m = randomMessage();
const tag = 'something-normal' as ReviewInputTag;
m.tag = tag;
assert.equal(TEST_ONLY.computeTag(m), tag);
});
test('updateTag TAG_NEW_WIP_PATCHSET', () => {
const m = randomMessage();
m.tag = MessageTag.TAG_NEW_WIP_PATCHSET as ReviewInputTag;
assert.equal(TEST_ONLY.computeTag(m), MessageTag.TAG_NEW_PATCHSET);
});
test('updateTag for outdated votes', () => {
const m = randomMessage();
m.tag = MessageTag.TAG_NEW_PATCHSET as ReviewInputTag;
m.message = '\nUploaded patch set 35.\n\nOutdated Votes:\n';
assert.equal(
TEST_ONLY.computeTag(m),
MessageTag.TAG_NEW_PATCHSET_OUTDATED_VOTES
);
m.tag = MessageTag.TAG_NEW_WIP_PATCHSET as ReviewInputTag;
assert.equal(TEST_ONLY.computeTag(m), MessageTag.TAG_NEW_PATCHSET);
});
test('updateTag remove postfix', () => {
const m = randomMessage();
m.tag = 'something~withpostfix' as ReviewInputTag;
assert.equal(TEST_ONLY.computeTag(m), 'something');
});
test('updateTag with robot comments', () => {
const m = randomMessage();
(m as any).commentThreads = [
{
comments: [
{
robot_id: 'id314',
change_message_id: m.id,
},
],
},
];
assert.notEqual(TEST_ONLY.computeTag(m), undefined);
});
test('setRevisionNumber nothing to change', () => {
const m1 = randomMessage();
const m2 = randomMessage();
assert.equal(TEST_ONLY.computeRevision(m1, [m1, m2]), 1 as PatchSetNum);
assert.equal(TEST_ONLY.computeRevision(m2, [m1, m2]), 1 as PatchSetNum);
});
test('setRevisionNumber reviewer updates', () => {
const m1 = randomMessage({
...randomMessage(),
tag: MessageTag.TAG_REVIEWER_UPDATE as ReviewInputTag,
date: '2020-01-01 10:00:00.000000000' as Timestamp,
});
m1._revision_number = 0 as PatchSetNum;
const m2 = randomMessage({
...randomMessage(),
date: '2020-01-02 10:00:00.000000000' as Timestamp,
});
m2._revision_number = 1 as PatchSetNum;
const m3 = randomMessage({
...randomMessage(),
tag: MessageTag.TAG_REVIEWER_UPDATE as ReviewInputTag,
date: '2020-01-03 10:00:00.000000000' as Timestamp,
});
m3._revision_number = 0 as PatchSetNum;
const m4 = randomMessage({
...randomMessage(),
date: '2020-01-04 10:00:00.000000000' as Timestamp,
});
m4._revision_number = 2 as PatchSetNum;
const m5 = randomMessage({
...randomMessage(),
tag: MessageTag.TAG_REVIEWER_UPDATE as ReviewInputTag,
date: '2020-01-05 10:00:00.000000000' as Timestamp,
});
m5._revision_number = 0 as PatchSetNum;
const allMessages = [m1, m2, m3, m4, m5];
assert.equal(TEST_ONLY.computeRevision(m1, allMessages), undefined);
assert.equal(
TEST_ONLY.computeRevision(m2, allMessages),
1 as PatchSetNum
);
assert.equal(
TEST_ONLY.computeRevision(m3, allMessages),
1 as PatchSetNum
);
assert.equal(
TEST_ONLY.computeRevision(m4, allMessages),
2 as PatchSetNum
);
assert.equal(
TEST_ONLY.computeRevision(m5, allMessages),
2 as PatchSetNum
);
});
test('isImportant human message', () => {
const m = randomMessage();
assert.isTrue(TEST_ONLY.computeIsImportant(m, []));
assert.isTrue(TEST_ONLY.computeIsImportant(m, [m]));
});
test('isImportant even with a tag', () => {
const m1 = randomMessage();
const m2 = randomMessage({
...randomMessage(),
tag: 'autogenerated:gerrit1' as ReviewInputTag,
});
const m3 = randomMessage({
...randomMessage(),
tag: 'autogenerated:gerrit2' as ReviewInputTag,
});
assert.isTrue(TEST_ONLY.computeIsImportant(m2, []));
assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2, m3]));
assert.isTrue(TEST_ONLY.computeIsImportant(m2, [m1, m2, m3]));
assert.isTrue(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
});
test('isImportant filters same tag and older revision', () => {
const m1 = randomMessage({
...randomMessage(),
tag: 'auto' as ReviewInputTag,
_revision_number: 2 as PatchSetNum,
});
const m2 = randomMessage({
...randomMessage(),
tag: 'auto' as ReviewInputTag,
_revision_number: 1 as PatchSetNum,
});
const m3 = randomMessage({
...randomMessage(),
tag: 'auto' as ReviewInputTag,
});
assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1]));
assert.isTrue(TEST_ONLY.computeIsImportant(m2, [m2]));
assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2]));
assert.isFalse(TEST_ONLY.computeIsImportant(m2, [m1, m2]));
assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m3]));
assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m3]));
assert.isTrue(TEST_ONLY.computeIsImportant(m1, [m1, m2, m3]));
assert.isFalse(TEST_ONLY.computeIsImportant(m2, [m1, m2, m3]));
assert.isFalse(TEST_ONLY.computeIsImportant(m3, [m1, m2, m3]));
});
test('isImportant is evaluated after tag update', async () => {
const m1 = randomMessage({
...randomMessage(),
tag: MessageTag.TAG_NEW_PATCHSET as ReviewInputTag,
_revision_number: 1 as PatchSetNum,
});
const m2 = randomMessage({
...randomMessage(),
tag: MessageTag.TAG_NEW_WIP_PATCHSET as ReviewInputTag,
_revision_number: 2 as PatchSetNum,
});
element.messages = [m1, m2];
await element.updateComplete;
assert.isFalse((m1 as CombinedMessage).isImportant);
assert.isTrue((m2 as CombinedMessage).isImportant);
});
test('messages without author do not throw', async () => {
const messages = [
{
_index: 5,
_revision_number: 4 as PatchSetNum,
message: 'Uploaded patch set 4.',
date: '2016-09-28 13:36:33.000000000' as Timestamp,
id: '8c19ccc949c6d482b061be6a28e10782abf0e7af' as ChangeMessageId,
},
];
element.messages = messages;
await element.updateComplete;
const messageEls = getMessages();
assert.equal(messageEls.length, 1);
assertIsDefined(messageEls[0].message, 'message');
assert.equal(messageEls[0].message.message, messages[0].message);
});
});
suite('gr-messages-list automate tests', () => {
let element: GrMessagesList;
let messages: ChangeMessageInfo[];
setup(async () => {
stubRestApi('getLoggedIn').returns(Promise.resolve(false));
stubRestApi('getDiffComments').returns(Promise.resolve({}));
stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
messages = [
randomMessage(),
randomMessage({
...randomMessage(),
tag: MessageTag.TAG_NEW_PATCHSET as ReviewInputTag,
message:
'\nUploaded patch set 35.\n\nInitial upload\n\nOutdated Votes:\n',
}),
randomMessage({
...randomMessage(),
tag: 'auto' as ReviewInputTag,
_revision_number: 2 as PatchSetNum,
}),
randomMessage({
...randomMessage(),
tag: 'auto' as ReviewInputTag,
_revision_number: 3 as PatchSetNum,
}),
];
element = await fixture<GrMessagesList>(
html`<gr-messages-list></gr-messages-list>`
);
element.messages = messages;
await element.updateComplete;
});
test('hide autogenerated button is not hidden', () => {
const toggle = queryAndAssert(element, '.showAllActivityToggle');
assert.isOk(toggle);
});
test('one unimportant message is hidden initially', () => {
const displayedMsgs = queryAll<GrMessage>(element, 'gr-message');
assert.equal(displayedMsgs.length, 3);
});
test('unimportant messages hidden after toggle', async () => {
element.showAllActivity = true;
await element.updateComplete;
const toggle = queryAndAssert<PaperToggleButtonElement>(
element,
'.showAllActivityToggle'
);
assert.isOk(toggle);
toggle.click();
await element.updateComplete;
const displayedMsgs = queryAll<GrMessage>(element, 'gr-message');
assert.equal(displayedMsgs.length, 3);
});
test('unimportant messages shown after toggle', async () => {
element.showAllActivity = false;
await element.updateComplete;
const toggle = queryAndAssert<PaperToggleButtonElement>(
element,
'.showAllActivityToggle'
);
assert.isOk(toggle);
toggle.click();
await element.updateComplete;
const displayedMsgs = queryAll<GrMessage>(element, 'gr-message');
assert.equal(displayedMsgs.length, 4);
});
test('_computeLabelExtremes', () => {
// Have to type as any to be able to use null.
element.labels = null as any;
assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {};
assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {'my-label': {}};
assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {'my-label': {values: {}}};
assert.deepEqual(element.computeLabelExtremes(), {});
element.labels = {
'my-label': {values: {'-12': {}}},
} as LabelNameToInfoMap;
assert.deepEqual(element.computeLabelExtremes(), {
'my-label': {min: -12, max: -12},
});
element.labels = {
'my-label': {values: {'-2': {}, '-1': {}, '0': {}, '+1': {}, '+2': {}}},
} as LabelNameToInfoMap;
assert.deepEqual(element.computeLabelExtremes(), {
'my-label': {min: -2, max: 2},
});
element.labels = {
'my-label': {values: {'-12': {}}},
'other-label': {values: {'-1': {}, ' 0': {}, '+1': {}}},
} as LabelNameToInfoMap;
assert.deepEqual(element.computeLabelExtremes(), {
'my-label': {min: -12, max: -12},
'other-label': {min: -1, max: 1},
});
});
});
});