| /** |
| * @license |
| * Copyright 2015 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup'; |
| import './gr-comment-thread'; |
| import {sortComments} from '../../../utils/comment-util'; |
| import {GrCommentThread} from './gr-comment-thread'; |
| import { |
| NumericChangeId, |
| UrlEncodedCommentId, |
| Timestamp, |
| CommentInfo, |
| RepoName, |
| DraftInfo, |
| } from '../../../types/common'; |
| import { |
| mockPromise, |
| queryAndAssert, |
| stubRestApi, |
| waitUntilCalled, |
| MockPromise, |
| } from '../../../test/test-utils'; |
| import { |
| createAccountDetailWithId, |
| createThread, |
| } from '../../../test/test-data-generators'; |
| import {SinonStubbedMember} from 'sinon'; |
| import {fixture, html, waitUntil, assert} from '@open-wc/testing'; |
| import {GrButton} from '../gr-button/gr-button'; |
| import {SpecialFilePath} from '../../../constants/constants'; |
| import {GrIcon} from '../gr-icon/gr-icon'; |
| import { |
| CommentsModel, |
| commentsModelToken, |
| } from '../../../models/comments/comments-model'; |
| import {testResolver} from '../../../test/common-test-setup'; |
| |
| const c1 = { |
| author: {name: 'Kermit'}, |
| id: 'the-root' as UrlEncodedCommentId, |
| message: 'start the conversation', |
| updated: '2021-11-01 10:11:12.000000000' as Timestamp, |
| }; |
| |
| const c2 = { |
| author: {name: 'Ms Piggy'}, |
| id: 'the-reply' as UrlEncodedCommentId, |
| message: 'keep it going', |
| updated: '2021-11-02 10:11:12.000000000' as Timestamp, |
| in_reply_to: 'the-root' as UrlEncodedCommentId, |
| }; |
| |
| const c3 = { |
| author: {name: 'Kermit'}, |
| id: 'the-draft' as UrlEncodedCommentId, |
| message: 'stop it', |
| updated: '2021-11-03 10:11:12.000000000' as Timestamp, |
| in_reply_to: 'the-reply' as UrlEncodedCommentId, |
| __draft: true, |
| }; |
| |
| const commentWithContext = { |
| author: {name: 'Kermit'}, |
| id: 'the-draft' as UrlEncodedCommentId, |
| message: 'just for context', |
| updated: '2021-11-03 10:11:12.000000000' as Timestamp, |
| line: 5, |
| context_lines: [ |
| {line_number: 4, context_line: 'content of line 4'}, |
| {line_number: 5, context_line: 'content of line 5'}, |
| {line_number: 6, context_line: 'content of line 6'}, |
| ], |
| }; |
| |
| suite('gr-comment-thread tests', () => { |
| let element: GrCommentThread; |
| |
| setup(async () => { |
| stubRestApi('getLoggedIn').returns(Promise.resolve(false)); |
| element = await fixture(html`<gr-comment-thread></gr-comment-thread>`); |
| element.changeNum = 1 as NumericChangeId; |
| element.showFileName = true; |
| element.showFilePath = true; |
| element.repoName = 'test-repo-name' as RepoName; |
| await element.updateComplete; |
| element.account = {...createAccountDetailWithId(13), name: 'Yoda'}; |
| }); |
| |
| test('renders with draft', async () => { |
| element.thread = createThread(c1, c2, c3); |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <div class="fileName"> |
| <a href="/c/test-repo-name/+/1/1/test-path-comment-thread"> |
| test-path-comment-thread |
| </a> |
| <gr-copy-clipboard hideinput=""></gr-copy-clipboard> |
| </div> |
| <div class="pathInfo"> |
| <a href="/c/test-repo-name/+/1/comment/the-root/"> #314 </a> |
| </div> |
| <div id="container"> |
| <h3 class="assistive-tech-only">Draft Comment thread by Kermit</h3> |
| <div class="comment-box" tabindex="0"> |
| <gr-comment |
| collapsed="" |
| initially-collapsed="" |
| robot-button-disabled="" |
| show-patchset="" |
| ></gr-comment> |
| <gr-comment |
| collapsed="" |
| initially-collapsed="" |
| robot-button-disabled="" |
| show-patchset="" |
| ></gr-comment> |
| <gr-comment robot-button-disabled="" show-patchset=""></gr-comment> |
| </div> |
| </div> |
| ` |
| ); |
| }); |
| |
| test('renders unsaved', async () => { |
| element.thread = createThread(); |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <div class="fileName"> |
| <span>test-path-comment-thread</span> |
| <gr-copy-clipboard hideinput=""></gr-copy-clipboard> |
| </div> |
| <div class="pathInfo"> |
| <span>#314</span> |
| </div> |
| <div id="container"> |
| <h3 class="assistive-tech-only"> |
| Unresolved Draft Comment thread by Yoda |
| </h3> |
| <div class="comment-box unresolved" tabindex="0"> |
| <gr-comment robot-button-disabled="" show-patchset=""></gr-comment> |
| </div> |
| </div> |
| ` |
| ); |
| }); |
| |
| test('renders with actions resolved', async () => { |
| element.thread = createThread(c1, c2); |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, '#container'), |
| /* HTML */ ` |
| <div id="container"> |
| <h3 class="assistive-tech-only">Comment thread by Kermit</h3> |
| <div class="comment-box" tabindex="0"> |
| <gr-comment |
| collapsed="" |
| initially-collapsed="" |
| show-patchset="" |
| ></gr-comment> |
| <gr-comment |
| collapsed="" |
| initially-collapsed="" |
| show-patchset="" |
| ></gr-comment> |
| <div id="actionsContainer"> |
| <span id="unresolvedLabel"> Resolved </span> |
| <div id="actions"> |
| <gr-button |
| aria-disabled="false" |
| class="action reply" |
| id="replyBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Reply |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action quote" |
| id="quoteBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Quote |
| </gr-button> |
| <gr-icon |
| icon="link" |
| class="copy link-icon" |
| role="button" |
| tabindex="0" |
| title="Copy link to this comment" |
| ></gr-icon> |
| </div> |
| </div> |
| </div> |
| </div> |
| ` |
| ); |
| }); |
| |
| test('renders with actions unresolved', async () => { |
| element.thread = createThread(c1, {...c2, unresolved: true}); |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, '#container'), |
| /* HTML */ ` |
| <div id="container"> |
| <h3 class="assistive-tech-only"> |
| Unresolved Comment thread by Kermit |
| </h3> |
| <div class="comment-box unresolved" tabindex="0"> |
| <gr-comment show-patchset=""></gr-comment> |
| <gr-comment show-patchset=""></gr-comment> |
| <div id="actionsContainer"> |
| <span id="unresolvedLabel"> Unresolved </span> |
| <div id="actions"> |
| <gr-button |
| aria-disabled="false" |
| class="action reply" |
| id="replyBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Reply |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action quote" |
| id="quoteBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Quote |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action ack" |
| id="ackBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Ack |
| </gr-button> |
| <gr-button |
| aria-disabled="false" |
| class="action done" |
| id="doneBtn" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| Done |
| </gr-button> |
| <gr-icon |
| icon="link" |
| class="copy link-icon" |
| role="button" |
| tabindex="0" |
| title="Copy link to this comment" |
| ></gr-icon> |
| </div> |
| </div> |
| </div> |
| </div> |
| ` |
| ); |
| }); |
| |
| test('renders with diff', async () => { |
| element.showCommentContext = true; |
| element.thread = createThread(commentWithContext); |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, '.diff-container'), |
| /* HTML */ ` |
| <div class="diff-container"> |
| <gr-diff |
| class="disable-context-control-buttons hide-line-length-indicator no-left" |
| id="diff" |
| style="--line-limit-marker:100ch; --content-width:none; --diff-max-width:none; --font-size:12px;" |
| > |
| </gr-diff> |
| <div class="view-diff-container"> |
| <a href="/c/test-repo-name/+/1/comment/the-draft/"> |
| <gr-button |
| aria-disabled="false" |
| class="view-diff-button" |
| link="" |
| role="button" |
| tabindex="0" |
| > |
| View Diff |
| </gr-button> |
| </a> |
| </div> |
| </div> |
| `, |
| {ignoreAttributes: ['style']} |
| ); |
| }); |
| |
| suite('action button clicks', () => { |
| let savePromise: MockPromise<DraftInfo>; |
| let stub: SinonStubbedMember<CommentsModel['saveDraft']>; |
| |
| setup(async () => { |
| savePromise = mockPromise<DraftInfo>(); |
| stub = sinon |
| .stub(testResolver(commentsModelToken), 'saveDraft') |
| .returns(savePromise); |
| |
| element.thread = createThread(c1, {...c2, unresolved: true}); |
| await element.updateComplete; |
| }); |
| |
| test('handle Acknowledge', async () => { |
| queryAndAssert<GrButton>(element, '#ackBtn').click(); |
| waitUntilCalled(stub, 'saveDraft()'); |
| assert.equal(stub.lastCall.firstArg.message, 'Acknowledged'); |
| assert.equal(stub.lastCall.firstArg.unresolved, false); |
| assert.isTrue(element.saving); |
| |
| savePromise.resolve(); |
| await element.updateComplete; |
| assert.isFalse(element.saving); |
| }); |
| |
| test('handle Done', async () => { |
| queryAndAssert<GrButton>(element, '#doneBtn').click(); |
| waitUntilCalled(stub, 'saveDraft()'); |
| assert.equal(stub.lastCall.firstArg.message, 'Done'); |
| assert.equal(stub.lastCall.firstArg.unresolved, false); |
| }); |
| |
| test('handle Reply', async () => { |
| assert.isUndefined(element.unsavedComment); |
| queryAndAssert<GrButton>(element, '#replyBtn').click(); |
| assert.equal(element.unsavedComment?.message, ''); |
| }); |
| |
| test('handle Quote', async () => { |
| assert.isUndefined(element.unsavedComment); |
| queryAndAssert<GrButton>(element, '#quoteBtn').click(); |
| assert.equal(element.unsavedComment?.message?.trim(), `> ${c2.message}`); |
| }); |
| }); |
| |
| suite('self removal when empty thread changed to editing:false', () => { |
| let threadEl: GrCommentThread; |
| |
| setup(async () => { |
| threadEl = await fixture(html`<gr-comment-thread></gr-comment-thread>`); |
| threadEl.thread = createThread(); |
| }); |
| |
| test('new thread el normally has a parent and an unsaved comment', async () => { |
| await waitUntil(() => threadEl.editing); |
| assert.isOk(threadEl.unsavedComment); |
| assert.isOk(threadEl.parentElement); |
| }); |
| |
| test('thread el removed after clicking CANCEL', async () => { |
| await waitUntil(() => threadEl.editing); |
| |
| const commentEl = queryAndAssert(threadEl, 'gr-comment'); |
| const buttonEl = queryAndAssert<GrButton>(commentEl, 'gr-button.cancel'); |
| buttonEl.click(); |
| |
| await waitUntil(() => !threadEl.editing); |
| assert.isNotOk(threadEl.parentElement); |
| }); |
| }); |
| |
| test('comments are sorted correctly', () => { |
| const comments: CommentInfo[] = [ |
| { |
| id: 'jacks_confession' as UrlEncodedCommentId, |
| in_reply_to: 'sallys_confession' as UrlEncodedCommentId, |
| message: 'i like you, too', |
| updated: '2015-12-25 15:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sallys_confession' as UrlEncodedCommentId, |
| message: 'i like you, jack', |
| updated: '2015-12-24 15:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sally_to_dr_finklestein' as UrlEncodedCommentId, |
| message: 'i’m running away', |
| updated: '2015-10-31 09:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sallys_defiance' as UrlEncodedCommentId, |
| in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId, |
| message: 'i will poison you so i can get away', |
| updated: '2015-10-31 15:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'dr_finklesteins_response' as UrlEncodedCommentId, |
| in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId, |
| message: 'no i will pull a thread and your arm will fall off', |
| updated: '2015-10-31 11:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sallys_mission' as UrlEncodedCommentId, |
| message: 'i have to find santa', |
| updated: '2015-12-24 15:00:20.396000000' as Timestamp, |
| }, |
| ]; |
| const results = sortComments(comments); |
| assert.deepEqual(results, [ |
| { |
| id: 'sally_to_dr_finklestein' as UrlEncodedCommentId, |
| message: 'i’m running away', |
| updated: '2015-10-31 09:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'dr_finklesteins_response' as UrlEncodedCommentId, |
| in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId, |
| message: 'no i will pull a thread and your arm will fall off', |
| updated: '2015-10-31 11:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sallys_defiance' as UrlEncodedCommentId, |
| in_reply_to: 'sally_to_dr_finklestein' as UrlEncodedCommentId, |
| message: 'i will poison you so i can get away', |
| updated: '2015-10-31 15:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sallys_confession' as UrlEncodedCommentId, |
| message: 'i like you, jack', |
| updated: '2015-12-24 15:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'sallys_mission' as UrlEncodedCommentId, |
| message: 'i have to find santa', |
| updated: '2015-12-24 15:00:20.396000000' as Timestamp, |
| }, |
| { |
| id: 'jacks_confession' as UrlEncodedCommentId, |
| in_reply_to: 'sallys_confession' as UrlEncodedCommentId, |
| message: 'i like you, too', |
| updated: '2015-12-25 15:00:20.396000000' as Timestamp, |
| }, |
| ]); |
| }); |
| |
| test('patchset comments link to /comments URL', async () => { |
| const clipboardStub = sinon.stub(navigator.clipboard, 'writeText'); |
| element.thread = { |
| ...createThread(c1), |
| path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| }; |
| await element.updateComplete; |
| |
| queryAndAssert<GrIcon>(element, 'gr-icon.copy').click(); |
| |
| assert.equal(1, clipboardStub.callCount); |
| assert.equal( |
| clipboardStub.firstCall.args[0], |
| 'http://localhost:9876/c/test-repo-name/+/1/comments/the-root' |
| ); |
| }); |
| |
| test('file comments link to /comment URL', async () => { |
| const clipboardStub = sinon.stub(navigator.clipboard, 'writeText'); |
| element.thread = createThread(c1); |
| await element.updateComplete; |
| |
| queryAndAssert<GrIcon>(element, 'gr-icon.copy').click(); |
| |
| assert.equal(1, clipboardStub.callCount); |
| assert.equal( |
| clipboardStub.firstCall.args[0], |
| 'http://localhost:9876/c/test-repo-name/+/1/comment/the-root/' |
| ); |
| }); |
| }); |