Fixed comments not displaying in readonly view (#820)

* Fixed comments not displaying in readonly view

* FIxed some tests

* Updated tests

Co-authored-by: Doug Lauder <wiggin77@warpmail.net>
Co-authored-by: Scott Bishel <scott.bishel@mattermost.com>
This commit is contained in:
Harshil Sharma 2021-08-05 18:52:15 +05:30 committed by GitHub
parent beebf70cbc
commit 578b91fd2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 355 additions and 51 deletions

View File

@ -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 = (
<ReduxProvider store={store}>
{wrapIntl(
<CardDetail
board={board}
activeView={view}
views={[view]}
cards={[card]}
card={card}
comments={[comment1, comment2]}
contents={[]}
readonly={false}
/>,
)}
</ReduxProvider>
)
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 = (
<ReduxProvider store={store}>
{wrapIntl(
<CardDetail
board={board}
activeView={view}
views={[view]}
cards={[card]}
card={card}
comments={[comment1, comment2]}
contents={[]}
readonly={true}
/>,
)}
</ReduxProvider>
)
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)
})
})

View File

@ -124,17 +124,13 @@ const CardDetail = (props: Props): JSX.Element|null => {
{/* Comments */}
{!props.readonly &&
<>
<hr/>
<CommentsList
comments={comments}
rootId={card.rootId}
cardId={card.id}
/>
<hr/>
</>
}
<hr/>
<CommentsList
comments={comments}
rootId={card.rootId}
cardId={card.id}
readonly={props.readonly}
/>
</div>
{/* Content blocks */}

View File

@ -20,6 +20,7 @@ type Props = {
comment: Block
userId: string
userImageUrl: string
readonly: boolean
}
const Comment: FC<Props> = (props: Props) => {
@ -42,17 +43,20 @@ const Comment: FC<Props> = (props: Props) => {
<div className='comment-date'>
{Utils.displayDateTime(new Date(comment.createAt), intl)}
</div>
<MenuWrapper>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'Comment.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deleteBlock(comment)}
/>
</Menu>
</MenuWrapper>
{!props.readonly && (
<MenuWrapper>
<IconButton icon={<OptionsIcon/>}/>
<Menu position='left'>
<Menu.Text
icon={<DeleteIcon/>}
id='delete'
name={intl.formatMessage({id: 'Comment.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deleteBlock(comment)}
/>
</Menu>
</MenuWrapper>
)}
</div>
<div
className='comment-text'

View File

@ -0,0 +1,126 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import 'isomorphic-fetch'
import {render} from '@testing-library/react'
import {act} from 'react-dom/test-utils'
import {Provider as ReduxProvider} from 'react-redux'
import configureStore from 'redux-mock-store'
import {CommentBlock} from '../../blocks/commentBlock'
import {mockDOM, wrapIntl} from '../../testUtils'
import {FetchMock} from '../../test/fetchMock'
import CommentsList from './commentsList'
global.fetch = FetchMock.fn
beforeEach(() => {
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 = (
<ReduxProvider store={store}>
{wrapIntl(
<CommentsList
comments={[comment1, comment2]}
rootId={'root_id'}
cardId={'card_id'}
readonly={false}
/>,
)}
</ReduxProvider>)
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 = (
<ReduxProvider store={store}>
{wrapIntl(
<CommentsList
comments={[comment1, comment2]}
rootId={'root_id'}
cardId={'card_id'}
readonly={true}
/>,
)}
</ReduxProvider>)
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)
})
})

View File

@ -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,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>'
const newCommentComponent = (
<div className='commentrow'>
<img
className='comment-avatar'
src={userImageUrl}
/>
<MarkdownEditor
className='newcomment'
text={newComment}
placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})}
onChange={(value: string) => {
if (newComment !== value) {
setNewComment(value)
}
}}
onAccept={onSendClicked}
/>
{newComment &&
<Button
filled={true}
onClick={onSendClicked}
>
<FormattedMessage
id='CommentsList.send'
defaultMessage='Send'
/>
</Button>
}
</div>
)
return (
<div className='CommentsList'>
{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}
<div className='commentrow'>
<img
className='comment-avatar'
src={userImageUrl}
/>
<MarkdownEditor
className='newcomment'
text={newComment}
placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})}
onChange={(value: string) => {
if (newComment !== value) {
setNewComment(value)
}
}}
onAccept={onSendClicked}
/>
{newComment &&
<Button
filled={true}
onClick={onSendClicked}
>
<FormattedMessage
id='CommentsList.send'
defaultMessage='Send'
/>
</Button>
}
</div>
{/* horizontal divider below comments */}
{!(comments.length === 0 && props.readonly) && <hr/>}
</div>
)
})

25
webapp/src/testUtils.tsx Normal file
View File

@ -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) => <IntlProvider locale='en'>{children}</IntlProvider>
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
}
}