create MessageListHolder and MessageListItem
This is the message collection on the left hand side of the Inbox. Test Plan: - Run Storybook - Verify that the MessageListHolder story is rendering as expected -- Unread converstaions should have an unread badge -- The number badge should show the total number of messages in the thread -- The checkbox should fire the onSelect action -- Mousing over a conversation should show its star button -- Clicking the star button on a conversation should fill in the icon and make the button persist even when moused out of the conversation -- Clicking the star button should fire the onStar callback closes VICE-844 Change-Id: Ia554f8fe9717934dcb72569360a7b180537583c1 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/253560 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Davis Hyer <dhyer@instructure.com> QA-Review: Davis Hyer <dhyer@instructure.com> Product-Review: Davis Hyer <dhyer@instructure.com>
This commit is contained in:
parent
934b46bacd
commit
6d91032edc
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import {View} from '@instructure/ui-view'
|
||||
|
||||
import {MessageListItem, conversationProp} from './MessageListItem'
|
||||
|
||||
export const MessageListHolder = ({...props}) => {
|
||||
return (
|
||||
<View
|
||||
as="div"
|
||||
maxWidth={400}
|
||||
height="100%"
|
||||
overflowX="hidden"
|
||||
overflowY="auto"
|
||||
borderWidth="small"
|
||||
>
|
||||
{props.conversations.map(conversation => (
|
||||
<MessageListItem
|
||||
conversation={conversation.conversation}
|
||||
isUnread={conversation.workflowState === 'unread'}
|
||||
onOpen={props.onOpen}
|
||||
onSelect={props.onSelect}
|
||||
onStar={props.onStar}
|
||||
key={conversation.id}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const conversationParticipantsProp = PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
workflowState: PropTypes.string,
|
||||
conversation: conversationProp
|
||||
})
|
||||
|
||||
MessageListHolder.propTypes = {
|
||||
conversations: PropTypes.arrayOf(conversationParticipantsProp),
|
||||
onOpen: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
onStar: PropTypes.func
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
|
||||
import {MessageListHolder} from './MessageListHolder'
|
||||
|
||||
export default {
|
||||
title: 'Examples/Canvas Inbox/MessageListHolder',
|
||||
component: MessageListHolder,
|
||||
argTypes: {
|
||||
handleOptionSelect: {action: 'onSelect'},
|
||||
handleStar: {action: 'onStar'},
|
||||
handleOpen: {action: 'onOpen'}
|
||||
}
|
||||
}
|
||||
|
||||
const Template = args => <MessageListHolder {...args} />
|
||||
|
||||
export const WithUnreadConversations = Template.bind({})
|
||||
WithUnreadConversations.args = {
|
||||
conversations: [
|
||||
{
|
||||
id: 1,
|
||||
workflowState: 'unread',
|
||||
conversation: {
|
||||
subject: 'This is the subject line',
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Bob Barker'},
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 5, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
},
|
||||
{
|
||||
author: {name: 'Sally Ford'},
|
||||
participants: [{name: 'Sally Ford'}, {name: 'Bob Barker'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 4, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
workflowState: 'unread',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
starred: true,
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const WithConversations = Template.bind({})
|
||||
WithConversations.args = {
|
||||
conversations: [
|
||||
{
|
||||
id: 1,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is the subject line',
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Bob Barker'},
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 5, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
},
|
||||
{
|
||||
author: {name: 'Sally Ford'},
|
||||
participants: [{name: 'Sally Ford'}, {name: 'Bob Barker'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 4, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [
|
||||
{name: 'Jim Clarkson'},
|
||||
{name: 'Barbara Ellis'},
|
||||
{name: 'Bob Barker'},
|
||||
{name: 'Sally Ford'},
|
||||
{name: 'Russel Franks'}
|
||||
],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Jim Clarkson'},
|
||||
participants: [{name: 'Jim Clarkson'}, {name: 'Barbara Ellis'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Badge} from '@instructure/ui-badge'
|
||||
import {Button, IconButton} from '@instructure/ui-buttons'
|
||||
import {Checkbox} from '@instructure/ui-checkbox'
|
||||
import {Focusable} from '@instructure/ui-focusable'
|
||||
import {Grid} from '@instructure/ui-grid'
|
||||
import {IconStarLightLine, IconStarSolid} from '@instructure/ui-icons'
|
||||
import PropTypes from 'prop-types'
|
||||
import React, {useState} from 'react'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
|
||||
import {Text} from '@instructure/ui-text'
|
||||
import {TruncateText} from '@instructure/ui-truncate-text'
|
||||
import {View} from '@instructure/ui-view'
|
||||
import I18n from 'i18n!conversations_2'
|
||||
|
||||
export const MessageListItem = ({...props}) => {
|
||||
const [selected, setSelected] = useState(false)
|
||||
const [isHovering, setIsHovering] = useState(false)
|
||||
const [isStarred, setIsStarred] = useState(props.conversation.starred)
|
||||
|
||||
const handleSelectionChange = () => {
|
||||
props.onSelect(!selected)
|
||||
setSelected(!selected)
|
||||
}
|
||||
|
||||
const handleMessageClick = e => {
|
||||
e.stopPropagation()
|
||||
props.onOpen()
|
||||
}
|
||||
|
||||
const handleMessageStarClick = e => {
|
||||
e.stopPropagation()
|
||||
props.onStar(!isStarred)
|
||||
setIsStarred(!isStarred)
|
||||
}
|
||||
|
||||
const formatParticipants = () => {
|
||||
const participantsStr = props.conversation.participants
|
||||
.filter(p => p.name !== props.conversation.conversationMessages[0].author.name)
|
||||
.reduce((prev, curr) => {
|
||||
return prev + ', ' + curr.name
|
||||
}, '')
|
||||
|
||||
return (
|
||||
<Text>
|
||||
<TruncateText>
|
||||
<b>{props.conversation.conversationMessages[0].author.name}</b>
|
||||
{participantsStr}
|
||||
</TruncateText>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
// TODO: Move these styles to a stylesheet once we are moved to the app/ directory
|
||||
boxShadow: isHovering && 'inset -4px 0px 0px rgb(0, 142, 226)',
|
||||
backgroundColor: selected && 'rgb(229,242,248)'
|
||||
}}
|
||||
>
|
||||
<View
|
||||
data-testid="conversation"
|
||||
as="div"
|
||||
borderWidth="none none small none"
|
||||
padding="small x-small"
|
||||
>
|
||||
<Grid
|
||||
vAlign="middle"
|
||||
colSpacing="none"
|
||||
rowSpacing="none"
|
||||
onMouseEnter={() => {
|
||||
setIsHovering(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setIsHovering(false)
|
||||
}}
|
||||
onClick={handleMessageClick}
|
||||
>
|
||||
<Grid.Row>
|
||||
<Grid.Col width="auto">
|
||||
<View
|
||||
textAlign="center"
|
||||
as="div"
|
||||
width={30}
|
||||
height={30}
|
||||
padding="xx-small"
|
||||
margin="0 small 0 0"
|
||||
>
|
||||
<Checkbox
|
||||
label={
|
||||
<ScreenReaderContent>
|
||||
{selected ? I18n.t('selected') : I18n.t('not selected')}
|
||||
</ScreenReaderContent>
|
||||
}
|
||||
checked={selected}
|
||||
onChange={handleSelectionChange}
|
||||
/>
|
||||
</View>
|
||||
</Grid.Col>
|
||||
<Grid.Col>
|
||||
<Text color="brand">{props.conversation.conversationMessages[0].created_at}</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col width="auto">
|
||||
<Badge
|
||||
count={props.conversation.conversationMessages.length}
|
||||
countUntil={99}
|
||||
standalone
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
<Grid.Col width="auto">
|
||||
<View textAlign="center" as="div" width={30} height={30} margin="0 small 0 0">
|
||||
{props.isUnread && (
|
||||
<Badge
|
||||
type="notification"
|
||||
standalone
|
||||
margin="x-small"
|
||||
formatOutput={() => {
|
||||
return <ScreenReaderContent>{I18n.t('Unread')}</ScreenReaderContent>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Grid.Col>
|
||||
<Grid.Col>{formatParticipants()}</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
<Grid.Col width="auto">
|
||||
<View textAlign="center" as="div" width={30} height={30} margin="0 small 0 0" />
|
||||
</Grid.Col>
|
||||
<Grid.Col>
|
||||
<Text weight="light">
|
||||
<TruncateText>{props.conversation.subject}</TruncateText>
|
||||
</Text>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
<Grid.Col width="auto">
|
||||
<View textAlign="center" as="div" width={30} height={30} margin="0 small 0 0" />
|
||||
</Grid.Col>
|
||||
<Grid.Col>
|
||||
<Text color="secondary">
|
||||
<TruncateText>{props.conversation.conversationMessages[0].body}</TruncateText>
|
||||
</Text>
|
||||
</Grid.Col>
|
||||
<Grid.Col width="auto">
|
||||
<View textAlign="center" as="div" width={30} height={30} margin="0 small 0 0">
|
||||
<Focusable>
|
||||
{({focused}) => {
|
||||
return (
|
||||
<div>
|
||||
{focused || isHovering || isStarred ? (
|
||||
<IconButton
|
||||
size="small"
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
renderIcon={isStarred ? IconStarSolid : IconStarLightLine}
|
||||
screenReaderLabel={
|
||||
isStarred ? I18n.t('starred') : I18n.t('not starred')
|
||||
}
|
||||
onClick={handleMessageStarClick}
|
||||
data-testid="visible-star"
|
||||
/>
|
||||
) : (
|
||||
<ScreenReaderContent>
|
||||
<IconButton
|
||||
size="small"
|
||||
withBackground={false}
|
||||
withBorder={false}
|
||||
renderIcon={isStarred ? IconStarSolid : IconStarLightLine}
|
||||
screenReaderLabel={
|
||||
isStarred ? I18n.t('starred') : I18n.t('not starred')
|
||||
}
|
||||
onClick={handleMessageStarClick}
|
||||
/>
|
||||
</ScreenReaderContent>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Focusable>
|
||||
</View>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
<Grid.Row>
|
||||
<Grid.Col>
|
||||
<Focusable>
|
||||
{({focused}) => {
|
||||
return focused ? (
|
||||
<Button
|
||||
display="block"
|
||||
textAlign="center"
|
||||
size="small"
|
||||
onClick={handleMessageClick}
|
||||
>
|
||||
{I18n.t('Open Message')}
|
||||
</Button>
|
||||
) : (
|
||||
<ScreenReaderContent tabIndex="0">{I18n.t('Open Message')}</ScreenReaderContent>
|
||||
)
|
||||
}}
|
||||
</Focusable>
|
||||
</Grid.Col>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</View>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const participantProp = PropTypes.shape({name: PropTypes.string})
|
||||
|
||||
const conversaionMessageProp = PropTypes.shape({
|
||||
author: participantProp,
|
||||
participants: PropTypes.arrayOf(participantProp),
|
||||
created_at: PropTypes.string,
|
||||
body: PropTypes.string
|
||||
})
|
||||
|
||||
export const conversationProp = PropTypes.shape({
|
||||
subject: PropTypes.string,
|
||||
participants: PropTypes.arrayOf(participantProp),
|
||||
conversationMessages: PropTypes.arrayOf(conversaionMessageProp)
|
||||
})
|
||||
|
||||
MessageListItem.propTypes = {
|
||||
conversation: conversationProp,
|
||||
isUnread: PropTypes.bool,
|
||||
onOpen: PropTypes.func,
|
||||
onSelect: PropTypes.func,
|
||||
onStar: PropTypes.func
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {render} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import {MessageListHolder} from '../MessageListHolder'
|
||||
|
||||
describe('MessageListHolder', () => {
|
||||
it('renders the provided conversations', () => {
|
||||
const props = {
|
||||
conversations: [
|
||||
{
|
||||
id: 1,
|
||||
workflowState: 'unread',
|
||||
conversation: {
|
||||
subject: 'This is the subject line',
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Bob Barker'},
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 5, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
},
|
||||
{
|
||||
author: {name: 'Sally Ford'},
|
||||
participants: [{name: 'Sally Ford'}, {name: 'Bob Barker'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 4, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
workflowState: 'read',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
workflowState: 'unread',
|
||||
conversation: {
|
||||
subject: 'This is a different subject line',
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Todd Martin'},
|
||||
participants: [{name: 'Todd Martin'}, {name: 'Jim Thompson'}],
|
||||
created_at: 'November 3, 2020 at 8:58am',
|
||||
body:
|
||||
'This conversation has a much longer body which should be too long to completely display.'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const {getAllByTestId} = render(<MessageListHolder {...props} />)
|
||||
const conversations = getAllByTestId('conversation')
|
||||
expect(conversations.length).toBe(3)
|
||||
})
|
||||
})
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {render, fireEvent} from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import {MessageListItem} from '../MessageListItem'
|
||||
|
||||
describe('MessageListItem', () => {
|
||||
const createProps = overrides => {
|
||||
return {
|
||||
conversation: {
|
||||
subject: 'This is the subject line',
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
conversationMessages: [
|
||||
{
|
||||
author: {name: 'Bob Barker'},
|
||||
participants: [{name: 'Bob Barker'}, {name: 'Sally Ford'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 5, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
},
|
||||
{
|
||||
author: {name: 'Sally Ford'},
|
||||
participants: [{name: 'Sally Ford'}, {name: 'Bob Barker'}, {name: 'Russel Franks'}],
|
||||
created_at: 'November 4, 2020 at 2:25pm',
|
||||
body: 'This is the body text for the message.'
|
||||
}
|
||||
]
|
||||
},
|
||||
isUnread: false,
|
||||
onSelect: jest.fn(),
|
||||
onOpen: jest.fn(),
|
||||
onStar: jest.fn(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
it('calls the onSelect callback with the new state', () => {
|
||||
const onSelectMock = jest.fn()
|
||||
|
||||
const props = createProps({onSelect: onSelectMock})
|
||||
|
||||
const {getByRole} = render(<MessageListItem {...props} />)
|
||||
|
||||
const checkbox = getByRole('checkbox')
|
||||
fireEvent.click(checkbox)
|
||||
expect(onSelectMock).toHaveBeenLastCalledWith(true)
|
||||
fireEvent.click(checkbox)
|
||||
expect(onSelectMock).toHaveBeenLastCalledWith(false)
|
||||
})
|
||||
|
||||
it('calls onOpen when the message is clicked', () => {
|
||||
const onOpenMock = jest.fn()
|
||||
|
||||
const props = createProps({onOpen: onOpenMock})
|
||||
|
||||
const {getByText} = render(<MessageListItem {...props} />)
|
||||
|
||||
const subjectLine = getByText('This is the subject line')
|
||||
fireEvent.click(subjectLine)
|
||||
expect(onOpenMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('shows and hides the star button correctly', () => {
|
||||
const onStarMock = jest.fn()
|
||||
|
||||
const props = createProps({onStar: onStarMock})
|
||||
|
||||
const {queryByTestId, getByText} = render(<MessageListItem {...props} />)
|
||||
|
||||
// star not shown by default
|
||||
expect(queryByTestId('visible-star')).not.toBeInTheDocument()
|
||||
// star shown when message is moused over
|
||||
const subjectLine = getByText('This is the subject line')
|
||||
fireEvent.mouseOver(subjectLine)
|
||||
expect(queryByTestId('visible-star')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(queryByTestId('visible-star'))
|
||||
expect(onStarMock).toHaveBeenLastCalledWith(true)
|
||||
// star always shows if selected
|
||||
fireEvent.mouseOut(subjectLine)
|
||||
expect(queryByTestId('visible-star')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the unread badge when the conversation is unread', () => {
|
||||
const props = createProps({isUnread: true})
|
||||
|
||||
const {getByText} = render(<MessageListItem {...props} />)
|
||||
|
||||
expect(getByText('Unread')).toBeInTheDocument()
|
||||
})
|
||||
})
|
|
@ -1 +0,0 @@
|
|||
/node_modules
|
|
@ -1,10 +0,0 @@
|
|||
module.exports = {
|
||||
"stories": [
|
||||
"../components/**/*.stories.mdx",
|
||||
"../components/**/*.stories.@(js|jsx|ts|tsx)"
|
||||
],
|
||||
"addons": [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials"
|
||||
]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<div id="canvas_inbox_screenreader_holder" role="alert" aria-live="assertive" aria-relevant="additions text" aria-atomic="false"></div>
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
}
|
||||
|
||||
import '@instructure/canvas-theme'
|
|
@ -1,33 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: '10'
|
||||
},
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3
|
||||
}
|
||||
],
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [['@babel/plugin-proposal-class-properties', {loose: true}]]
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 0,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/nq/_m3pt0qj1177j4xtkbg6kgxrpx_hfv/T/jest_cpta6z",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// Indicates which provider should be used to instrument code for coverage
|
||||
// coverageProvider: "babel",
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less)$': 'identity-obj-proxy'
|
||||
},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
// preset: undefined,
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
setupFiles: ['./utils/jestSetup.js', 'jest-canvas-mock'],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['./utils/envSetup.js'],
|
||||
|
||||
// The number of seconds after which a test is considered as slow and reported as such in the results.
|
||||
// slowTestThreshold: 5,
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// The test environment that will be used for testing
|
||||
testEnvironment: 'jsdom'
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
// testMatch: [
|
||||
// "**/__tests__/**/*.[jt]s?(x)",
|
||||
// "**/?(*.)+(spec|test).[tj]s?(x)"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
// transform: undefined,
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// "/node_modules/",
|
||||
// "\\.pnp\\.[^\\/]+$"
|
||||
// ],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
{
|
||||
"name": "canvas_inbox",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook",
|
||||
"test": "echo"
|
||||
},
|
||||
"description": "An application for Canvas Conversations",
|
||||
"repository": "https://github.com/instructure/canvas-lms.git",
|
||||
"licenses": [],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/preset-env": "^7.11.6",
|
||||
"@babel/preset-react": "^7.11.6",
|
||||
"@sheerun/mutationobserver-shim": "0.3.2",
|
||||
"@testing-library/jest-dom": "^4",
|
||||
"@testing-library/react": "^9",
|
||||
"babel-jest": "^24",
|
||||
"babel-loader": "^8",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^24",
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom-fifteen": "1.0.2",
|
||||
"prop-types": "^15",
|
||||
"react-is": "^16.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@instructure/canvas-theme": "6",
|
||||
"@instructure/ui-a11y-content": "6",
|
||||
"@instructure/ui-alerts": "6",
|
||||
"@instructure/ui-avatar": "6",
|
||||
"@instructure/ui-buttons": "6",
|
||||
"@instructure/ui-checkbox": "6",
|
||||
"@instructure/ui-flex": "6",
|
||||
"@instructure/ui-heading": "6",
|
||||
"@instructure/ui-icons": "6",
|
||||
"@instructure/media-capture": "~7.1.0",
|
||||
"@instructure/ui-menu": "6",
|
||||
"@instructure/ui-modal": "6",
|
||||
"@instructure/ui-simple-select": "6",
|
||||
"@instructure/ui-select": "6",
|
||||
"@instructure/ui-tabs": "6",
|
||||
"@instructure/ui-text": "6",
|
||||
"@instructure/ui-text-area": "6",
|
||||
"@instructure/ui-text-input": "6",
|
||||
"@instructure/ui-tooltip": "6",
|
||||
"@instructure/ui-truncate-text": "6",
|
||||
"@instructure/ui-view": "6",
|
||||
"core-js": "^3.6.5",
|
||||
"prop-types": "^15",
|
||||
"react": "^16.9.0",
|
||||
"react-dom": "^16.9.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@storybook/addon-actions": "^6.0.26",
|
||||
"@storybook/addon-essentials": "^6.0.26",
|
||||
"@storybook/addon-links": "^6.0.26",
|
||||
"@storybook/react": "^6.0.26"
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
|
||||
window.scroll = () => {}
|
|
@ -1,70 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2020 - present Instructure, Inc.
|
||||
*
|
||||
* This file is part of Canvas.
|
||||
*
|
||||
* Canvas is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU Affero General Public License as published by the Free
|
||||
* Software Foundation, version 3 of the License.
|
||||
*
|
||||
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
* details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import '@instructure/canvas-theme'
|
||||
|
||||
// because InstUI themeable components need an explicit "dir" attribute on the <html> element
|
||||
document.documentElement.setAttribute('dir', 'ltr')
|
||||
|
||||
// set up mocks for native APIs
|
||||
if (!('MutationObserver' in window)) {
|
||||
Object.defineProperty(window, 'MutationObserver', {
|
||||
value: require('@sheerun/mutationobserver-shim')
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* We want to ensure errors and warnings get appropriate eyes. If
|
||||
* you are seeing an exception from here, it probably means you
|
||||
* have an unintended consequence from your changes. If you expect
|
||||
* the warning/error, add it to the ignore list below.
|
||||
*/
|
||||
/* eslint-disable no-console */
|
||||
const globalError = console.error
|
||||
const ignoredErrors = []
|
||||
const globalWarn = console.warn
|
||||
const ignoredWarnings = [
|
||||
/Warning: componentWillReceiveProps has been renamed/,
|
||||
/Warning: \[SimpleSelect\] is experimental.*/,
|
||||
/Warning: \[themeable\] component styles require setting a \'dir\'*/,
|
||||
/Warning: \[Focusable\] Exactly one focusable child is required \(0 found\)/
|
||||
]
|
||||
global.console = {
|
||||
log: console.log,
|
||||
error: error => {
|
||||
if (ignoredErrors.some(regex => regex.test(error))) {
|
||||
return
|
||||
}
|
||||
globalError(error)
|
||||
throw new Error(
|
||||
'Looks like you have an unhandled error. Keep our test logs clean by handling or filtering it'
|
||||
)
|
||||
},
|
||||
warn: warning => {
|
||||
if (ignoredWarnings.some(regex => regex.test(warning))) {
|
||||
return
|
||||
}
|
||||
globalWarn(warning)
|
||||
throw new Error(
|
||||
'Looks like you have an unhandled warning. Keep our test logs clean by handling or filtering it'
|
||||
)
|
||||
},
|
||||
info: console.info,
|
||||
debug: console.debug
|
||||
}
|
||||
/* eslint-enable no-console */
|
|
@ -32,6 +32,7 @@
|
|||
"@instructure/ui-a11y": "6",
|
||||
"@instructure/ui-alerts": "6",
|
||||
"@instructure/ui-avatar": "6",
|
||||
"@instructure/ui-badge": "6",
|
||||
"@instructure/ui-billboard": "6",
|
||||
"@instructure/ui-breadcrumb": "6",
|
||||
"@instructure/ui-buttons": "6",
|
||||
|
|
Loading…
Reference in New Issue