| /** |
| * @license |
| * Copyright 2018 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import '../../../test/common-test-setup'; |
| import './gr-thread-list'; |
| import {CommentSide, SpecialFilePath} from '../../../constants/constants'; |
| import {CommentTabState} from '../../../types/events'; |
| import { |
| compareThreads, |
| GrThreadList, |
| __testOnly_SortDropdownState, |
| } from './gr-thread-list'; |
| import {queryAll, stubFlags} from '../../../test/test-utils'; |
| import {getUserId} from '../../../utils/account-util'; |
| import { |
| createAccountDetailWithId, |
| createComment, |
| createCommentThread, |
| createDraft, |
| createParsedChange, |
| createThread, |
| } from '../../../test/test-data-generators'; |
| import { |
| AccountId, |
| EmailAddress, |
| NumericChangeId, |
| Timestamp, |
| } from '../../../api/rest-api'; |
| import { |
| RobotId, |
| UrlEncodedCommentId, |
| RevisionPatchSetNum, |
| } from '../../../types/common'; |
| import {CommentThread, isDraft} from '../../../utils/comment-util'; |
| import {query, queryAndAssert} from '../../../utils/common-util'; |
| import {GrAccountLabel} from '../../shared/gr-account-label/gr-account-label'; |
| import {GrDropdownList} from '../../shared/gr-dropdown-list/gr-dropdown-list'; |
| import {fixture, html, assert} from '@open-wc/testing'; |
| import {GrCommentThread} from '../../shared/gr-comment-thread/gr-comment-thread'; |
| |
| suite('gr-thread-list tests', () => { |
| let element: GrThreadList; |
| |
| setup(async () => { |
| element = await fixture(html`<gr-thread-list></gr-thread-list>`); |
| element.changeNum = 123 as NumericChangeId; |
| element.change = createParsedChange(); |
| element.account = createAccountDetailWithId(); |
| element.threads = [ |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000001 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2015-12-01 15:15:15.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| }, |
| { |
| id: '503008e2_0ab203ee' as UrlEncodedCommentId, |
| path: '/COMMIT_MSG', |
| line: 5, |
| in_reply_to: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| updated: '2015-12-01 15:16:15.000000000' as Timestamp, |
| message: 'draft', |
| unresolved: true, |
| __draft: true, |
| patch_set: '2' as RevisionPatchSetNum, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 5, |
| rootId: 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: 'test.txt', |
| author: { |
| _account_id: 1000002 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 3 as RevisionPatchSetNum, |
| id: '09a9fb0a_1484e6cf' as UrlEncodedCommentId, |
| updated: '2015-12-02 15:16:15.000000000' as Timestamp, |
| message: 'Some comment on another patchset.', |
| unresolved: false, |
| }, |
| ], |
| patchNum: 3 as RevisionPatchSetNum, |
| path: 'test.txt', |
| rootId: '09a9fb0a_1484e6cf' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000002 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 2 as RevisionPatchSetNum, |
| id: '8caddf38_44770ec1' as UrlEncodedCommentId, |
| updated: '2015-12-03 15:16:15.000000000' as Timestamp, |
| message: 'Another unresolved comment', |
| unresolved: false, |
| }, |
| ], |
| patchNum: 2 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| rootId: '8caddf38_44770ec1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000003 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 2 as RevisionPatchSetNum, |
| id: 'scaddf38_44770ec1' as UrlEncodedCommentId, |
| line: 4, |
| updated: '2015-12-04 15:16:15.000000000' as Timestamp, |
| message: 'Yet another unresolved comment', |
| unresolved: true, |
| }, |
| ], |
| patchNum: 2 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 4, |
| rootId: 'scaddf38_44770ec1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| id: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| path: '/COMMIT_MSG', |
| line: 6, |
| updated: '2015-12-05 15:16:15.000000000' as Timestamp, |
| message: 'resolved draft', |
| unresolved: false, |
| __draft: true, |
| patch_set: '2' as RevisionPatchSetNum, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 6, |
| rootId: 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| id: 'patchset_level_1' as UrlEncodedCommentId, |
| path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| updated: '2015-12-06 15:16:15.000000000' as Timestamp, |
| message: 'patchset comment 1', |
| unresolved: false, |
| patch_set: '2' as RevisionPatchSetNum, |
| }, |
| ], |
| patchNum: 2 as RevisionPatchSetNum, |
| path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| rootId: 'patchset_level_1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| id: 'patchset_level_2' as UrlEncodedCommentId, |
| path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| updated: '2015-12-07 15:16:15.000000000' as Timestamp, |
| message: 'patchset comment 2', |
| unresolved: false, |
| patch_set: '3' as RevisionPatchSetNum, |
| }, |
| ], |
| patchNum: 3 as RevisionPatchSetNum, |
| path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| rootId: 'patchset_level_2' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'rc1' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2015-12-08 15:16:15.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| robot_id: 'rc1' as RobotId, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 5, |
| rootId: 'rc1' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| { |
| comments: [ |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'rc2' as UrlEncodedCommentId, |
| line: 7, |
| updated: '2015-12-09 15:16:15.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| robot_id: 'rc2' as RobotId, |
| }, |
| { |
| path: '/COMMIT_MSG', |
| author: { |
| _account_id: 1000000 as AccountId, |
| name: 'user', |
| username: 'user', |
| email: 'abcd' as EmailAddress, |
| }, |
| patch_set: 4 as RevisionPatchSetNum, |
| id: 'c2_1' as UrlEncodedCommentId, |
| line: 5, |
| updated: '2015-12-10 15:16:15.000000000' as Timestamp, |
| message: 'test', |
| unresolved: true, |
| }, |
| ], |
| patchNum: 4 as RevisionPatchSetNum, |
| path: '/COMMIT_MSG', |
| line: 7, |
| rootId: 'rc2' as UrlEncodedCommentId, |
| commentSide: CommentSide.REVISION, |
| }, |
| ]; |
| await element.updateComplete; |
| }); |
| |
| suite('sort threads', () => { |
| test('sort all threads', () => { |
| element.sortDropdownValue = __testOnly_SortDropdownState.FILES; |
| assert.equal(element.getDisplayedThreads().length, 9); |
| const expected: UrlEncodedCommentId[] = [ |
| 'patchset_level_2' as UrlEncodedCommentId, // Posted on Patchset 3 |
| 'patchset_level_1' as UrlEncodedCommentId, // Posted on Patchset 2 |
| '8caddf38_44770ec1' as UrlEncodedCommentId, // File level on COMMIT_MSG |
| 'scaddf38_44770ec1' as UrlEncodedCommentId, // Line 4 on COMMIT_MSG |
| 'rc1' as UrlEncodedCommentId, // Line 5 on COMMIT_MESSAGE newer |
| 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, // Line 5 on COMMIT_MESSAGE older |
| 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, // Line 6 on COMMIT_MSG |
| 'rc2' as UrlEncodedCommentId, // Line 7 on COMMIT_MSG |
| '09a9fb0a_1484e6cf' as UrlEncodedCommentId, // File level on test.txt |
| ]; |
| const actual = element.getDisplayedThreads().map(t => t.rootId); |
| assert.sameOrderedMembers(actual, expected); |
| }); |
| |
| test('respects special cases for ordering', async () => { |
| element.sortDropdownValue = __testOnly_SortDropdownState.FILES; |
| element.threads = [ |
| { |
| ...createThread(createComment({path: '/app/test.cc'})), |
| path: '/app/test.cc', |
| }, |
| { |
| ...createThread(createComment({path: '/app/test.h'})), |
| path: '/app/test.h', |
| }, |
| { |
| ...createThread( |
| createComment({path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS}) |
| ), |
| path: SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| }, |
| ]; |
| await element.updateComplete; |
| |
| const paths = Array.from( |
| queryAll<GrCommentThread>(element, 'gr-comment-thread') |
| ).map(threadElement => threadElement.thread?.path); |
| |
| // Patchset comment is always first, then we have a special case where .h |
| // files should appear above other files of the same name regardless of |
| // their alphabetical ordering. |
| assert.sameOrderedMembers(paths, [ |
| SpecialFilePath.PATCHSET_LEVEL_COMMENTS, |
| '/app/test.h', |
| '/app/test.cc', |
| ]); |
| }); |
| |
| test('sort all threads by timestamp', () => { |
| element.sortDropdownValue = __testOnly_SortDropdownState.TIMESTAMP; |
| assert.equal(element.getDisplayedThreads().length, 9); |
| const expected: UrlEncodedCommentId[] = [ |
| 'rc2' as UrlEncodedCommentId, |
| 'rc1' as UrlEncodedCommentId, |
| 'patchset_level_2' as UrlEncodedCommentId, |
| 'patchset_level_1' as UrlEncodedCommentId, |
| 'zcf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| 'scaddf38_44770ec1' as UrlEncodedCommentId, |
| '8caddf38_44770ec1' as UrlEncodedCommentId, |
| '09a9fb0a_1484e6cf' as UrlEncodedCommentId, |
| 'ecf0b9fa_fe1a5f62' as UrlEncodedCommentId, |
| ]; |
| const actual = element.getDisplayedThreads().map(t => t.rootId); |
| assert.sameOrderedMembers(actual, expected); |
| }); |
| }); |
| |
| test('renders', async () => { |
| await element.updateComplete; |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <div class="header"> |
| <span class="sort-text">Sort By:</span> |
| <gr-dropdown-list id="sortDropdown"></gr-dropdown-list> |
| <span class="separator"></span> |
| <span class="filter-text">Filter By:</span> |
| <gr-dropdown-list id="filterDropdown"></gr-dropdown-list> |
| <span class="author-text">From:</span> |
| <gr-account-label |
| deselected="" |
| selectionchipstyle="" |
| nostatusicons="" |
| ></gr-account-label> |
| <gr-account-label |
| deselected="" |
| selectionchipstyle="" |
| nostatusicons="" |
| ></gr-account-label> |
| <gr-account-label |
| deselected="" |
| selectionchipstyle="" |
| nostatusicons="" |
| ></gr-account-label> |
| <gr-account-label |
| deselected="" |
| selectionchipstyle="" |
| nostatusicons="" |
| ></gr-account-label> |
| <gr-account-label |
| deselected="" |
| selectionchipstyle="" |
| nostatusicons="" |
| ></gr-account-label> |
| </div> |
| <div id="threads" part="threads"> |
| <gr-comment-thread |
| show-file-name="" |
| show-file-path="" |
| ></gr-comment-thread> |
| <gr-comment-thread show-file-path=""></gr-comment-thread> |
| <div class="thread-separator"></div> |
| <gr-comment-thread |
| show-file-name="" |
| show-file-path="" |
| ></gr-comment-thread> |
| <gr-comment-thread show-file-path=""></gr-comment-thread> |
| <div class="thread-separator"></div> |
| <gr-comment-thread |
| has-draft="" |
| show-file-name="" |
| show-file-path="" |
| ></gr-comment-thread> |
| <gr-comment-thread show-file-path=""></gr-comment-thread> |
| <gr-comment-thread show-file-path=""></gr-comment-thread> |
| <div class="thread-separator"></div> |
| <gr-comment-thread |
| show-file-name="" |
| show-file-path="" |
| ></gr-comment-thread> |
| <div class="thread-separator"></div> |
| <gr-comment-thread |
| has-draft="" |
| show-file-name="" |
| show-file-path="" |
| ></gr-comment-thread> |
| </div> |
| ` |
| ); |
| }); |
| |
| test('renders empty', async () => { |
| element.threads = []; |
| await element.updateComplete; |
| assert.dom.equal( |
| queryAndAssert(element, 'div#threads'), |
| /* HTML */ ` |
| <div id="threads" part="threads"> |
| <div><span>No comments</span></div> |
| </div> |
| ` |
| ); |
| }); |
| |
| test('tapping single author chips', async () => { |
| element.account = createAccountDetailWithId(1); |
| await element.updateComplete; |
| const chips = Array.from( |
| queryAll<GrAccountLabel>(element, 'gr-account-label') |
| ); |
| const authors = chips.map(chip => getUserId(chip.account!)).sort(); |
| assert.deepEqual(authors, [ |
| 1 as AccountId, |
| 1000000 as AccountId, |
| 1000001 as AccountId, |
| 1000002 as AccountId, |
| 1000003 as AccountId, |
| ]); |
| assert.equal(element.threads.length, 9); |
| assert.equal(element.getDisplayedThreads().length, 9); |
| |
| const chip = chips.find(chip => chip.account!._account_id === 1000001); |
| chip!.click(); |
| await element.updateComplete; |
| |
| assert.equal(element.threads.length, 9); |
| assert.equal(element.getDisplayedThreads().length, 1); |
| assert.equal( |
| element.getDisplayedThreads()[0].comments[0].author?._account_id, |
| 1000001 as AccountId |
| ); |
| |
| chip!.click(); |
| await element.updateComplete; |
| assert.equal(element.threads.length, 9); |
| assert.equal(element.getDisplayedThreads().length, 9); |
| }); |
| |
| test('tapping single author with only drafts', async () => { |
| element.account = createAccountDetailWithId(1); |
| element.threads = [createThread(createDraft())]; |
| await element.updateComplete; |
| const chips = Array.from( |
| queryAll<GrAccountLabel>(element, 'gr-account-label') |
| ); |
| const authors = chips.map(chip => getUserId(chip.account!)).sort(); |
| assert.deepEqual(authors, [1 as AccountId]); |
| assert.equal(element.threads.length, 1); |
| assert.equal(element.getDisplayedThreads().length, 1); |
| |
| const chip = chips.find(chip => chip.account!._account_id === 1); |
| chip!.click(); |
| await element.updateComplete; |
| |
| assert.equal(element.threads.length, 1); |
| assert.equal(element.getDisplayedThreads().length, 1); |
| assert.isTrue(isDraft(element.getDisplayedThreads()[0].comments[0])); |
| |
| chip!.click(); |
| await element.updateComplete; |
| assert.equal(element.threads.length, 1); |
| assert.equal(element.getDisplayedThreads().length, 1); |
| }); |
| |
| test('tapping multiple author chips', async () => { |
| element.account = createAccountDetailWithId(1); |
| await element.updateComplete; |
| const chips = Array.from( |
| queryAll<GrAccountLabel>(element, 'gr-account-label') |
| ); |
| |
| chips.find(chip => chip.account?._account_id === 1000001)!.click(); |
| chips.find(chip => chip.account?._account_id === 1000002)!.click(); |
| await element.updateComplete; |
| |
| assert.equal(element.threads.length, 9); |
| assert.equal(element.getDisplayedThreads().length, 3); |
| assert.equal( |
| element.getDisplayedThreads()[0].comments[0].author?._account_id, |
| 1000002 as AccountId |
| ); |
| assert.equal( |
| element.getDisplayedThreads()[1].comments[0].author?._account_id, |
| 1000002 as AccountId |
| ); |
| assert.equal( |
| element.getDisplayedThreads()[2].comments[0].author?._account_id, |
| 1000001 as AccountId |
| ); |
| }); |
| |
| test('show all comments', async () => { |
| const filterDropdown = queryAndAssert<GrDropdownList>( |
| element, |
| '#filterDropdown' |
| ); |
| filterDropdown.value = CommentTabState.SHOW_ALL; |
| await filterDropdown.updateComplete; |
| await element.updateComplete; |
| assert.equal(element.getDisplayedThreads().length, 9); |
| }); |
| |
| test('unresolved shows all unresolved comments', async () => { |
| const filterDropdown = queryAndAssert<GrDropdownList>( |
| element, |
| '#filterDropdown' |
| ); |
| filterDropdown.value = CommentTabState.UNRESOLVED; |
| await filterDropdown.updateComplete; |
| await element.updateComplete; |
| assert.equal(element.getDisplayedThreads().length, 4); |
| }); |
| |
| test('toggle drafts only shows threads with draft comments', async () => { |
| const filterDropdown = queryAndAssert<GrDropdownList>( |
| element, |
| '#filterDropdown' |
| ); |
| filterDropdown.value = CommentTabState.DRAFTS; |
| await filterDropdown.updateComplete; |
| await element.updateComplete; |
| assert.equal(element.getDisplayedThreads().length, 2); |
| }); |
| |
| suite('mention threads', () => { |
| let mentionedThreads: CommentThread[]; |
| setup(async () => { |
| stubFlags('isEnabled').returns(true); |
| mentionedThreads = [ |
| createCommentThread([ |
| { |
| ...createComment(), |
| message: 'random text with no emails', |
| }, |
| ]), |
| // Resolved thread does not contribute to the count |
| createCommentThread([ |
| { |
| ...createComment(), |
| message: '@abcd@def.com please take a look', |
| }, |
| { |
| ...createComment(), |
| message: '@abcd@def.com please take a look again at this', |
| }, |
| ]), |
| createCommentThread([ |
| { |
| ...createComment(), |
| message: '@abcd@def.com this is important', |
| unresolved: true, |
| }, |
| ]), |
| ]; |
| element.account!.email = 'abcd@def.com' as EmailAddress; |
| element.threads.push(...mentionedThreads); |
| element.requestUpdate(); |
| await element.updateComplete; |
| }); |
| |
| test('mentions filter', async () => { |
| const filterDropdown = queryAndAssert<GrDropdownList>( |
| element, |
| '#filterDropdown' |
| ); |
| filterDropdown.value = CommentTabState.MENTIONS; |
| await filterDropdown.updateComplete; |
| await element.updateComplete; |
| assert.deepEqual(element.getDisplayedThreads(), [mentionedThreads[2]]); |
| }); |
| }); |
| |
| suite('hideDropdown', () => { |
| test('header hidden for hideDropdown=true', async () => { |
| element.hideDropdown = true; |
| await element.updateComplete; |
| assert.isUndefined(query(element, '.header')); |
| }); |
| |
| test('header shown for hideDropdown=false', async () => { |
| element.hideDropdown = false; |
| await element.updateComplete; |
| assert.isDefined(query(element, '.header')); |
| }); |
| }); |
| |
| suite('empty thread', () => { |
| setup(async () => { |
| element.threads = []; |
| await element.updateComplete; |
| }); |
| |
| test('default empty message should show', () => { |
| const threadsEl = queryAndAssert(element, '#threads'); |
| assert.isTrue(threadsEl.textContent?.trim().includes('No comments')); |
| }); |
| }); |
| }); |
| |
| suite('compareThreads', () => { |
| let t1: CommentThread; |
| let t2: CommentThread; |
| |
| const sortPredicate = (thread1: CommentThread, thread2: CommentThread) => |
| compareThreads(thread1, thread2); |
| |
| const checkOrder = (expected: CommentThread[]) => { |
| assert.sameOrderedMembers([t1, t2].sort(sortPredicate), expected); |
| assert.sameOrderedMembers([t2, t1].sort(sortPredicate), expected); |
| }; |
| |
| setup(() => { |
| t1 = createThread({}); |
| t2 = createThread({}); |
| }); |
| |
| test('patchset-level before file comments', () => { |
| t1.path = SpecialFilePath.PATCHSET_LEVEL_COMMENTS; |
| t2.path = SpecialFilePath.COMMIT_MESSAGE; |
| checkOrder([t1, t2]); |
| }); |
| |
| test('paths lexicographically', () => { |
| t1.path = 'a.txt'; |
| t2.path = 'b.txt'; |
| checkOrder([t1, t2]); |
| }); |
| |
| test('patchsets in reverse order', () => { |
| t1.patchNum = 2 as RevisionPatchSetNum; |
| t2.patchNum = 3 as RevisionPatchSetNum; |
| checkOrder([t2, t1]); |
| }); |
| |
| test('file level comment before line', () => { |
| t1.line = 123; |
| t2.line = 'FILE'; |
| checkOrder([t2, t1]); |
| }); |
| |
| test('comments sorted by line', () => { |
| t1.line = 123; |
| t2.line = 321; |
| checkOrder([t1, t2]); |
| }); |
| }); |