diff --git a/webapp/src/components/cardDetail/cardDetail.test.tsx b/webapp/src/components/cardDetail/cardDetail.test.tsx new file mode 100644 index 000000000..2dfd2042f --- /dev/null +++ b/webapp/src/components/cardDetail/cardDetail.test.tsx @@ -0,0 +1,145 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import React from 'react' + +import 'isomorphic-fetch' +import {act, render} from '@testing-library/react' + +import configureStore from 'redux-mock-store' +import {Provider as ReduxProvider} from 'react-redux' + +import {FetchMock} from '../../test/fetchMock' +import {TestBlockFactory} from '../../test/testBlockFactory' + +import {mockDOM, wrapIntl} from '../../testUtils' + +import CardDetail from './cardDetail' + +global.fetch = FetchMock.fn + +beforeEach(() => { + FetchMock.fn.mockReset() +}) + +// This is needed to run EasyMDE in tests. +// It needs bounding rectangle box property +// on HTML elements, but Jest's HTML engine jsdom +// doesn't provide it. +// So we mock it. +beforeAll(() => { + mockDOM() +}) + +describe('components/cardDetail/CardDetail', () => { + const board = TestBlockFactory.createBoard() + + const view = TestBlockFactory.createBoardView(board) + view.fields.sortOptions = [] + view.fields.groupById = undefined + view.fields.hiddenOptionIds = [] + + const card = TestBlockFactory.createCard(board) + + const createdAt = Date.parse('01 Jan 2021 00:00:00 GMT') + const comment1 = TestBlockFactory.createComment(card) + comment1.type = 'comment' + comment1.title = 'Comment 1' + comment1.parentId = card.id + comment1.createAt = createdAt + + const comment2 = TestBlockFactory.createComment(card) + comment2.type = 'comment' + comment2.title = 'Comment 2' + comment2.parentId = card.id + comment2.createAt = createdAt + + test('should show comments', async () => { + const mockStore = configureStore([]) + const store = mockStore({ + users: { + workspaceUsers: [ + {username: 'username_1'}, + ], + }, + }) + + const component = ( + + {wrapIntl( + , + )} + + ) + + let container: Element | DocumentFragment | null = null + + await act(async () => { + const result = render(component) + container = result.container + }) + + expect(container).toBeDefined() + + // Comments show up + const comments = container!.querySelectorAll('.comment-text') + expect(comments.length).toBe(2) + + // Add comment option visible when readonly mode is off + const newCommentSection = container!.querySelectorAll('.newcomment') + expect(newCommentSection.length).toBe(1) + }) + + test('should show comments in readonly view', async () => { + const mockStore = configureStore([]) + const store = mockStore({ + users: { + workspaceUsers: [ + {username: 'username_1'}, + ], + }, + }) + + const component = ( + + {wrapIntl( + , + )} + + ) + + let container: Element | DocumentFragment | null = null + + await act(async () => { + const result = render(component) + container = result.container + }) + + expect(container).toBeDefined() + + // comments show up + const comments = container!.querySelectorAll('.comment-text') + expect(comments.length).toBe(2) + + // Add comment option is not shown in readonly mode + const newCommentSection = container!.querySelectorAll('.newcomment') + expect(newCommentSection.length).toBe(0) + }) +}) diff --git a/webapp/src/components/cardDetail/cardDetail.tsx b/webapp/src/components/cardDetail/cardDetail.tsx index bd4729246..89954ccf1 100644 --- a/webapp/src/components/cardDetail/cardDetail.tsx +++ b/webapp/src/components/cardDetail/cardDetail.tsx @@ -124,17 +124,13 @@ const CardDetail = (props: Props): JSX.Element|null => { {/* Comments */} - {!props.readonly && - <> -
- -
- - } +
+ {/* Content blocks */} diff --git a/webapp/src/components/cardDetail/comment.tsx b/webapp/src/components/cardDetail/comment.tsx index 6db38809a..7389020a5 100644 --- a/webapp/src/components/cardDetail/comment.tsx +++ b/webapp/src/components/cardDetail/comment.tsx @@ -20,6 +20,7 @@ type Props = { comment: Block userId: string userImageUrl: string + readonly: boolean } const Comment: FC = (props: Props) => { @@ -42,17 +43,20 @@ const Comment: FC = (props: Props) => {
{Utils.displayDateTime(new Date(comment.createAt), intl)}
- - }/> - - } - id='delete' - name={intl.formatMessage({id: 'Comment.delete', defaultMessage: 'Delete'})} - onClick={() => mutator.deleteBlock(comment)} - /> - - + + {!props.readonly && ( + + }/> + + } + id='delete' + name={intl.formatMessage({id: 'Comment.delete', defaultMessage: 'Delete'})} + onClick={() => mutator.deleteBlock(comment)} + /> + + + )}
{ + FetchMock.fn.mockReset() +}) + +beforeAll(() => { + mockDOM() +}) + +describe('components/cardDetail/CommentsList', () => { + const createdAt = Date.parse('01 Jan 2021 00:00:00 GMT') + const comment1: CommentBlock = { + id: 'comment_id_1', + title: 'Comment 1', + createAt: createdAt, + modifiedBy: 'user_id_1', + } as CommentBlock + + const comment2: CommentBlock = { + id: 'comment_id_2', + title: 'Comment 2', + createAt: createdAt, + modifiedBy: 'user_id_2', + } as CommentBlock + + test('comments show up', async () => { + const mockStore = configureStore([]) + const store = mockStore({ + users: { + workspaceUsers: [ + {username: 'username_1'}, + ], + }, + }) + + const component = ( + + {wrapIntl( + , + )} + ) + + let container: Element | DocumentFragment | null = null + + await act(async () => { + const result = render(component) + container = result.container + }) + + expect(container).toBeDefined() + + // Comments show up + const comments = container!.querySelectorAll('.comment-text') + expect(comments.length).toBe(2) + + // Add comment option visible when readonly mode is off + const newCommentSection = container!.querySelectorAll('.newcomment') + expect(newCommentSection.length).toBe(1) + }) + + test('comments show up in readonly mode', async () => { + const mockStore = configureStore([]) + const store = mockStore({ + users: { + workspaceUsers: [ + {username: 'username_1'}, + ], + }, + }) + + const component = ( + + {wrapIntl( + , + )} + ) + + let container: Element | DocumentFragment | null = null + + await act(async () => { + const result = render(component) + container = result.container + }) + + expect(container).toBeDefined() + + // Comments show up + const comments = container!.querySelectorAll('.comment-text') + expect(comments.length).toBe(2) + + // Add comment option visible when readonly mode is off + const newCommentSection = container!.querySelectorAll('.newcomment') + expect(newCommentSection.length).toBe(0) + }) +}) diff --git a/webapp/src/components/cardDetail/commentsList.tsx b/webapp/src/components/cardDetail/commentsList.tsx index f58311e66..f53324294 100644 --- a/webapp/src/components/cardDetail/commentsList.tsx +++ b/webapp/src/components/cardDetail/commentsList.tsx @@ -17,6 +17,7 @@ type Props = { comments: readonly CommentBlock[] rootId: string cardId: string + readonly: boolean } const CommentsList = React.memo((props: Props) => { @@ -44,6 +45,38 @@ const CommentsList = React.memo((props: Props) => { // TODO: Replace this placeholder const userImageUrl = 'data:image/svg+xml,' + const newCommentComponent = ( +
+ + { + if (newComment !== value) { + setNewComment(value) + } + }} + onAccept={onSendClicked} + /> + + {newComment && + + } +
+ ) + return (
{comments.map((comment) => ( @@ -52,40 +85,15 @@ const CommentsList = React.memo((props: Props) => { comment={comment} userImageUrl={userImageUrl} userId={comment.modifiedBy} + readonly={props.readonly} /> ))} {/* New comment */} + {!props.readonly && newCommentComponent} -
- - { - if (newComment !== value) { - setNewComment(value) - } - }} - onAccept={onSendClicked} - /> - - {newComment && - - } -
+ {/* horizontal divider below comments */} + {!(comments.length === 0 && props.readonly) &&
}
) }) diff --git a/webapp/src/testUtils.tsx b/webapp/src/testUtils.tsx new file mode 100644 index 000000000..3a6a9abf3 --- /dev/null +++ b/webapp/src/testUtils.tsx @@ -0,0 +1,25 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. +import {IntlProvider} from 'react-intl' +import React from 'react' + +export const wrapIntl = (children: any) => {children} + +export function mockDOM(): void { + window.focus = jest.fn() + document.createRange = () => { + const range = new Range() + + range.getBoundingClientRect = jest.fn() + + range.getClientRects = () => { + return { + item: () => null, + length: 0, + [Symbol.iterator]: jest.fn(), + } + } + + return range + } +}