blob: 53e62ff6001aa6c330309ebcdbc378f282f73566 [file] [log] [blame]
/**
* @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.
*/
import '../../../test/common-test-setup-karma';
import './gr-comment';
import {html} from '@polymer/polymer/lib/utils/html-tag';
import {GrComment, __testOnly_UNSAVED_MESSAGE} from './gr-comment';
import {SpecialFilePath, CommentSide} from '../../../constants/constants';
import {
queryAndAssert,
stubRestApi,
stubStorage,
spyStorage,
query,
isVisible,
stubReporting,
mockPromise,
} from '../../../test/test-utils';
import {
AccountId,
EmailAddress,
FixId,
NumericChangeId,
ParsedJSON,
PatchSetNum,
RobotId,
RobotRunId,
Timestamp,
UrlEncodedCommentId,
} from '../../../types/common';
import {
pressAndReleaseKeyOn,
tap,
} from '@polymer/iron-test-helpers/mock-interactions';
import {
createComment,
createDraft,
createFixSuggestionInfo,
} from '../../../test/test-data-generators';
import {Timer} from '../../../services/gr-reporting/gr-reporting';
import {SinonFakeTimers, SinonStubbedMember} from 'sinon';
import {CreateFixCommentEvent} from '../../../types/events';
import {DraftInfo, UIRobot} from '../../../utils/comment-util';
import {MockTimer} from '../../../services/gr-reporting/gr-reporting_mock';
import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog';
const basicFixture = fixtureFromElement('gr-comment');
const draftFixture = fixtureFromTemplate(html`
<gr-comment draft="true"></gr-comment>
`);
suite('gr-comment tests', () => {
suite('basic tests', () => {
let element: GrComment;
let openOverlaySpy: sinon.SinonSpy;
setup(() => {
stubRestApi('getAccount').returns(
Promise.resolve({
email: 'dhruvsri@google.com' as EmailAddress,
name: 'Dhruv Srivastava',
_account_id: 1083225 as AccountId,
avatars: [{url: 'abc', height: 32, width: 32}],
registered_on: '123' as Timestamp,
})
);
element = basicFixture.instantiate();
element.comment = {
...createComment(),
author: {
name: 'Mr. Peanutbutter',
email: 'tenn1sballchaser@aol.com' as EmailAddress,
},
id: 'baf0414d_60047215' as UrlEncodedCommentId,
line: 5,
message: 'is this a crossover episode!?',
updated: '2015-12-08 19:48:33.843000000' as Timestamp,
};
openOverlaySpy = sinon.spy(element, '_openOverlay');
});
teardown(() => {
openOverlaySpy.getCalls().forEach(call => {
call.args[0].remove();
});
});
test('collapsible comments', () => {
// When a comment (not draft) is loaded, it should be collapsed
assert.isTrue(element.collapsed);
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.actions')),
'actions are not visible'
);
assert.isNotOk(element.textarea, 'textarea is not visible');
// The header middle content is only visible when comments are collapsed.
// It shows the message in a condensed way, and limits to a single line.
assert.isTrue(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is visible'
);
// When the header row is clicked, the comment should expand
tap(element.$.header);
assert.isFalse(element.collapsed);
assert.isTrue(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.actions')),
'actions are visible'
);
assert.isNotOk(element.textarea, 'textarea is not visible');
assert.isFalse(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is not visible'
);
});
test('clicking on date link fires event', () => {
element.side = 'PARENT';
const stub = sinon.stub();
element.addEventListener('comment-anchor-tap', stub);
flush();
const dateEl = queryAndAssert(element, '.date');
assert.ok(dateEl);
tap(dateEl);
assert.isTrue(stub.called);
assert.deepEqual(stub.lastCall.args[0].detail, {
side: element.side,
number: element.comment!.line,
});
});
test('message is not retrieved from storage when missing path', async () => {
const storageStub = stubStorage('getDraftComment');
const loadSpy = sinon.spy(element, '_loadLocalDraft');
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element.comment = {
author: {
name: 'Mr. Peanutbutter',
email: 'tenn1sballchaser@aol.com' as EmailAddress,
},
line: 5,
};
await flush();
assert.isTrue(loadSpy.called);
assert.isFalse(storageStub.called);
});
test('message is not retrieved from storage when message present', async () => {
const storageStub = stubStorage('getDraftComment');
const loadSpy = sinon.spy(element, '_loadLocalDraft');
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element.comment = {
author: {
name: 'Mr. Peanutbutter',
email: 'tenn1sballchaser@aol.com' as EmailAddress,
},
message: 'This is a message',
line: 5,
path: 'test',
__editing: true,
__draft: true,
};
await flush();
assert.isTrue(loadSpy.called);
assert.isFalse(storageStub.called);
});
test('message is retrieved from storage for drafts in edit', async () => {
const storageStub = stubStorage('getDraftComment');
const loadSpy = sinon.spy(element, '_loadLocalDraft');
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element.comment = {
author: {
name: 'Mr. Peanutbutter',
email: 'tenn1sballchaser@aol.com' as EmailAddress,
},
line: 5,
path: 'test',
__editing: true,
__draft: true,
};
await flush();
assert.isTrue(loadSpy.called);
assert.isTrue(storageStub.called);
});
test('comment message sets messageText only when empty', () => {
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element._messageText = '';
element.comment = {
author: {
name: 'Mr. Peanutbutter',
email: 'tenn1sballchaser@aol.com' as EmailAddress,
},
line: 5,
path: 'test',
__editing: true,
__draft: true,
message: 'hello world',
};
// messageText was empty so overwrite the message now
assert.equal(element._messageText, 'hello world');
element.comment!.message = 'new message';
// messageText was already set so do not overwrite it
assert.equal(element._messageText, 'hello world');
});
test('comment message sets messageText when not edited', () => {
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element._messageText = 'Some text';
element.comment = {
author: {
name: 'Mr. Peanutbutter',
email: 'tenn1sballchaser@aol.com' as EmailAddress,
},
line: 5,
path: 'test',
__editing: false,
__draft: true,
message: 'hello world',
};
// messageText was empty so overwrite the message now
assert.equal(element._messageText, 'hello world');
element.comment!.message = 'new message';
// messageText was already set so do not overwrite it
assert.equal(element._messageText, 'hello world');
});
test('_getPatchNum', () => {
element.side = 'PARENT';
element.patchNum = 1 as PatchSetNum;
assert.equal(element._getPatchNum(), 'PARENT' as PatchSetNum);
element.side = 'REVISION';
assert.equal(element._getPatchNum(), 1 as PatchSetNum);
});
test('comment expand and collapse', () => {
element.collapsed = true;
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.actions')),
'actions are not visible'
);
assert.isNotOk(element.textarea, 'textarea is not visible');
assert.isTrue(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is visible'
);
element.collapsed = false;
assert.isFalse(element.collapsed);
assert.isTrue(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.actions')),
'actions are visible'
);
assert.isNotOk(element.textarea, 'textarea is not visible');
assert.isFalse(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is is not visible'
);
});
suite('while editing', () => {
let handleCancelStub: sinon.SinonStub;
let handleSaveStub: sinon.SinonStub;
setup(() => {
element.editing = true;
element._messageText = 'test';
handleCancelStub = sinon.stub(element, '_handleCancel');
handleSaveStub = sinon.stub(element, '_handleSave');
flush();
});
suite('when text is empty', () => {
setup(() => {
element._messageText = '';
element.comment = {};
});
test('esc closes comment when text is empty', () => {
pressAndReleaseKeyOn(element.textarea!, 27); // esc
assert.isTrue(handleCancelStub.called);
});
test('ctrl+enter does not save', () => {
pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl'); // ctrl + enter
assert.isFalse(handleSaveStub.called);
});
test('meta+enter does not save', () => {
pressAndReleaseKeyOn(element.textarea!, 13, 'meta'); // meta + enter
assert.isFalse(handleSaveStub.called);
});
test('ctrl+s does not save', () => {
pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl'); // ctrl + s
assert.isFalse(handleSaveStub.called);
});
});
test('esc does not close comment that has content', () => {
pressAndReleaseKeyOn(element.textarea!, 27); // esc
assert.isFalse(handleCancelStub.called);
});
test('ctrl+enter saves', () => {
pressAndReleaseKeyOn(element.textarea!, 13, 'ctrl'); // ctrl + enter
assert.isTrue(handleSaveStub.called);
});
test('meta+enter saves', () => {
pressAndReleaseKeyOn(element.textarea!, 13, 'meta'); // meta + enter
assert.isTrue(handleSaveStub.called);
});
test('ctrl+s saves', () => {
pressAndReleaseKeyOn(element.textarea!, 83, 'ctrl'); // ctrl + s
assert.isTrue(handleSaveStub.called);
});
});
test('delete comment button for non-admins is hidden', () => {
element._isAdmin = false;
assert.isFalse(
queryAndAssert(element, '.action.delete').classList.contains(
'showDeleteButtons'
)
);
});
test('delete comment button for admins with draft is hidden', () => {
element._isAdmin = false;
element.draft = true;
assert.isFalse(
queryAndAssert(element, '.action.delete').classList.contains(
'showDeleteButtons'
)
);
});
test('delete comment', async () => {
const stub = stubRestApi('deleteComment').returns(
Promise.resolve({
id: '1' as UrlEncodedCommentId,
updated: '1' as Timestamp,
...createComment(),
})
);
const openSpy = sinon.spy(element.confirmDeleteOverlay!, 'open');
element.changeNum = 42 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element._isAdmin = true;
assert.isTrue(
queryAndAssert(element, '.action.delete').classList.contains(
'showDeleteButtons'
)
);
tap(queryAndAssert(element, '.action.delete'));
await flush();
await openSpy.lastCall.returnValue;
const dialog = element.confirmDeleteOverlay?.querySelector(
'#confirmDeleteComment'
) as GrConfirmDeleteCommentDialog;
dialog.message = 'removal reason';
element._handleConfirmDeleteComment();
assert.isTrue(
stub.calledWith(
42 as NumericChangeId,
1 as PatchSetNum,
'baf0414d_60047215' as UrlEncodedCommentId,
'removal reason'
)
);
});
suite('draft update reporting', () => {
let endStub: SinonStubbedMember<() => Timer>;
let getTimerStub: sinon.SinonStub;
const mockEvent = {...new Event('click'), preventDefault() {}};
setup(() => {
sinon.stub(element, 'save').returns(Promise.resolve({}));
endStub = sinon.stub();
const mockTimer = new MockTimer();
mockTimer.end = endStub;
getTimerStub = stubReporting('getTimer').returns(mockTimer);
});
test('create', async () => {
element.patchNum = 1 as PatchSetNum;
element.comment = {};
sinon.stub(element, '_discardDraft').returns(Promise.resolve({}));
await element._handleSave(mockEvent);
await flush();
const grAccountLabel = queryAndAssert(element, 'gr-account-label');
const spanName = queryAndAssert<HTMLSpanElement>(
grAccountLabel,
'span.name'
);
assert.equal(spanName.innerText.trim(), 'Dhruv Srivastava');
assert.isTrue(endStub.calledOnce);
assert.isTrue(getTimerStub.calledOnce);
assert.equal(getTimerStub.lastCall.args[0], 'CreateDraftComment');
});
test('update', () => {
element.comment = {
...createComment(),
id: 'abc_123' as UrlEncodedCommentId as UrlEncodedCommentId,
};
sinon.stub(element, '_discardDraft').returns(Promise.resolve({}));
return element._handleSave(mockEvent)!.then(() => {
assert.isTrue(endStub.calledOnce);
assert.isTrue(getTimerStub.calledOnce);
assert.equal(getTimerStub.lastCall.args[0], 'UpdateDraftComment');
});
});
test('discard', () => {
element.comment = {
...createComment(),
id: 'abc_123' as UrlEncodedCommentId as UrlEncodedCommentId,
};
element.comment = createDraft();
sinon.stub(element, '_fireDiscard');
sinon.stub(element, '_eraseDraftCommentFromStorage');
sinon
.stub(element, '_deleteDraft')
.returns(Promise.resolve(new Response()));
return element._discardDraft().then(() => {
assert.isTrue(endStub.calledOnce);
assert.isTrue(getTimerStub.calledOnce);
assert.equal(getTimerStub.lastCall.args[0], 'DiscardDraftComment');
});
});
});
test('edit reports interaction', () => {
const reportStub = stubReporting('recordDraftInteraction');
sinon.stub(element, '_fireEdit');
element.draft = true;
flush();
tap(queryAndAssert(element, '.edit'));
assert.isTrue(reportStub.calledOnce);
});
test('discard reports interaction', () => {
const reportStub = stubReporting('recordDraftInteraction');
sinon.stub(element, '_eraseDraftCommentFromStorage');
sinon.stub(element, '_fireDiscard');
sinon
.stub(element, '_deleteDraft')
.returns(Promise.resolve(new Response()));
element.draft = true;
element.comment = createDraft();
flush();
tap(queryAndAssert(element, '.discard'));
assert.isTrue(reportStub.calledOnce);
});
test('failed save draft request', async () => {
element.draft = true;
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
const updateRequestStub = sinon.stub(element, '_updateRequestToast');
const diffDraftStub = stubRestApi('saveDiffDraft').returns(
Promise.resolve({...new Response(), ok: false})
);
element._saveDraft({
...createComment(),
id: 'abc_123' as UrlEncodedCommentId,
});
await flush();
let args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0, true]);
assert.equal(
element._getSavingMessage(...args),
__testOnly_UNSAVED_MESSAGE
);
assert.equal(
(queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
'DRAFT(Failed to save)'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.save')),
'save is visible'
);
diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
element._saveDraft({
...createComment(),
id: 'abc_123' as UrlEncodedCommentId,
});
await flush();
args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0]);
assert.equal(element._getSavingMessage(...args), 'All changes saved');
assert.equal(
(queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
'DRAFT'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.save')),
'save is not visible'
);
assert.isFalse(element._unableToSave);
});
test('failed save draft request with promise failure', async () => {
element.draft = true;
element.changeNum = 1 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
const updateRequestStub = sinon.stub(element, '_updateRequestToast');
const diffDraftStub = stubRestApi('saveDiffDraft').returns(
Promise.reject(new Error())
);
element._saveDraft({
...createComment(),
id: 'abc_123' as UrlEncodedCommentId,
});
await flush();
let args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0, true]);
assert.equal(
element._getSavingMessage(...args),
__testOnly_UNSAVED_MESSAGE
);
assert.equal(
(queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
'DRAFT(Failed to save)'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.save')),
'save is visible'
);
diffDraftStub.returns(Promise.resolve({...new Response(), ok: true}));
element._saveDraft({
...createComment(),
id: 'abc_123' as UrlEncodedCommentId,
});
await flush();
args = updateRequestStub.lastCall.args;
assert.deepEqual(args, [0]);
assert.equal(element._getSavingMessage(...args), 'All changes saved');
assert.equal(
(queryAndAssert(element, '.draftLabel') as HTMLSpanElement).innerText,
'DRAFT'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.save')),
'save is not visible'
);
assert.isFalse(element._unableToSave);
});
});
suite('gr-comment draft tests', () => {
let element: GrComment;
setup(() => {
stubRestApi('getAccount').returns(Promise.resolve(undefined));
stubRestApi('saveDiffDraft').returns(
Promise.resolve({
...new Response(),
ok: true,
text() {
return Promise.resolve(
")]}'\n{" +
'"id": "baf0414d_40572e03",' +
'"path": "/path/to/file",' +
'"line": 5,' +
'"updated": "2015-12-08 21:52:36.177000000",' +
'"message": "saved!",' +
'"side": "REVISION",' +
'"unresolved": false,' +
'"patch_set": 1' +
'}'
);
},
})
);
stubRestApi('removeChangeReviewer').returns(
Promise.resolve({...new Response(), ok: true})
);
element = draftFixture.instantiate() as GrComment;
stubStorage('getDraftComment').returns(null);
element.changeNum = 42 as NumericChangeId;
element.patchNum = 1 as PatchSetNum;
element.editing = false;
element.comment = {
...createComment(),
__draft: true,
__draftID: 'temp_draft_id',
path: '/path/to/file',
line: 5,
id: undefined,
};
});
test('button visibility states', async () => {
element.showActions = false;
assert.isTrue(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isTrue(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
element.showActions = true;
assert.isFalse(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isTrue(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
element.draft = true;
await flush();
assert.isTrue(
isVisible(queryAndAssert(element, '.edit')),
'edit is visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.discard')),
'discard is visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.save')),
'save is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.cancel')),
'cancel is not visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.resolve')),
'resolve is visible'
);
assert.isFalse(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isTrue(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
element.editing = true;
await flush();
assert.isFalse(
isVisible(queryAndAssert(element, '.edit')),
'edit is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.discard')),
'discard not visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.save')),
'save is visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.cancel')),
'cancel is visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.resolve')),
'resolve is visible'
);
assert.isFalse(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isTrue(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
element.draft = false;
element.editing = false;
await flush();
assert.isFalse(
isVisible(queryAndAssert(element, '.edit')),
'edit is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.discard')),
'discard is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.save')),
'save is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.cancel')),
'cancel is not visible'
);
assert.isFalse(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isTrue(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
element.comment!.id = 'foo' as UrlEncodedCommentId;
element.draft = true;
element.editing = true;
await flush();
assert.isTrue(
isVisible(queryAndAssert(element, '.cancel')),
'cancel is visible'
);
assert.isFalse(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isTrue(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
// Delete button is not hidden by default
assert.isFalse(
(queryAndAssert(element, '#deleteBtn') as HTMLElement).hidden
);
element.isRobotComment = true;
element.draft = true;
assert.isTrue(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isFalse(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
// It is not expected to see Robot comment drafts, but if they appear,
// they will behave the same as non-drafts.
element.draft = false;
assert.isTrue(
queryAndAssert(element, '.humanActions').hasAttribute('hidden')
);
assert.isFalse(
queryAndAssert(element, '.robotActions').hasAttribute('hidden')
);
// A robot comment with run ID should display plain text.
element.set(['comment', 'robot_run_id'], 'text');
element.editing = false;
element.collapsed = false;
await flush();
assert.isTrue(
queryAndAssert(element, '.robotRun.link').textContent === 'Run Details'
);
// A robot comment with run ID and url should display a link.
element.set(['comment', 'url'], '/path/to/run');
await flush();
assert.notEqual(
getComputedStyle(queryAndAssert(element, '.robotRun.link')).display,
'none'
);
// Delete button is hidden for robot comments
assert.isTrue(
(queryAndAssert(element, '#deleteBtn') as HTMLElement).hidden
);
});
test('collapsible drafts', async () => {
const fireEditStub = sinon.stub(element, '_fireEdit');
assert.isTrue(element.collapsed);
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.actions')),
'actions are not visible'
);
assert.isNotOk(element.textarea, 'textarea is not visible');
assert.isTrue(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is visible'
);
tap(element.$.header);
assert.isFalse(element.collapsed);
assert.isTrue(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.actions')),
'actions are visible'
);
assert.isNotOk(element.textarea, 'textarea is not visible');
assert.isFalse(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is is not visible'
);
// When the edit button is pressed, should still see the actions
// and also textarea
element.draft = true;
await flush();
tap(queryAndAssert(element, '.edit'));
await flush();
assert.isTrue(fireEditStub.called);
assert.isFalse(element.collapsed);
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is not visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.actions')),
'actions are visible'
);
assert.isTrue(isVisible(element.textarea!), 'textarea is visible');
assert.isFalse(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is not visible'
);
// When toggle again, everything should be hidden except for textarea
// and header middle content should be visible
tap(element.$.header);
assert.isTrue(element.collapsed);
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, '.actions')),
'actions are not visible'
);
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-textarea')),
'textarea is not visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is visible'
);
// When toggle again, textarea should remain open in the state it was
// before
tap(element.$.header);
assert.isFalse(
isVisible(queryAndAssert(element, 'gr-formatted-text')),
'gr-formatted-text is not visible'
);
assert.isTrue(
isVisible(queryAndAssert(element, '.actions')),
'actions are visible'
);
assert.isTrue(isVisible(element.textarea!), 'textarea is visible');
assert.isFalse(
isVisible(queryAndAssert(element, '.collapsedContent')),
'header middle content is not visible'
);
});
test('robot comment layout', async () => {
const comment = {
robot_id: 'happy_robot_id' as RobotId,
url: '/robot/comment',
author: {
name: 'Happy Robot',
display_name: 'Display name Robot',
},
...element.comment,
};
element.comment = comment;
element.collapsed = false;
await flush;
let runIdMessage;
runIdMessage = queryAndAssert(element, '.runIdMessage') as HTMLElement;
assert.isFalse((runIdMessage as HTMLElement).hidden);
const runDetailsLink = queryAndAssert(
element,
'.robotRunLink'
) as HTMLAnchorElement;
assert.isTrue(
runDetailsLink.href.indexOf((element.comment as UIRobot).url!) !== -1
);
const robotServiceName = queryAndAssert(element, '.robotName');
assert.equal(robotServiceName.textContent?.trim(), 'happy_robot_id');
const authorName = queryAndAssert(element, '.robotId');
assert.isTrue((authorName as HTMLDivElement).innerText === 'Happy Robot');
element.collapsed = true;
await flush();
runIdMessage = queryAndAssert(element, '.runIdMessage');
assert.isTrue((runIdMessage as HTMLDivElement).hidden);
});
test('author name fallback to email', async () => {
const comment = {
url: '/robot/comment',
author: {
email: 'test@test.com' as EmailAddress,
},
...element.comment,
};
element.comment = comment;
element.collapsed = false;
await flush();
const authorName = queryAndAssert(
queryAndAssert(element, 'gr-account-label'),
'span.name'
) as HTMLSpanElement;
assert.equal(authorName.innerText.trim(), 'test@test.com');
});
test('patchset level comment', async () => {
const fireEditStub = sinon.stub(element, '_fireEdit');
const comment = {
...element.comment,
path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS,
line: undefined,
range: undefined,
};
element.comment = comment;
await flush();
tap(queryAndAssert(element, '.edit'));
assert.isTrue(fireEditStub.called);
assert.isTrue(element.editing);
element._messageText = 'hello world';
const eraseMessageDraftSpy = spyStorage('eraseDraftComment');
const mockEvent = {...new Event('click'), preventDefault: sinon.stub()};
element._handleSave(mockEvent);
await flush();
assert.isTrue(eraseMessageDraftSpy.called);
});
test('draft creation/cancellation', async () => {
const fireEditStub = sinon.stub(element, '_fireEdit');
assert.isFalse(element.editing);
element.draft = true;
await flush();
tap(queryAndAssert(element, '.edit'));
assert.isTrue(fireEditStub.called);
assert.isTrue(element.editing);
element.comment!.message = '';
element._messageText = '';
const eraseMessageDraftSpy = sinon.spy(
element,
'_eraseDraftCommentFromStorage'
);
// Save should be disabled on an empty message.
let disabled = queryAndAssert(element, '.save').hasAttribute('disabled');
assert.isTrue(disabled, 'save button should be disabled.');
element._messageText = ' ';
disabled = queryAndAssert(element, '.save').hasAttribute('disabled');
assert.isTrue(disabled, 'save button should be disabled.');
const updateStub = sinon.stub();
element.addEventListener('comment-update', updateStub);
let numDiscardEvents = 0;
const promise = mockPromise();
element.addEventListener('comment-discard', () => {
numDiscardEvents++;
assert.isFalse(eraseMessageDraftSpy.called);
if (numDiscardEvents === 2) {
assert.isFalse(updateStub.called);
promise.resolve();
}
});
tap(queryAndAssert(element, '.cancel'));
await flush();
element._messageText = '';
element.editing = true;
await flush();
pressAndReleaseKeyOn(element.textarea!, 27); // esc
await promise;
});
test('draft discard removes message from storage', async () => {
element._messageText = '';
const eraseMessageDraftSpy = sinon.spy(
element,
'_eraseDraftCommentFromStorage'
);
const promise = mockPromise();
element.addEventListener('comment-discard', () => {
assert.isTrue(eraseMessageDraftSpy.called);
promise.resolve();
});
element._handleDiscard({
...new Event('click'),
preventDefault: sinon.stub(),
});
await promise;
});
test('storage is cleared only after save success', () => {
element._messageText = 'test';
const eraseStub = sinon.stub(element, '_eraseDraftCommentFromStorage');
stubRestApi('getResponseObject').returns(
Promise.resolve({...(createDraft() as ParsedJSON)})
);
const saveDraftStub = sinon
.stub(element, '_saveDraft')
.returns(Promise.resolve({...new Response(), ok: false}));
const savePromise = element.save();
assert.isFalse(eraseStub.called);
return savePromise.then(() => {
assert.isFalse(eraseStub.called);
saveDraftStub.restore();
sinon
.stub(element, '_saveDraft')
.returns(Promise.resolve({...new Response(), ok: true}));
return element.save().then(() => {
assert.isTrue(eraseStub.called);
});
});
});
test('_computeSaveDisabled', () => {
const comment = {unresolved: true};
const msgComment = {message: 'test', unresolved: true};
assert.equal(element._computeSaveDisabled('', comment, false), true);
assert.equal(element._computeSaveDisabled('test', comment, false), false);
assert.equal(element._computeSaveDisabled('', msgComment, false), true);
assert.equal(
element._computeSaveDisabled('test', msgComment, false),
false
);
assert.equal(
element._computeSaveDisabled('test2', msgComment, false),
false
);
assert.equal(element._computeSaveDisabled('test', comment, true), false);
assert.equal(element._computeSaveDisabled('', comment, true), true);
assert.equal(element._computeSaveDisabled('', comment, false), true);
});
test('ctrl+s saves comment', async () => {
const promise = mockPromise();
const stub = sinon.stub(element, 'save').callsFake(() => {
assert.isTrue(stub.called);
stub.restore();
promise.resolve();
return Promise.resolve();
});
element._messageText = 'is that the horse from horsing around??';
element.editing = true;
await flush();
pressAndReleaseKeyOn(element.textarea!.$.textarea.textarea, 83, 'ctrl'); // 'ctrl + s'
await promise;
});
test('draft saving/editing', async () => {
const dispatchEventStub = sinon.stub(element, 'dispatchEvent');
const fireEditStub = sinon.stub(element, '_fireEdit');
const clock: SinonFakeTimers = sinon.useFakeTimers();
const tickAndFlush = async (repetitions: number) => {
for (let i = 1; i <= repetitions; i++) {
clock.tick(1000);
await flush();
}
};
element.draft = true;
await flush();
tap(queryAndAssert(element, '.edit'));
assert.isTrue(fireEditStub.called);
tickAndFlush(1);
element._messageText = 'good news, everyone!';
tickAndFlush(1);
assert.equal(dispatchEventStub.lastCall.args[0].type, 'comment-update');
assert.isTrue(dispatchEventStub.calledTwice);
element._messageText = 'good news, everyone!';
await flush();
assert.isTrue(dispatchEventStub.calledTwice);
tap(queryAndAssert(element, '.save'));
assert.isTrue(
element.disabled,
'Element should be disabled when creating draft.'
);
let draft = await element._xhrPromise!;
const evt = dispatchEventStub.lastCall.args[0] as CustomEvent<{
comment: DraftInfo;
}>;
assert.equal(evt.type, 'comment-save');
const expectedDetail = {
comment: {
...createComment(),
__draft: true,
__draftID: 'temp_draft_id',
id: 'baf0414d_40572e03' as UrlEncodedCommentId,
line: 5,
message: 'saved!',
path: '/path/to/file',
updated: '2015-12-08 21:52:36.177000000' as Timestamp,
},
patchNum: 1 as PatchSetNum,
};
assert.deepEqual(evt.detail, expectedDetail);
assert.isFalse(
element.disabled,
'Element should be enabled when done creating draft.'
);
assert.equal(draft.message, 'saved!');
assert.isFalse(element.editing);
tap(queryAndAssert(element, '.edit'));
assert.isTrue(fireEditStub.calledTwice);
element._messageText =
'You’ll be delivering a package to Chapek 9, ' +
'a world where humans are killed on sight.';
tap(queryAndAssert(element, '.save'));
assert.isTrue(
element.disabled,
'Element should be disabled when updating draft.'
);
draft = await element._xhrPromise!;
assert.isFalse(
element.disabled,
'Element should be enabled when done updating draft.'
);
assert.equal(draft.message, 'saved!');
assert.isFalse(element.editing);
dispatchEventStub.restore();
});
test('draft prevent save when disabled', async () => {
const saveStub = sinon.stub(element, 'save').returns(Promise.resolve());
element.showActions = true;
element.draft = true;
await flush();
tap(element.$.header);
tap(queryAndAssert(element, '.edit'));
element._messageText = 'good news, everyone!';
await flush();
element.disabled = true;
tap(queryAndAssert(element, '.save'));
assert.isFalse(saveStub.called);
element.disabled = false;
tap(queryAndAssert(element, '.save'));
assert.isTrue(saveStub.calledOnce);
});
test('proper event fires on resolve, comment is not saved', async () => {
const save = sinon.stub(element, 'save');
const promise = mockPromise();
element.addEventListener('comment-update', e => {
assert.isTrue(e.detail.comment.unresolved);
assert.isFalse(save.called);
promise.resolve();
});
tap(queryAndAssert(element, '.resolve input'));
await promise;
});
test('resolved comment state indicated by checkbox', () => {
sinon.stub(element, 'save');
element.comment = {unresolved: false};
assert.isTrue(
(queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
);
element.comment = {unresolved: true};
assert.isFalse(
(queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
);
});
test('resolved checkbox saves with tap when !editing', () => {
element.editing = false;
const save = sinon.stub(element, 'save');
element.comment = {unresolved: false};
assert.isTrue(
(queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
);
element.comment = {unresolved: true};
assert.isFalse(
(queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
);
assert.isFalse(save.called);
tap(element.$.resolvedCheckbox);
assert.isTrue(
(queryAndAssert(element, '.resolve input') as HTMLInputElement).checked
);
assert.isTrue(save.called);
});
suite('draft saving messages', () => {
test('_getSavingMessage', () => {
assert.equal(element._getSavingMessage(0), 'All changes saved');
assert.equal(element._getSavingMessage(1), 'Saving 1 draft...');
assert.equal(element._getSavingMessage(2), 'Saving 2 drafts...');
assert.equal(element._getSavingMessage(3), 'Saving 3 drafts...');
});
test('_show{Start,End}Request', () => {
const updateStub = sinon.stub(element, '_updateRequestToast');
element._numPendingDraftRequests.number = 1;
element._showStartRequest();
assert.isTrue(updateStub.calledOnce);
assert.equal(updateStub.lastCall.args[0], 2);
assert.equal(element._numPendingDraftRequests.number, 2);
element._showEndRequest();
assert.isTrue(updateStub.calledTwice);
assert.equal(updateStub.lastCall.args[0], 1);
assert.equal(element._numPendingDraftRequests.number, 1);
element._showEndRequest();
assert.isTrue(updateStub.calledThrice);
assert.equal(updateStub.lastCall.args[0], 0);
assert.equal(element._numPendingDraftRequests.number, 0);
});
});
test('cancelling an unsaved draft discards, persists in storage', async () => {
const clock: SinonFakeTimers = sinon.useFakeTimers();
const tickAndFlush = async (repetitions: number) => {
for (let i = 1; i <= repetitions; i++) {
clock.tick(1000);
await flush();
}
};
const discardSpy = sinon.spy(element, '_fireDiscard');
const storeStub = stubStorage('setDraftComment');
const eraseStub = stubStorage('eraseDraftComment');
element.comment!.id = undefined; // set id undefined for draft
element._messageText = 'test text';
tickAndFlush(1);
assert.isTrue(storeStub.called);
assert.equal(storeStub.lastCall.args[1], 'test text');
element._handleCancel({
...new Event('click'),
preventDefault: sinon.stub(),
});
await flush();
assert.isTrue(discardSpy.called);
assert.isFalse(eraseStub.called);
});
test('cancelling edit on a saved draft does not store', () => {
element.comment!.id = 'foo' as UrlEncodedCommentId;
const discardSpy = sinon.spy(element, '_fireDiscard');
const storeStub = stubStorage('setDraftComment');
element.comment!.id = undefined; // set id undefined for draft
element._messageText = 'test text';
flush();
assert.isFalse(storeStub.called);
element._handleCancel({...new Event('click'), preventDefault: () => {}});
assert.isTrue(discardSpy.called);
});
test('deleting text from saved draft and saving deletes the draft', () => {
element.comment = {
...createComment(),
id: 'foo' as UrlEncodedCommentId,
message: 'test',
};
element._messageText = '';
const discardStub = sinon.stub(element, '_discardDraft');
element.save();
assert.isTrue(discardStub.called);
});
test('_handleFix fires create-fix event', async () => {
const promise = mockPromise();
element.addEventListener(
'create-fix-comment',
(e: CreateFixCommentEvent) => {
assert.deepEqual(e.detail, element._getEventPayload());
promise.resolve();
}
);
element.isRobotComment = true;
element.comments = [element.comment!];
await flush();
tap(queryAndAssert(element, '.fix'));
await promise;
});
test('do not show Please Fix button if human reply exists', () => {
element.comments = [
{
robot_id: 'happy_robot_id' as RobotId,
robot_run_id: '5838406743490560' as RobotRunId,
fix_suggestions: [
{
fix_id: '478ff847_3bf47aaf' as FixId,
description: 'Make the smiley happier by giving it a nose.',
replacements: [
{
path: 'Documentation/config-gerrit.txt',
range: {
start_line: 10,
start_character: 7,
end_line: 10,
end_character: 9,
},
replacement: ':-)',
},
],
},
],
author: {
_account_id: 1030912 as AccountId,
name: 'Alice Kober-Sotzek',
email: 'aliceks@google.com' as EmailAddress,
avatars: [
{
url: '/s32-p/photo.jpg',
height: 32,
width: 32,
},
{
url: '/AaAdOFzPlFI/s56-p/photo.jpg',
height: 56,
width: 32,
},
{
url: '/AaAdOFzPlFI/s100-p/photo.jpg',
height: 100,
width: 32,
},
{
url: '/AaAdOFzPlFI/s120-p/photo.jpg',
height: 120,
width: 32,
},
],
},
patch_set: 1 as PatchSetNum,
...createComment(),
id: 'eb0d03fd_5e95904f' as UrlEncodedCommentId,
line: 10,
updated: '2017-04-04 15:36:17.000000000' as Timestamp,
message: 'This is a robot comment with a fix.',
unresolved: false,
collapsed: false,
},
{
__draft: true,
__draftID: '0.wbrfbwj89sa',
__date: new Date(),
path: 'Documentation/config-gerrit.txt',
side: CommentSide.REVISION,
line: 10,
in_reply_to: 'eb0d03fd_5e95904f' as UrlEncodedCommentId,
message: '> This is a robot comment with a fix.\n\nPlease fix.',
unresolved: true,
},
];
element.comment = element.comments[0];
flush();
assert.isNull(
element.shadowRoot?.querySelector('robotActions gr-button')
);
});
test('show Please Fix if no human reply', () => {
element.comments = [
{
robot_id: 'happy_robot_id' as RobotId,
robot_run_id: '5838406743490560' as RobotRunId,
fix_suggestions: [
{
fix_id: '478ff847_3bf47aaf' as FixId,
description: 'Make the smiley happier by giving it a nose.',
replacements: [
{
path: 'Documentation/config-gerrit.txt',
range: {
start_line: 10,
start_character: 7,
end_line: 10,
end_character: 9,
},
replacement: ':-)',
},
],
},
],
author: {
_account_id: 1030912 as AccountId,
name: 'Alice Kober-Sotzek',
email: 'aliceks@google.com' as EmailAddress,
avatars: [
{
url: '/s32-p/photo.jpg',
height: 32,
width: 32,
},
{
url: '/AaAdOFzPlFI/s56-p/photo.jpg',
height: 56,
width: 32,
},
{
url: '/AaAdOFzPlFI/s100-p/photo.jpg',
height: 100,
width: 32,
},
{
url: '/AaAdOFzPlFI/s120-p/photo.jpg',
height: 120,
width: 32,
},
],
},
patch_set: 1 as PatchSetNum,
...createComment(),
id: 'eb0d03fd_5e95904f' as UrlEncodedCommentId,
line: 10,
updated: '2017-04-04 15:36:17.000000000' as Timestamp,
message: 'This is a robot comment with a fix.',
unresolved: false,
collapsed: false,
},
];
element.comment = element.comments[0];
flush();
queryAndAssert(element, '.robotActions gr-button');
});
test('_handleShowFix fires open-fix-preview event', async () => {
const promise = mockPromise();
element.addEventListener('open-fix-preview', e => {
assert.deepEqual(e.detail, element._getEventPayload());
promise.resolve();
});
element.comment = {
...createComment(),
fix_suggestions: [{...createFixSuggestionInfo()}],
};
element.isRobotComment = true;
await flush();
tap(queryAndAssert(element, '.show-fix'));
await promise;
});
});
suite('respectful tips', () => {
let element: GrComment;
let clock: sinon.SinonFakeTimers;
setup(() => {
stubRestApi('getAccount').returns(Promise.resolve(undefined));
clock = sinon.useFakeTimers();
});
teardown(() => {
clock.restore();
sinon.restore();
});
test('show tip when no cached record', async () => {
element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
// fake random
element.getRandomNum = () => 0;
element.comment = {__editing: true, __draft: true};
await flush();
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
});
test('add 14-day delays once dismissed', async () => {
element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
// fake random
element.getRandomNum = () => 0;
element.comment = {__editing: true, __draft: true};
await flush();
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
assert.isTrue(respectfulSetStub.lastCall.args[0] === undefined);
assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
tap(queryAndAssert(element, '.respectfulReviewTip .close'));
flush();
assert.isTrue(respectfulSetStub.lastCall.args[0] === 14);
});
test('do not show tip when fall out of probability', async () => {
element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
// fake random
element.getRandomNum = () => 3;
element.comment = {__editing: true, __draft: true};
await flush();
assert.isTrue(respectfulGetStub.called);
assert.isFalse(respectfulSetStub.called);
assert.isNotOk(query(element, '.respectfulReviewTip'));
});
test('show tip when editing changed to true', async () => {
element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns(null);
// fake random
element.getRandomNum = () => 0;
element.comment = {__editing: false};
await flush();
assert.isFalse(respectfulGetStub.called);
assert.isFalse(respectfulSetStub.called);
assert.isNotOk(query(element, '.respectfulReviewTip'));
element.editing = true;
await flush();
assert.isTrue(respectfulGetStub.called);
assert.isTrue(respectfulSetStub.called);
assert.isTrue(!!queryAndAssert(element, '.respectfulReviewTip'));
});
test('no tip when cached record', async () => {
element = draftFixture.instantiate() as GrComment;
const respectfulGetStub = stubStorage('getRespectfulTipVisibility');
const respectfulSetStub = stubStorage('setRespectfulTipVisibility');
respectfulGetStub.returns({updated: 0});
// fake random
element.getRandomNum = () => 0;
element.comment = {__editing: true, __draft: true};
await flush();
assert.isTrue(respectfulGetStub.called);
assert.isFalse(respectfulSetStub.called);
assert.isNotOk(query(element, '.respectfulReviewTip'));
});
});
});