| /** |
| * @license |
| * Copyright 2015 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup'; |
| import './gr-comment'; |
| import {AUTO_SAVE_DEBOUNCE_DELAY_MS, GrComment} from './gr-comment'; |
| import { |
| queryAndAssert, |
| stubRestApi, |
| query, |
| pressKey, |
| listenOnce, |
| mockPromise, |
| waitUntilCalled, |
| dispatch, |
| MockPromise, |
| stubFlags, |
| waitUntil, |
| } from '../../../test/test-utils'; |
| import { |
| AccountId, |
| DraftInfo, |
| SavingState, |
| EmailAddress, |
| NumericChangeId, |
| PatchSetNum, |
| Timestamp, |
| UrlEncodedCommentId, |
| FixId, |
| } from '../../../types/common'; |
| import { |
| createComment, |
| createDraft, |
| createRobotComment, |
| createNewDraft, |
| } from '../../../test/test-data-generators'; |
| import {ReplyToCommentEvent} from '../../../types/events'; |
| import {GrConfirmDeleteCommentDialog} from '../gr-confirm-delete-comment-dialog/gr-confirm-delete-comment-dialog'; |
| import {assertIsDefined} from '../../../utils/common-util'; |
| import {Key, Modifier} from '../../../utils/dom-util'; |
| import {SinonStubbedMember} from 'sinon'; |
| import {fixture, html, assert} from '@open-wc/testing'; |
| import {GrButton} from '../gr-button/gr-button'; |
| import {testResolver} from '../../../test/common-test-setup'; |
| import { |
| CommentsModel, |
| commentsModelToken, |
| } from '../../../models/comments/comments-model'; |
| import {KnownExperimentId} from '../../../services/flags/flags'; |
| |
| suite('gr-comment tests', () => { |
| let element: GrComment; |
| let commentsModel: CommentsModel; |
| const account = { |
| 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, |
| }; |
| const comment = { |
| ...createComment(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| id: 'baf0414d_60047215' as UrlEncodedCommentId, |
| line: 5, |
| message: 'This is the test comment message.', |
| updated: '2015-12-08 19:48:33.843000000' as Timestamp, |
| }; |
| |
| setup(async () => { |
| element = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| ></gr-comment>` |
| ); |
| commentsModel = testResolver(commentsModelToken); |
| }); |
| |
| suite('DOM rendering', () => { |
| test('renders collapsed', async () => { |
| const initiallyCollapsedElement = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| .initiallyCollapsed=${true} |
| ></gr-comment>` |
| ); |
| assert.shadowDom.equal( |
| initiallyCollapsedElement, |
| /* HTML */ ` |
| <gr-endpoint-decorator name="comment"> |
| <gr-endpoint-param name="comment"></gr-endpoint-param> |
| <gr-endpoint-param name="editing"></gr-endpoint-param> |
| <gr-endpoint-param name="message"></gr-endpoint-param> |
| <gr-endpoint-param name="isDraft"></gr-endpoint-param> |
| <div class="container" id="container"> |
| <div class="header" id="header"> |
| <div class="headerLeft"> |
| <gr-account-label deselected=""></gr-account-label> |
| </div> |
| <div class="headerMiddle"> |
| <span class="collapsedContent"> |
| This is the test comment message. |
| </span> |
| </div> |
| <span class="patchset-text">Patchset 1</span> |
| <div class="show-hide" tabindex="0"> |
| <label aria-label="Expand" class="show-hide"> |
| <input checked="" class="show-hide" type="checkbox" /> |
| <gr-icon id="icon" icon="expand_more"></gr-icon> |
| </label> |
| </div> |
| </div> |
| <div class="body"> |
| <gr-endpoint-slot name="above-actions"></gr-endpoint-slot> |
| </div> |
| </div> |
| </gr-endpoint-decorator> |
| <dialog id="confirmDeleteModal" tabindex="-1"> |
| <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog"> |
| </gr-confirm-delete-comment-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| |
| test('renders expanded', async () => { |
| element.initiallyCollapsed = false; |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <gr-endpoint-decorator name="comment"> |
| <gr-endpoint-param name="comment"></gr-endpoint-param> |
| <gr-endpoint-param name="editing"></gr-endpoint-param> |
| <gr-endpoint-param name="message"></gr-endpoint-param> |
| <gr-endpoint-param name="isDraft"></gr-endpoint-param> |
| <div class="container" id="container"> |
| <div class="header" id="header"> |
| <div class="headerLeft"> |
| <gr-account-label deselected=""></gr-account-label> |
| </div> |
| <div class="headerMiddle"></div> |
| <span class="patchset-text">Patchset 1</span> |
| <span class="separator"></span> |
| <span class="date" tabindex="0"> |
| <gr-date-formatter withtooltip=""></gr-date-formatter> |
| </span> |
| <div class="show-hide" tabindex="0"> |
| <label aria-label="Collapse" class="show-hide"> |
| <input class="show-hide" type="checkbox" /> |
| <gr-icon id="icon" icon="expand_less"></gr-icon> |
| </label> |
| </div> |
| </div> |
| <div class="body"> |
| <gr-formatted-text class="message"></gr-formatted-text> |
| <gr-endpoint-slot name="above-actions"></gr-endpoint-slot> |
| </div> |
| </div> |
| </gr-endpoint-decorator> |
| <dialog id="confirmDeleteModal" tabindex="-1"> |
| <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog"> |
| </gr-confirm-delete-comment-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| |
| test('renders expanded robot', async () => { |
| element.initiallyCollapsed = false; |
| element.comment = createRobotComment(); |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <gr-endpoint-decorator name="comment"> |
| <gr-endpoint-param name="comment"></gr-endpoint-param> |
| <gr-endpoint-param name="editing"></gr-endpoint-param> |
| <gr-endpoint-param name="message"></gr-endpoint-param> |
| <gr-endpoint-param name="isDraft"></gr-endpoint-param> |
| <div class="container" id="container"> |
| <div class="header" id="header"> |
| <div class="headerLeft"> |
| <span class="robotName">robot-id-123</span> |
| </div> |
| <div class="headerMiddle"></div> |
| <span class="patchset-text">Patchset 1</span> |
| <span class="separator"></span> |
| <span class="date" tabindex="0"> |
| <gr-date-formatter withtooltip=""></gr-date-formatter> |
| </span> |
| <div class="show-hide" tabindex="0"> |
| <label aria-label="Collapse" class="show-hide"> |
| <input class="show-hide" type="checkbox" /> |
| <gr-icon id="icon" icon="expand_less"></gr-icon> |
| </label> |
| </div> |
| </div> |
| <div class="body"> |
| <div class="robotId"></div> |
| <gr-formatted-text class="message"></gr-formatted-text> |
| <gr-endpoint-slot name="above-actions"></gr-endpoint-slot> |
| <div class="robotActions"> |
| <gr-icon |
| icon="link" |
| class="copy link-icon" |
| role="button" |
| tabindex="0" |
| title="Copy link to this comment" |
| ></gr-icon> |
| <gr-endpoint-decorator name="robot-comment-controls"> |
| <gr-endpoint-param name="comment"></gr-endpoint-param> |
| </gr-endpoint-decorator> |
| <gr-button |
| aria-disabled="false" |
| class="action show-fix" |
| link="" |
| role="button" |
| secondary="" |
| tabindex="0" |
| > |
| Show Fix |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action fix" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Please Fix |
| </gr-button> |
| </div> |
| </div> |
| </div> |
| </gr-endpoint-decorator> |
| <dialog id="confirmDeleteModal" tabindex="-1"> |
| <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog"> |
| </gr-confirm-delete-comment-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| |
| test('renders expanded admin', async () => { |
| element.initiallyCollapsed = false; |
| element.isAdmin = true; |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, 'gr-button.delete'), |
| /* HTML */ ` |
| <gr-button |
| aria-disabled="false" |
| class="action delete" |
| id="deleteBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| title="Delete Comment" |
| > |
| <gr-icon id="icon" icon="delete" filled></gr-icon> |
| </gr-button> |
| ` |
| ); |
| }); |
| |
| test('renders draft', async () => { |
| element.initiallyCollapsed = false; |
| (element.comment as DraftInfo).savingState = SavingState.OK; |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <gr-endpoint-decorator name="comment"> |
| <gr-endpoint-param name="comment"></gr-endpoint-param> |
| <gr-endpoint-param name="editing"></gr-endpoint-param> |
| <gr-endpoint-param name="message"></gr-endpoint-param> |
| <gr-endpoint-param name="isDraft"></gr-endpoint-param> |
| <div class="container draft" id="container"> |
| <div class="header" id="header"> |
| <div class="headerLeft"> |
| <gr-tooltip-content |
| class="draftTooltip" |
| has-tooltip="" |
| max-width="20em" |
| title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key." |
| > |
| <gr-icon filled icon="rate_review"></gr-icon> |
| <span class="draftLabel">Draft</span> |
| </gr-tooltip-content> |
| </div> |
| <div class="headerMiddle"></div> |
| <span class="patchset-text">Patchset 1</span> |
| <span class="separator"></span> |
| <span class="date" tabindex="0"> |
| <gr-date-formatter withtooltip=""></gr-date-formatter> |
| </span> |
| <div class="show-hide" tabindex="0"> |
| <label aria-label="Collapse" class="show-hide"> |
| <input class="show-hide" type="checkbox" /> |
| <gr-icon id="icon" icon="expand_less"></gr-icon> |
| </label> |
| </div> |
| </div> |
| <div class="body"> |
| <gr-formatted-text class="message"></gr-formatted-text> |
| <gr-endpoint-slot name="above-actions"></gr-endpoint-slot> |
| <div class="actions"> |
| <div class="leftActions"> |
| <div class="action resolve"> |
| <label> |
| <input id="resolvedCheckbox" type="checkbox" /> |
| Resolved |
| </label> |
| </div> |
| </div> |
| <div class="rightActions"> |
| <gr-button |
| aria-disabled="false" |
| class="action discard" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Discard |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action edit" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Edit |
| </gr-button> |
| <gr-endpoint-slot name="draft-actions-end"> |
| </gr-endpoint-slot> |
| </div> |
| </div> |
| </div> |
| </div> |
| </gr-endpoint-decorator> |
| <dialog id="confirmDeleteModal" tabindex="-1"> |
| <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog"> |
| </gr-confirm-delete-comment-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| |
| test('renders draft in editing mode', async () => { |
| element.initiallyCollapsed = false; |
| (element.comment as DraftInfo).savingState = SavingState.OK; |
| element.editing = true; |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <gr-endpoint-decorator name="comment"> |
| <gr-endpoint-param name="comment"></gr-endpoint-param> |
| <gr-endpoint-param name="editing"></gr-endpoint-param> |
| <gr-endpoint-param name="message"></gr-endpoint-param> |
| <gr-endpoint-param name="isDraft"></gr-endpoint-param> |
| <div class="container draft" id="container"> |
| <div class="header" id="header"> |
| <div class="headerLeft"> |
| <gr-tooltip-content |
| class="draftTooltip" |
| has-tooltip="" |
| max-width="20em" |
| title="This draft is only visible to you. To publish drafts, click the 'Reply' or 'Start review' button at the top of the change or press the 'a' key." |
| > |
| <gr-icon filled icon="rate_review"></gr-icon> |
| <span class="draftLabel">Draft</span> |
| </gr-tooltip-content> |
| </div> |
| <div class="headerMiddle"></div> |
| <gr-button |
| aria-disabled="false" |
| class="action suggestEdit" |
| link="" |
| role="button" |
| tabindex="0" |
| title="This button copies the text to make a suggestion" |
| > |
| <gr-icon filled="" icon="edit" id="icon"> </gr-icon> |
| Suggest edit |
| </gr-button> |
| <span class="patchset-text">Patchset 1</span> |
| <span class="separator"></span> |
| <span class="date" tabindex="0"> |
| <gr-date-formatter withtooltip=""></gr-date-formatter> |
| </span> |
| <div class="show-hide" tabindex="0"> |
| <label aria-label="Collapse" class="show-hide"> |
| <input class="show-hide" type="checkbox" /> |
| <gr-icon id="icon" icon="expand_less"></gr-icon> |
| </label> |
| </div> |
| </div> |
| <div class="body"> |
| <gr-suggestion-textarea |
| autocomplete="on" |
| autocompletehint="" |
| class="code editMessage" |
| code="" |
| id="editTextarea" |
| rows="4" |
| text="This is the test comment message." |
| > |
| </gr-suggestion-textarea> |
| <gr-endpoint-slot name="above-actions"></gr-endpoint-slot> |
| <div class="actions"> |
| <div class="leftActions"> |
| <div class="action resolve"> |
| <label> |
| <input id="resolvedCheckbox" type="checkbox" /> |
| Resolved |
| </label> |
| </div> |
| </div> |
| <div class="rightActions"> |
| <gr-button |
| aria-disabled="false" |
| class="action cancel" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Cancel |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action save" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Save |
| </gr-button> |
| <gr-endpoint-slot name="draft-actions-end"> |
| </gr-endpoint-slot> |
| </div> |
| </div> |
| </div> |
| </div> |
| </gr-endpoint-decorator> |
| <dialog id="confirmDeleteModal" tabindex="-1"> |
| <gr-confirm-delete-comment-dialog id="confirmDeleteCommentDialog"> |
| </gr-confirm-delete-comment-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| }); |
| |
| test('clicking on date link fires event', async () => { |
| const stub = sinon.stub(); |
| element.addEventListener('comment-anchor-tap', stub); |
| await element.updateComplete; |
| |
| const dateEl = queryAndAssert<HTMLSpanElement>(element, '.date'); |
| dateEl.click(); |
| |
| assert.isTrue(stub.called); |
| assert.deepEqual(stub.lastCall.args[0].detail, { |
| side: 'REVISION', |
| number: element.comment!.line, |
| }); |
| }); |
| |
| test('comment message sets messageText only when empty', async () => { |
| element.changeNum = 1 as NumericChangeId; |
| element.messageText = ''; |
| element.comment = { |
| ...createComment(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| }; |
| element.editing = true; |
| await element.updateComplete; |
| // messageText was empty so overwrite the message now |
| assert.equal(element.messageText, 'hello world'); |
| |
| element.comment.message = 'new message'; |
| await element.updateComplete; |
| // messageText was already set so do not overwrite it |
| assert.equal(element.messageText, 'hello world'); |
| }); |
| |
| test('comment message sets messageText when not edited', async () => { |
| element.changeNum = 1 as NumericChangeId; |
| element.messageText = 'Some text'; |
| element.comment = { |
| ...createComment(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| }; |
| element.editing = true; |
| await element.updateComplete; |
| // messageText was empty so overwrite the message now |
| assert.equal(element.messageText, 'hello world'); |
| |
| element.comment.message = 'new message'; |
| await element.updateComplete; |
| // messageText was already set so do not overwrite it |
| assert.equal(element.messageText, 'hello world'); |
| }); |
| |
| test('delete comment', async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.isAdmin = true; |
| await element.updateComplete; |
| |
| const deleteButton = queryAndAssert<GrButton>(element, '.action.delete'); |
| deleteButton.click(); |
| await element.updateComplete; |
| |
| assertIsDefined(element.confirmDeleteModal, 'confirmDeleteModal'); |
| const dialog = queryAndAssert<GrConfirmDeleteCommentDialog>( |
| element.confirmDeleteModal, |
| '#confirmDeleteCommentDialog' |
| ); |
| dialog.message = 'removal reason'; |
| await element.updateComplete; |
| |
| const stub = stubRestApi('deleteComment').returns( |
| Promise.resolve(createComment()) |
| ); |
| element.handleConfirmDeleteComment(); |
| assert.isTrue( |
| stub.calledWith( |
| 42 as NumericChangeId, |
| 1 as PatchSetNum, |
| 'baf0414d_60047215' as UrlEncodedCommentId, |
| 'removal reason' |
| ) |
| ); |
| }); |
| |
| suite('gr-comment draft tests', () => { |
| setup(async () => { |
| element.changeNum = 42 as NumericChangeId; |
| element.comment = { |
| ...createComment(), |
| savingState: SavingState.OK, |
| path: '/path/to/file', |
| line: 5, |
| }; |
| }); |
| |
| test('isSaveDisabled', async () => { |
| element.unresolved = true; |
| element.comment = {...createComment(), unresolved: true}; |
| element.messageText = 'asdf'; |
| await element.updateComplete; |
| assert.isFalse(element.isSaveDisabled()); |
| |
| element.messageText = ''; |
| await element.updateComplete; |
| assert.isTrue(element.isSaveDisabled()); |
| |
| // After changing the 'resolved' state of the comment the 'Save' button |
| // should stay disabled, if the message is empty. |
| element.unresolved = false; |
| await element.updateComplete; |
| assert.isTrue(element.isSaveDisabled()); |
| |
| element.comment = {...element.comment, savingState: SavingState.SAVING}; |
| await element.updateComplete; |
| assert.isTrue(element.isSaveDisabled()); |
| }); |
| |
| test('ctrl+s saves comment', async () => { |
| const spy = sinon.stub(element, 'save'); |
| element.messageText = 'is that the horse from horsing around??'; |
| element.editing = true; |
| await element.updateComplete; |
| pressKey(element.textarea!, 's', Modifier.CTRL_KEY); |
| assert.isTrue(spy.called); |
| }); |
| |
| suite('ctrl+ENTER ', () => { |
| test('saves comment', async () => { |
| const spy = sinon.stub(element, 'save'); |
| element.messageText = 'is that the horse from horsing around??'; |
| element.editing = true; |
| await element.updateComplete; |
| pressKey(element.textarea!, Key.ENTER, Modifier.CTRL_KEY); |
| assert.isTrue(spy.called); |
| }); |
| test('propagates on patchset comment', async () => { |
| const event = new KeyboardEvent('keydown', { |
| key: Key.ENTER, |
| ctrlKey: true, |
| }); |
| const stopPropagationStub = sinon.stub(event, 'stopPropagation'); |
| element.permanentEditingMode = true; |
| element.messageText = 'is that the horse from horsing around??'; |
| element.editing = true; |
| await element.updateComplete; |
| element.dispatchEvent(event); |
| assert.isFalse(stopPropagationStub.called); |
| }); |
| test('does not propagate on normal comment', async () => { |
| const event = new KeyboardEvent('keydown', { |
| key: Key.ENTER, |
| ctrlKey: true, |
| }); |
| const stopPropagationStub = sinon.stub(event, 'stopPropagation'); |
| element.messageText = 'is that the horse from horsing around??'; |
| element.editing = true; |
| await element.updateComplete; |
| element.dispatchEvent(event); |
| assert.isTrue(stopPropagationStub.called); |
| }); |
| }); |
| |
| test('save', async () => { |
| const savePromise = mockPromise<DraftInfo>(); |
| const stub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise); |
| |
| element.comment = createDraft(); |
| element.editing = true; |
| await element.updateComplete; |
| const textToSave = 'something, not important'; |
| element.messageText = textToSave; |
| element.unresolved = true; |
| await element.updateComplete; |
| |
| element.save(); |
| |
| await element.updateComplete; |
| waitUntilCalled(stub, 'saveDraft()'); |
| assert.equal(stub.lastCall.firstArg.message, textToSave); |
| assert.equal(stub.lastCall.firstArg.unresolved, true); |
| assert.isFalse(element.editing); |
| |
| savePromise.resolve(); |
| await element.updateComplete; |
| |
| assert.isFalse(element.editing); |
| }); |
| |
| test('previewing formatting triggers save', async () => { |
| element.permanentEditingMode = true; |
| |
| const saveStub = sinon.stub(element, 'save').returns(Promise.resolve()); |
| |
| element.comment = createDraft(); |
| element.editing = true; |
| element.messageText = 'something, not important'; |
| await element.updateComplete; |
| |
| assert.isFalse(saveStub.called); |
| |
| queryAndAssert<GrButton>(element, '.save').click(); |
| |
| assert.isTrue(saveStub.called); |
| }); |
| |
| test('save failed', async () => { |
| sinon.stub(commentsModel, 'saveDraft').returns( |
| Promise.resolve({ |
| ...createNewDraft(), |
| message: 'something, not important', |
| unresolved: true, |
| savingState: SavingState.ERROR, |
| }) |
| ); |
| |
| element.comment = createNewDraft({ |
| message: '', |
| unresolved: true, |
| }); |
| element.unresolved = true; |
| element.editing = true; |
| await element.updateComplete; |
| element.messageText = 'something, not important'; |
| await element.updateComplete; |
| |
| element.save(); |
| assert.isFalse(element.editing); |
| |
| await waitUntil(() => element.hasAttribute('error')); |
| assert.isTrue(element.editing); |
| }); |
| |
| test('discard', async () => { |
| const discardPromise = mockPromise<void>(); |
| const stub = sinon |
| .stub(commentsModel, 'discardDraft') |
| .returns(discardPromise); |
| |
| element.comment = createDraft(); |
| element.editing = true; |
| await element.updateComplete; |
| |
| element.discard(); |
| |
| await element.updateComplete; |
| waitUntilCalled(stub, 'discardDraft()'); |
| assert.equal(stub.lastCall.firstArg, element.comment.id); |
| assert.isFalse(element.editing); |
| |
| discardPromise.resolve(); |
| await element.updateComplete; |
| |
| assert.isFalse(element.editing); |
| }); |
| |
| test('resolved comment state indicated by checkbox', async () => { |
| const saveStub = sinon.stub(commentsModel, 'saveDraft'); |
| element.comment = { |
| ...createComment(), |
| savingState: SavingState.OK, |
| unresolved: false, |
| }; |
| await element.updateComplete; |
| |
| let checkbox = queryAndAssert<HTMLInputElement>( |
| element, |
| '#resolvedCheckbox' |
| ); |
| assert.isTrue(checkbox.checked); |
| |
| checkbox.click(); |
| await element.updateComplete; |
| |
| checkbox = queryAndAssert<HTMLInputElement>(element, '#resolvedCheckbox'); |
| assert.isFalse(checkbox.checked); |
| |
| assert.isTrue(saveStub.called); |
| }); |
| |
| test('saving empty text calls discard()', async () => { |
| const saveStub = sinon.stub(commentsModel, 'saveDraft'); |
| const discardStub = sinon.stub(commentsModel, 'discardDraft'); |
| element.comment = createDraft(); |
| element.editing = true; |
| await element.updateComplete; |
| |
| element.messageText = ''; |
| await element.updateComplete; |
| |
| await element.save(); |
| assert.isTrue(discardStub.called); |
| assert.isFalse(saveStub.called); |
| }); |
| |
| test('converting to input for empty text calls discard()', async () => { |
| const saveStub = sinon.stub(commentsModel, 'saveDraft'); |
| const discardStub = sinon.stub(commentsModel, 'discardDraft'); |
| element.comment = createDraft(); |
| element.editing = true; |
| await element.updateComplete; |
| |
| element.messageText = ''; |
| await element.updateComplete; |
| |
| await element.convertToCommentInputAndOrDiscard(); |
| assert.isTrue(discardStub.called); |
| assert.isFalse(saveStub.called); |
| }); |
| |
| test('handlePleaseFix fires reply-to-comment event', async () => { |
| const listener = listenOnce<ReplyToCommentEvent>( |
| element, |
| 'reply-to-comment' |
| ); |
| element.comment = createRobotComment(); |
| element.comments = [element.comment]; |
| await element.updateComplete; |
| |
| queryAndAssert<GrButton>(element, '.fix').click(); |
| |
| const e = await listener; |
| assert.equal(e.detail.unresolved, true); |
| assert.equal(e.detail.userWantsToEdit, false); |
| assert.isTrue(e.detail.content.includes('Please fix.')); |
| }); |
| |
| test('do not show Please Fix button if human reply exists', async () => { |
| element.initiallyCollapsed = false; |
| const robotComment = createRobotComment(); |
| element.comment = robotComment; |
| await element.updateComplete; |
| |
| let actions = query(element, '.robotActions gr-button.fix'); |
| assert.isOk(actions); |
| |
| element.comments = [ |
| robotComment, |
| {...createComment(), in_reply_to: robotComment.id}, |
| ]; |
| await element.updateComplete; |
| actions = query(element, '.robotActions gr-button.fix'); |
| assert.isNotOk(actions); |
| }); |
| }); |
| |
| suite('auto saving', () => { |
| let clock: sinon.SinonFakeTimers; |
| let savePromise: MockPromise<DraftInfo>; |
| let saveStub: SinonStubbedMember<CommentsModel['saveDraft']>; |
| |
| setup(async () => { |
| clock = sinon.useFakeTimers(); |
| savePromise = mockPromise<DraftInfo>(); |
| saveStub = sinon.stub(commentsModel, 'saveDraft').returns(savePromise); |
| |
| element.comment = createNewDraft(); |
| element.editing = true; |
| await element.updateComplete; |
| }); |
| |
| teardown(() => { |
| clock.restore(); |
| sinon.restore(); |
| }); |
| |
| test('basic auto saving', async () => { |
| const textarea = queryAndAssert<HTMLElement>(element, '#editTextarea'); |
| dispatch(textarea, 'text-changed', {value: 'some new text '}); |
| |
| clock.tick(AUTO_SAVE_DEBOUNCE_DELAY_MS / 2); |
| assert.isFalse(saveStub.called); |
| |
| clock.tick(AUTO_SAVE_DEBOUNCE_DELAY_MS); |
| assert.isTrue(saveStub.called); |
| assert.equal( |
| saveStub.firstCall.firstArg.message, |
| 'some new text '.trimEnd() |
| ); |
| }); |
| |
| test('saving while auto saving', async () => { |
| saveStub.reset(); |
| const autoSavePromise = mockPromise<DraftInfo>(); |
| saveStub.onCall(0).returns(autoSavePromise); |
| saveStub.onCall(1).returns(savePromise); |
| |
| const textarea = queryAndAssert<HTMLElement>(element, '#editTextarea'); |
| dispatch(textarea, 'text-changed', {value: 'auto save text'}); |
| |
| clock.tick(2 * AUTO_SAVE_DEBOUNCE_DELAY_MS); |
| assert.equal(saveStub.callCount, 1); |
| assert.equal(saveStub.firstCall.firstArg.message, 'auto save text'); |
| |
| element.messageText = 'actual save text'; |
| const save = element.save(); |
| await element.updateComplete; |
| // First wait for the auto saving to finish. |
| assert.equal(saveStub.callCount, 1); |
| |
| autoSavePromise.resolve({ |
| ...element.comment, |
| savingState: SavingState.OK, |
| message: 'auto save text', |
| id: 'exp123' as UrlEncodedCommentId, |
| updated: '2018-02-13 22:48:48.018000000' as Timestamp, |
| }); |
| savePromise.resolve({ |
| ...element.comment, |
| savingState: SavingState.OK, |
| message: 'actual save text', |
| id: 'exp123' as UrlEncodedCommentId, |
| updated: '2018-02-13 22:48:49.018000000' as Timestamp, |
| }); |
| await save; |
| // Only then save. |
| assert.equal(saveStub.callCount, 2); |
| assert.equal(saveStub.lastCall.firstArg.message, 'actual save text'); |
| assert.equal(saveStub.lastCall.firstArg.id, 'exp123'); |
| }); |
| }); |
| |
| suite('handleTextChangedForAutocomplete', () => { |
| test('foo -> foo with asdf', async () => { |
| element.autocompleteHint = 'asdf'; |
| element.handleTextChangedForAutocomplete('foo', 'foo'); |
| assert.equal(element.autocompleteHint, 'asdf'); |
| }); |
| |
| test('foo -> bar with asdf', async () => { |
| element.autocompleteHint = 'asdf'; |
| element.handleTextChangedForAutocomplete('foo', 'bar'); |
| assert.equal(element.autocompleteHint, ''); |
| }); |
| |
| test('foo -> foofoo with asdf', async () => { |
| element.autocompleteHint = 'asdf'; |
| element.handleTextChangedForAutocomplete('foo', 'foofoo'); |
| assert.equal(element.autocompleteHint, ''); |
| }); |
| |
| test('foo -> foofoo with foomore', async () => { |
| element.autocompleteHint = 'foomore'; |
| element.handleTextChangedForAutocomplete('foo', 'foofoo'); |
| assert.equal(element.autocompleteHint, 'more'); |
| }); |
| }); |
| |
| suite('suggest edit', () => { |
| let element: GrComment; |
| setup(async () => { |
| stubFlags('isEnabled').returns(true); |
| const comment = { |
| ...createComment(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| }; |
| element = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| .initiallyCollapsed=${false} |
| ></gr-comment>` |
| ); |
| element.editing = true; |
| }); |
| test('renders suggest edit button', () => { |
| assert.dom.equal( |
| queryAndAssert(element, 'gr-button.suggestEdit'), |
| /* HTML */ `<gr-button |
| class="action suggestEdit" |
| link="" |
| role="button" |
| tabindex="0" |
| title="This button copies the text to make a suggestion" |
| > |
| <gr-icon icon="edit" id="icon" filled></gr-icon> Suggest edit |
| </gr-button> ` |
| ); |
| }); |
| }); |
| |
| suite('suggested fix', () => { |
| let element: GrComment; |
| const generatedFixSuggestion = { |
| description: 'prompt_to_edit', |
| fix_id: 'ml' as FixId, |
| replacements: [ |
| { |
| path: 'google3/ts', |
| range: { |
| start_line: 83, |
| start_character: 0, |
| end_line: 83, |
| end_character: 0, |
| }, |
| replacement: "import {useUtil} from '../../../utils/use_util';\n", |
| }, |
| { |
| path: 'google3/ts', |
| range: { |
| start_line: 985, |
| start_character: 0, |
| end_line: 988, |
| end_character: 0, |
| }, |
| replacement: |
| ' this.suggestionsProvider.supportedFileExtensions.includes(useUtil.getExtension(this.comment.path))) &&\n', |
| }, |
| ], |
| }; |
| setup(async () => { |
| stubFlags('isEnabled') |
| .withArgs(KnownExperimentId.ML_SUGGESTED_EDIT_V2) |
| .returns(true); |
| }); |
| |
| test('renders suggestions in comment', async () => { |
| const comment = { |
| ...createComment(), |
| author: { |
| name: 'MitoMr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| fix_suggestions: [generatedFixSuggestion], |
| }; |
| element = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| .initiallyCollapsed=${false} |
| ></gr-comment>` |
| ); |
| element.editing = false; |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, 'gr-fix-suggestions'), |
| /* HTML */ '<gr-fix-suggestions> </gr-fix-suggestions>' |
| ); |
| }); |
| |
| test('renders suggestions in draft', async () => { |
| const comment: DraftInfo = { |
| ...createDraft(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| fix_suggestions: [generatedFixSuggestion], |
| }; |
| element = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| .initiallyCollapsed=${false} |
| ></gr-comment>` |
| ); |
| element.editing = false; |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, 'gr-fix-suggestions'), |
| /* HTML */ '<gr-fix-suggestions> </gr-fix-suggestions>' |
| ); |
| }); |
| |
| test('doesn`t render fix_suggestion when not in draft', async () => { |
| const comment: DraftInfo = { |
| ...createDraft(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| }; |
| element = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| .initiallyCollapsed=${false} |
| ></gr-comment>` |
| ); |
| element.editing = true; |
| await element.updateComplete; |
| assert.isUndefined(query(element, 'gr-suggestion-diff-preview')); |
| }); |
| |
| test('render suggestions in draft is in editing & generatedFixSuggestions is not empty', async () => { |
| const comment: DraftInfo = { |
| ...createDraft(), |
| author: { |
| name: 'Mr. Peanutbutter', |
| email: 'tenn1sballchaser@aol.com' as EmailAddress, |
| }, |
| line: 5, |
| path: 'test', |
| savingState: SavingState.OK, |
| message: 'hello world', |
| }; |
| element = await fixture( |
| html`<gr-comment |
| .account=${account} |
| .showPatchset=${true} |
| .comment=${comment} |
| .initiallyCollapsed=${false} |
| ></gr-comment>` |
| ); |
| element.editing = true; |
| sinon.stub(element, 'showGeneratedSuggestion').returns(true); |
| element.generateSuggestion = true; |
| element.generatedFixSuggestion = generatedFixSuggestion; |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, 'gr-suggestion-diff-preview'), |
| /* HTML */ '<gr-suggestion-diff-preview id="suggestionDiffPreview"> </gr-suggestion-diff-preview>' |
| ); |
| }); |
| }); |
| }); |