import { Map as ImmutableMap } from 'immutable'; import sumBy from 'lodash/sumBy'; import { useEffect } from 'react'; import { __stub } from 'soapbox/api'; import { createTestStore, mockStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers'; import { normalizeRelationship } from 'soapbox/normalizers'; import { Store } from 'soapbox/store'; import { flattenPages } from 'soapbox/utils/queries'; import { IAccount } from '../accounts'; import { ChatKeys, IChat, IChatMessage, isLastMessage, useChat, useChatActions, useChatMessages, useChats } from '../chats'; jest.mock('soapbox/utils/queries'); const chat: IChat = { accepted: true, account: { username: 'username', verified: true, id: '1', acct: 'acct', avatar: 'avatar', avatar_static: 'avatar', display_name: 'my name', } as IAccount, created_at: '2020-06-10T02:05:06.000Z', created_by_account: '1', discarded_at: null, id: '1', last_message: null, latest_read_message_by_account: [], latest_read_message_created_at: null, message_expiration: 1209600, unread: 0, }; const buildChatMessage = (id: string): IChatMessage => ({ id, chat_id: '1', account_id: '1', content: `chat message #${id}`, created_at: '2020-06-10T02:05:06.000Z', unread: true, }); describe('ChatKeys', () => { it('has a "chat" key', () => { const id = '1'; expect(ChatKeys.chat(id)).toEqual(['chats', 'chat', id]); }); it('has a "chatMessages" key', () => { const id = '1'; expect(ChatKeys.chatMessages(id)).toEqual(['chats', 'messages', id]); }); it('has a "chatSearch" key', () => { const searchQuery = 'che'; expect(ChatKeys.chatSearch()).toEqual(['chats', 'search']); expect(ChatKeys.chatSearch(searchQuery)).toEqual(['chats', 'search', searchQuery]); }); }); describe('isLastMessage', () => { describe('when its the last message', () => { it('is truthy', () => { const id = '5'; const newChat = { ...chat, last_message: { id } } as any; const initialQueryData = { pages: [ { result: [newChat], hasMore: false, link: undefined }, ], pageParams: [undefined], }; const initialFlattenedData = flattenPages(initialQueryData); expect(sumBy(initialFlattenedData, (chat: IChat) => chat.unread)).toBe(0); queryClient.setQueryData(ChatKeys.chatSearch(), initialQueryData); expect(isLastMessage(id)).toBeTruthy(); }); }); describe('when its not the last message', () => { it('is not truthy', () => { const id = '5'; const newChat = { ...chat, last_message: { id } } as any; const initialQueryData = { pages: [ { result: [newChat], hasMore: false, link: undefined }, ], pageParams: [undefined], }; const initialFlattenedData = flattenPages(initialQueryData); expect(sumBy(initialFlattenedData, (chat: IChat) => chat.unread)).toBe(0); queryClient.setQueryData(ChatKeys.chatSearch(), initialQueryData); expect(isLastMessage('10')).not.toBeTruthy(); }); }); }); describe('useChatMessages', () => { let store: ReturnType; beforeEach(() => { queryClient.clear(); }); describe('when the user is blocked', () => { beforeEach(() => { const state = rootState .set( 'relationships', ImmutableMap({ '1': normalizeRelationship({ blocked_by: true }) }), ); store = mockStore(state); }); it('is does not fetch the endpoint', async () => { const { result } = renderHook(() => useChatMessages(chat), undefined, store); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(result.current.data?.length).toBeUndefined(); }); }); describe('when the user is not blocked', () => { describe('with a successful request', () => { beforeEach(() => { __stub((mock) => { mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`) .reply(200, [ buildChatMessage('2'), ], { link: `; rel="prev"`, }); }); }); it('is successful', async () => { const { result } = renderHook(() => useChatMessages(chat)); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(result.current.data?.length).toBe(1); }); }); describe('with an unsuccessful query', () => { beforeEach(() => { __stub((mock) => { mock.onGet(`/api/v1/pleroma/chats/${chat.id}/messages`).networkError(); }); }); it('is has error state', async() => { const { result } = renderHook(() => useChatMessages(chat)); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(result.current.error).toBeDefined(); }); }); }); }); describe('useChats', () => { let store: ReturnType; beforeEach(() => { queryClient.clear(); }); describe('with a successful request', () => { beforeEach(() => { store = mockStore(rootState); __stub((mock) => { mock.onGet('/api/v1/pleroma/chats') .reply(200, [ chat, ], { link: '; rel="prev"', }); }); }); it('is successful', async () => { const { result } = renderHook(() => useChats().chatsQuery, undefined, store); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(result.current.data?.length).toBe(1); }); }); describe('with an unsuccessful query', () => { beforeEach(() => { __stub((mock) => { mock.onGet('/api/v1/pleroma/chats').networkError(); }); }); it('is has error state', async() => { const { result } = renderHook(() => useChats().chatsQuery); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(result.current.error).toBeDefined(); }); }); }); describe('useChat()', () => { const relationshipId = '123'; let store: Store; beforeEach(() => { const state = rootState; store = createTestStore(state); queryClient.clear(); }); describe('with a successful request', () => { beforeEach(() => { __stub((mock) => { mock.onGet(`/api/v1/pleroma/chats/${chat.id}`).reply(200, chat); mock .onGet(`/api/v1/accounts/relationships?id[]=${chat.account.id}`) .reply(200, [normalizeRelationship({ id: relationshipId, blocked_by: true })]); }); }); it('is successful', async () => { expect(store.getState().relationships.getIn([relationshipId, 'blocked_by'])).toBeUndefined(); const { result } = renderHook(() => useChat(chat.id), undefined, store); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(store.getState().relationships.getIn([relationshipId, 'id'])).toBe(relationshipId); expect(store.getState().relationships.getIn([relationshipId, 'blocked_by'])).toBe(true); expect(result.current.data.id).toBe(chat.id); }); }); describe('with an unsuccessful query', () => { beforeEach(() => { __stub((mock) => { mock.onGet(`/api/v1/pleroma/chats/${chat.id}`).networkError(); }); }); it('is has error state', async() => { const { result } = renderHook(() => useChat(chat.id)); await waitFor(() => expect(result.current.isFetching).toBe(false)); expect(result.current.error).toBeDefined(); }); }); }); describe('useChatActions', () => { beforeEach(() => { queryClient.clear(); }); describe('markChatAsRead()', () => { const nextUnreadCount = 5; beforeEach(() => { __stub((mock) => { mock .onPost(`/api/v1/pleroma/chats/${chat.id}/read`, { last_read_id: '2' }) .reply(200, { ...chat, unread: nextUnreadCount }); }); }); it('updates the queryCache', async() => { const initialQueryData = { pages: [ { result: [chat], hasMore: false, link: undefined }, ], pageParams: [undefined], }; const initialFlattenedData = flattenPages(initialQueryData); expect(sumBy(initialFlattenedData, (chat: IChat) => chat.unread)).toBe(0); queryClient.setQueryData(ChatKeys.chatSearch(), initialQueryData); const { result } = renderHook(() => useChatActions(chat.id).markChatAsRead('2')); await waitFor(() => { expect(result.current).resolves.toBeDefined(); }); const nextQueryData = queryClient.getQueryData(ChatKeys.chatSearch()); const nextFlattenedData = flattenPages(nextQueryData as any); expect(sumBy(nextFlattenedData as any, (chat: IChat) => chat.unread)).toBe(nextUnreadCount); }); }); describe('createChatMessage()', () => { beforeEach(() => { const initialQueryData = { pages: [ { result: [buildChatMessage('1')], hasMore: false, link: undefined }, ], pageParams: [undefined], }; queryClient.setQueryData(ChatKeys.chatMessages(chat.id), initialQueryData); __stub((mock) => { mock .onPost(`/api/v1/pleroma/chats/${chat.id}/messages`) .reply(200, { hello: 'world' }); }); }); it('creates a chat message', async() => { const { result } = renderHook(() => { const { createChatMessage } = useChatActions(chat.id); useEffect(() => { createChatMessage.mutate({ chatId: chat.id, content: 'hello' }); }, []); return createChatMessage; }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); expect(result.current.data.data).toEqual({ hello: 'world' }); }); }); describe('updateChat()', () => { const nextUnreadCount = 5; beforeEach(() => { __stub((mock) => { mock .onPatch(`/api/v1/pleroma/chats/${chat.id}`) .reply(200, { ...chat, unread: nextUnreadCount }); }); }); it('updates the queryCache for the chat', async() => { const initialQueryData = { ...chat }; expect(initialQueryData.message_expiration).toBe(1209600); queryClient.setQueryData(ChatKeys.chat(chat.id), initialQueryData); const { result } = renderHook(() => { const { updateChat } = useChatActions(chat.id); useEffect(() => { updateChat.mutate({ message_expiration: 1200 }); }, []); return updateChat; }); await waitFor(() => { expect(result.current.isLoading).toBe(false); }); const nextQueryData = queryClient.getQueryData(ChatKeys.chat(chat.id)); expect((nextQueryData as any).message_expiration).toBe(1200); }); }); });