chats-router
Justin 2022-09-12 14:42:15 -04:00
rodzic a68aeb8464
commit 81bfc06990
11 zmienionych plików z 387 dodań i 12 usunięć

Wyświetl plik

@ -216,7 +216,12 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }} ref={this.setRef}>
<div
className={`dropdown-menu ${placement}`}
style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }}
ref={this.setRef}
data-testid='dropdown-menu'
>
<div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} />
<ul>
{items.map((option, i) => this.renderItem(option, i))}

Wyświetl plik

@ -0,0 +1,136 @@
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ChatContext } from 'soapbox/contexts/chat-context';
import { IAccount } from 'soapbox/queries/accounts';
import { __stub } from '../../../../api';
import { queryClient, render, rootState, screen, waitFor } from '../../../../jest/test-helpers';
import { IChat, IChatMessage } from '../../../../queries/chats';
import ChatMessageList from '../chat-message-list';
const chat: IChat = {
id: '14',
unread: 5,
created_by_account: '2',
account: {
id: '1',
avatar: 'url',
acct: 'username',
} as IAccount,
last_message: null,
accepted: true,
} as IChat;
const chatMessages: IChatMessage[] = [
{
account_id: '1',
chat_id: '14',
content: 'this is the first chat',
created_at: new Date('2022-09-09T16:02:26.186Z'),
id: '1',
unread: false,
pending: false,
},
{
account_id: '2',
chat_id: '14',
content: 'this is the second chat',
created_at: new Date('2022-09-09T16:04:26.186Z'),
id: '2',
unread: true,
pending: false,
},
];
// Mock scrollIntoView function.
window.HTMLElement.prototype.scrollIntoView = function() { };
Object.assign(navigator, {
clipboard: {
writeText: () => {},
},
});
const store = rootState.set('me', '1');
const renderComponentWithChatContext = () => render(
<ChatContext.Provider value={{ chat }}>
<ChatMessageList chat={chat} />
</ChatContext.Provider>,
undefined,
store,
);
beforeEach(() => {
queryClient.clear();
});
describe('<ChatMessageList />', () => {
describe('when the query is loading', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).reply(200, chatMessages, {
link: null,
});
});
});
it('displays the skeleton loader', async() => {
renderComponentWithChatContext();
expect(screen.queryAllByTestId('placeholder-chat-message')).toHaveLength(5);
await waitFor(() => {
expect(screen.getByTestId('chat-message-list-intro')).toBeInTheDocument();
expect(screen.queryAllByTestId('placeholder-chat-message')).toHaveLength(0);
});
});
});
describe('when the query is finished loading', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).reply(200, chatMessages, {
link: null,
});
});
});
it('displays the intro', async() => {
renderComponentWithChatContext();
expect(screen.queryAllByTestId('chat-message-list-intro')).toHaveLength(0);
await waitFor(() => {
expect(screen.getByTestId('chat-message-list-intro')).toBeInTheDocument();
});
});
it('displays the messages', async() => {
renderComponentWithChatContext();
expect(screen.queryAllByTestId('chat-message')).toHaveLength(0);
await waitFor(() => {
expect(screen.queryAllByTestId('chat-message')).toHaveLength(chatMessages.length);
expect(screen.queryAllByTestId('chat-message')[0]).toHaveTextContent(chatMessages[0].content);
});
});
it('displays the correct menu options depending on the owner of the message', async() => {
renderComponentWithChatContext();
await waitFor(() => {
expect(screen.queryAllByTestId('chat-message-menu')).toHaveLength(2);
});
await userEvent.click(screen.queryAllByTestId('chat-message-menu')[0].querySelector('button') as any);
expect(screen.getByTestId('dropdown-menu')).toHaveTextContent('Delete');
expect(screen.getByTestId('dropdown-menu')).toHaveTextContent('Copy');
await userEvent.click(screen.queryAllByTestId('chat-message-menu')[1].querySelector('button') as any);
expect(screen.getByTestId('dropdown-menu')).not.toHaveTextContent('Delete');
expect(screen.getByTestId('dropdown-menu')).toHaveTextContent('Copy');
});
});
});

Wyświetl plik

@ -0,0 +1,83 @@
import userEvent from '@testing-library/user-event';
import React from 'react';
import { render, screen } from '../../../../jest/test-helpers';
import ChatPaneHeader from '../chat-pane-header';
describe('<ChatPaneHeader />', () => {
it('handles the onToggle prop', async() => {
const mockFn = jest.fn();
render(<ChatPaneHeader title='title' onToggle={mockFn} isOpen />);
await userEvent.click(screen.getByTestId('icon-button'));
expect(mockFn).toHaveBeenCalled();
});
describe('the "title" prop', () => {
describe('when it is a string', () => {
it('renders the title', () => {
const title = 'Messages';
render(<ChatPaneHeader title={title} onToggle={jest.fn()} isOpen />);
expect(screen.getByTestId('title')).toHaveTextContent(title);
});
});
describe('when it is a node', () => {
it('renders the title', () => {
const title = (
<div><p>hello world</p></div>
);
render(<ChatPaneHeader title={title} onToggle={jest.fn()} isOpen />);
expect(screen.getByTestId('title')).toHaveTextContent('hello world');
});
});
});
describe('the "unreadCount" prop', () => {
describe('when present', () => {
it('renders the unread count', () => {
const count = 14;
render(<ChatPaneHeader title='title' onToggle={jest.fn()} isOpen unreadCount={count} />);
expect(screen.getByTestId('unread-count')).toHaveTextContent(String(count));
});
});
describe('when 0', () => {
it('does not render the unread count', () => {
const count = 0;
render(<ChatPaneHeader title='title' onToggle={jest.fn()} isOpen unreadCount={count} />);
expect(screen.queryAllByTestId('unread-count')).toHaveLength(0);
});
});
describe('when unprovided', () => {
it('does not render the unread count', () => {
render(<ChatPaneHeader title='title' onToggle={jest.fn()} isOpen />);
expect(screen.queryAllByTestId('unread-count')).toHaveLength(0);
});
});
});
describe('secondaryAction prop', () => {
it('handles the secondaryAction callback', async() => {
const mockFn = jest.fn();
render(
<ChatPaneHeader
title='title'
onToggle={jest.fn()}
isOpen
secondaryAction={mockFn}
secondaryActionIcon='icon.svg'
/>,
);
await userEvent.click(screen.queryAllByTestId('icon-button')[0]);
expect(mockFn).toBeCalled();
});
});
});

Wyświetl plik

@ -0,0 +1,65 @@
import userEvent from '@testing-library/user-event';
import React from 'react';
import { __stub } from 'soapbox/api';
import { ChatProvider } from 'soapbox/contexts/chat-context';
import { render, screen, waitFor } from '../../../../jest/test-helpers';
import ChatSearch from '../chat-search';
const renderComponent = () => render(
<ChatProvider>
<ChatSearch />
</ChatProvider>,
);
describe('<ChatSearch />', () => {
it('renders correctly', () => {
renderComponent();
expect(screen.getByTestId('pane-header')).toHaveTextContent('Messages');
});
describe('when the pane is closed', () => {
it('does not render the search input', () => {
renderComponent();
expect(screen.queryAllByTestId('search')).toHaveLength(0);
});
});
describe('when the pane is open', () => {
beforeEach(async() => {
renderComponent();
await userEvent.click(screen.getByTestId('icon-button'));
});
it('renders the search input', () => {
expect(screen.getByTestId('search')).toBeInTheDocument();
});
describe('when searching', () => {
beforeEach(() => {
__stub((mock) => {
mock.onGet('/api/v1/accounts/search').reply(200, [{
id: '1',
avatar: 'url',
verified: false,
display_name: 'steve',
acct: 'sjobs',
}]);
});
});
it('renders accounts', async() => {
renderComponent();
const user = userEvent.setup();
await user.type(screen.getByTestId('search'), 'ste');
await waitFor(() => {
expect(screen.queryAllByTestId('account')).toHaveLength(1);
});
});
});
});
});

Wyświetl plik

@ -0,0 +1,68 @@
import React from 'react';
import { IChat } from 'soapbox/queries/chats';
import { render, screen } from '../../../../jest/test-helpers';
import Chat from '../chat';
const chat: any = {
id: '1',
unread: 5,
created_by_account: '2',
last_message: {
account_id: '2',
chat_id: '1',
content: 'hello world',
created_at: '2022-09-09T16:02:26.186Z',
discarded_at: null,
id: '12332423234',
unread: true,
},
created_at: new Date('2022-09-09T16:02:26.186Z'),
updated_at: new Date('2022-09-09T16:02:26.186Z'),
accepted: true,
discarded_at: null,
account: {
acct: 'username',
display_name: 'johnnie',
},
};
describe('<Chat />', () => {
it('renders correctly', () => {
render(<Chat chat={chat as IChat} onClick={jest.fn()} />);
expect(screen.getByTestId('chat')).toBeInTheDocument();
expect(screen.getByTestId('chat')).toHaveTextContent(chat.account.display_name);
});
describe('last message content', () => {
it('renders the last message', () => {
render(<Chat chat={chat as IChat} onClick={jest.fn()} />);
expect(screen.getByTestId('chat-last-message')).toBeInTheDocument();
});
it('does not render the last message', () => {
const changedChat = { ...chat, last_message: null };
render(<Chat chat={changedChat as IChat} onClick={jest.fn()} />);
expect(screen.queryAllByTestId('chat-last-message')).toHaveLength(0);
});
describe('unread', () => {
it('renders the unread dot', () => {
render(<Chat chat={chat as IChat} onClick={jest.fn()} />);
expect(screen.getByTestId('chat-unread-indicator')).toBeInTheDocument();
});
it('does not render the unread dot', () => {
const changedChat = { ...chat, last_message: { ...chat.last_message, unread: false } };
render(<Chat chat={changedChat as IChat} onClick={jest.fn()} />);
expect(screen.queryAllByTestId('chat-unread-indicator')).toHaveLength(0);
});
});
});
});

Wyświetl plik

@ -37,6 +37,7 @@ const ChatMessageListIntro = () => {
return (
<Stack
data-testid='chat-message-list-intro'
justifyContent='center'
alignItems='center'
space={4}

Wyświetl plik

@ -3,7 +3,7 @@ import classNames from 'clsx';
import { List as ImmutableList } from 'immutable';
import escape from 'lodash/escape';
import throttle from 'lodash/throttle';
import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
@ -81,7 +81,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
const formattedChatMessages = chatMessages || [];
const me = useAppSelector((state) => state.me);
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account, 'blocked_by']));
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat.account.id, 'blocked_by']));
const node = useRef<HTMLDivElement>(null);
const messagesEnd = useRef<HTMLDivElement>(null);
@ -240,7 +240,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
}
return (
<div key={chatMessage.id} className='group'>
<div key={chatMessage.id} className='group' data-testid='chat-message'>
<Stack
space={1}
className={classNames({
@ -293,7 +293,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
>
{maybeRenderMedia(chatMessage)}
<Text size='sm' theme='inherit' dangerouslySetInnerHTML={{ __html: parseContent(chatMessage) }} />
<div className='chat-message__menu'>
<div className='chat-message__menu' data-testid='chat-message-menu'>
<DropdownMenuContainer
items={menu}
src={require('@tabler/icons/dots.svg')}
@ -395,7 +395,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat, autosize }) => {
return (
<Stack alignItems='center' justifyContent='center' className='h-full flex-grow'>
<Stack alignItems='center' space={2}>
<Avatar src={chat.account.avatar_static} size={75} />
<Avatar src={chat.account.avatar} size={75} />
<Text align='center'>
<>
<Text tag='span'>You are blocked by</Text>

Wyświetl plik

@ -21,6 +21,7 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
secondaryActionIcon,
title,
unreadCount,
...rest
} = props;
const ButtonComp = isToggleable ? 'button' : 'div';
@ -30,9 +31,10 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
}
return (
<HStack alignItems='center' justifyContent='between' className='rounded-t-xl h-16 py-3 px-4'>
<HStack {...rest} alignItems='center' justifyContent='between' className='rounded-t-xl h-16 py-3 px-4'>
<ButtonComp
className='flex-grow flex items-center flex-row space-x-1 h-16'
data-testid='title'
{...buttonProps}
>
{typeof title === 'string' ? (
@ -43,7 +45,7 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
{(typeof unreadCount !== 'undefined' && unreadCount > 0) && (
<HStack alignItems='center' space={2}>
<Text weight='semibold'>
<Text weight='semibold' data-testid='unread-count'>
({unreadCount})
</Text>

Wyświetl plik

@ -50,6 +50,7 @@ const ChatSearch = () => {
return (
<Pane isOpen={isOpen} index={0} main>
<ChatPaneHeader
data-testid='pane-header'
title={
<HStack alignItems='center' space={2}>
<button onClick={() => setSearching(false)}>
@ -71,6 +72,7 @@ const ChatSearch = () => {
<Stack space={4} className='flex-grow h-full'>
<div className='px-4'>
<Input
data-testid='search'
type='text'
autoFocus
placeholder='Type a name'
@ -100,6 +102,7 @@ const ChatSearch = () => {
handleClickOnSearchResult.mutate(account.id);
clearValue();
}}
data-testid='account'
>
<HStack alignItems='center' space={2}>
<Avatar src={account.avatar} size={40} />

Wyświetl plik

@ -18,6 +18,7 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
type='button'
onClick={() => onClick(chat)}
className='px-4 py-2 w-full flex flex-col hover:bg-gray-100 dark:hover:bg-gray-800'
data-testid='chat'
>
<HStack alignItems='center' justifyContent='between' space={2} className='w-full'>
<HStack alignItems='center' space={2}>
@ -30,9 +31,16 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
</div>
{chat.last_message?.content && (
<Text align='left' size='sm' weight='medium' theme='muted' truncate className='max-w-[200px]'>
{chat.last_message?.content}
</Text>
<Text
align='left'
size='sm'
weight='medium'
theme='muted'
truncate
className='max-w-[200px]'
data-testid='chat-last-message'
dangerouslySetInnerHTML={{ __html: chat.last_message?.content }}
/>
)}
</Stack>
</HStack>
@ -40,7 +48,10 @@ const Chat: React.FC<IChatInterface> = ({ chat, onClick }) => {
{chat.last_message && (
<HStack alignItems='center' space={2}>
{chat.last_message.unread && (
<div className='w-2 h-2 rounded-full bg-secondary-500' />
<div
className='w-2 h-2 rounded-full bg-secondary-500'
data-testid='chat-unread-indicator'
/>
)}
<RelativeTimestamp timestamp={chat.last_message.created_at} size='sm' />

Wyświetl plik

@ -13,6 +13,7 @@ const PlaceholderChatMessage = ({ isMyMessage = false }: { isMyMessage?: boolean
return (
<Stack
data-testid='placeholder-chat-message'
space={1}
className={classNames({
'max-w-[85%] animate-pulse': true,