Add tests for chats query
This commit is contained in:
parent
3e2888eb75
commit
24bca458bd
8 changed files with 378 additions and 36 deletions
|
@ -18,8 +18,8 @@ const chat: any = {
|
|||
id: '12332423234',
|
||||
unread: true,
|
||||
},
|
||||
created_at: new Date('2022-09-09T16:02:26.186Z'),
|
||||
updated_at: new Date('2022-09-09T16:02:26.186Z'),
|
||||
created_at: '2022-09-09T16:02:26.186Z',
|
||||
updated_at: '2022-09-09T16:02:26.186Z',
|
||||
accepted: true,
|
||||
discarded_at: null,
|
||||
account: {
|
||||
|
|
|
@ -28,7 +28,7 @@ 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'),
|
||||
created_at: '2022-09-09T16:02:26.186Z',
|
||||
id: '1',
|
||||
unread: false,
|
||||
pending: false,
|
||||
|
@ -37,7 +37,7 @@ const chatMessages: IChatMessage[] = [
|
|||
account_id: '2',
|
||||
chat_id: '14',
|
||||
content: 'this is the second chat',
|
||||
created_at: new Date('2022-09-09T16:04:26.186Z'),
|
||||
created_at: '2022-09-09T16:04:26.186Z',
|
||||
id: '2',
|
||||
unread: true,
|
||||
pending: false,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { configureMockStore } from '@jedmao/redux-mock-store';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { render, RenderOptions } from '@testing-library/react';
|
||||
import { renderHook, RenderHookOptions } from '@testing-library/react-hooks';
|
||||
import { merge } from 'immutable';
|
||||
|
@ -11,6 +11,10 @@ import { Action, applyMiddleware, createStore } from 'redux';
|
|||
import thunk from 'redux-thunk';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { ChatProvider } from 'soapbox/contexts/chat-context';
|
||||
import { StatProvider } from 'soapbox/contexts/stat-context';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
|
||||
import NotificationsContainer from '../features/ui/containers/notifications_container';
|
||||
import { default as rootReducer } from '../reducers';
|
||||
|
||||
|
@ -27,23 +31,6 @@ const applyActions = (state: any, actions: any, reducer: any) => {
|
|||
return actions.reduce((state: any, action: any) => reducer(state, action), state);
|
||||
};
|
||||
|
||||
/** React Query client for tests. */
|
||||
const queryClient = new QueryClient({
|
||||
logger: {
|
||||
// eslint-disable-next-line no-console
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: () => { },
|
||||
},
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 0,
|
||||
cacheTime: Infinity,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createTestStore = (initialState: any) => createStore(rootReducer, initialState, applyMiddleware(thunk));
|
||||
const TestApp: FC<any> = ({ children, storeProps, routerProps = {} }) => {
|
||||
let store: ReturnType<typeof createTestStore>;
|
||||
|
@ -63,15 +50,19 @@ const TestApp: FC<any> = ({ children, storeProps, routerProps = {} }) => {
|
|||
|
||||
return (
|
||||
<Provider store={props.store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IntlProvider locale={props.locale}>
|
||||
<MemoryRouter {...routerProps}>
|
||||
{children}
|
||||
<StatProvider>
|
||||
<ChatProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<IntlProvider locale={props.locale}>
|
||||
<MemoryRouter {...routerProps}>
|
||||
{children}
|
||||
|
||||
<NotificationsContainer />
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
<NotificationsContainer />
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
</QueryClientProvider>
|
||||
</ChatProvider>
|
||||
</StatProvider>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,9 @@ import { __clear as clearApiMocks } from '../__mocks__/api';
|
|||
jest.mock('soapbox/api');
|
||||
afterEach(() => clearApiMocks());
|
||||
|
||||
// Query mocking
|
||||
jest.mock('soapbox/queries/client');
|
||||
|
||||
// Mock IndexedDB
|
||||
// https://dev.to/andyhaskell/testing-your-indexeddb-code-with-jest-2o17
|
||||
require('fake-indexeddb/auto');
|
||||
|
|
19
app/soapbox/queries/__mocks__/client.ts
Normal file
19
app/soapbox/queries/__mocks__/client.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
logger: {
|
||||
// eslint-disable-next-line no-console
|
||||
log: console.log,
|
||||
warn: console.warn,
|
||||
error: () => { },
|
||||
},
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 0,
|
||||
cacheTime: Infinity,
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export { queryClient };
|
271
app/soapbox/queries/__tests__/chats.test.ts
Normal file
271
app/soapbox/queries/__tests__/chats.test.ts
Normal file
|
@ -0,0 +1,271 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import sumBy from 'lodash/sumBy';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, queryClient, renderHook, rootState, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeRelationship } from 'soapbox/normalizers';
|
||||
import { flattenPages } from 'soapbox/utils/queries';
|
||||
|
||||
import { IAccount } from '../accounts';
|
||||
import { ChatKeys, IChat, IChatMessage, 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: null,
|
||||
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('useChatMessages', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
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: `<https://example.com/api/v1/pleroma/chats/${chat.id}/messages?since_id=2>; 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<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
store = mockStore(ImmutableMap());
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/pleroma/chats')
|
||||
.reply(200, [
|
||||
chat,
|
||||
], {
|
||||
link: '<https://example.com/api/v1/pleroma/chats?since_id=2>; 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()', () => {
|
||||
beforeEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
describe('with a successful request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/chats/${chat.id}`).reply(200, chat);
|
||||
});
|
||||
});
|
||||
|
||||
it('is successful', async () => {
|
||||
const { result } = renderHook(() => useChat(chat.id).chat);
|
||||
|
||||
await waitFor(() => expect(result.current.isFetching).toBe(false));
|
||||
|
||||
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).chat);
|
||||
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -27,7 +27,7 @@ export enum MessageExpirationValues {
|
|||
export interface IChat {
|
||||
accepted: boolean
|
||||
account: IAccount
|
||||
created_at: Date
|
||||
created_at: string
|
||||
created_by_account: string
|
||||
discarded_at: null | string
|
||||
id: string
|
||||
|
@ -40,10 +40,10 @@ export interface IChat {
|
|||
id: string
|
||||
unread: boolean
|
||||
}
|
||||
latest_read_message_by_account: {
|
||||
latest_read_message_by_account: null | {
|
||||
[id: number]: string
|
||||
}[]
|
||||
latest_read_message_created_at: string
|
||||
latest_read_message_created_at: null | string
|
||||
message_expiration: MessageExpirationValues
|
||||
unread: number
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ export interface IChatMessage {
|
|||
account_id: string
|
||||
chat_id: string
|
||||
content: string
|
||||
created_at: Date
|
||||
created_at: string
|
||||
id: string
|
||||
unread: boolean
|
||||
pending?: boolean
|
||||
|
@ -193,11 +193,12 @@ const useChatActions = (chatId: string) => {
|
|||
|
||||
const { chat, setChat, setEditing } = useChatContext();
|
||||
|
||||
const markChatAsRead = (lastReadId: string) => {
|
||||
api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
|
||||
const markChatAsRead = async (lastReadId: string) => {
|
||||
return api.post<IChat>(`/api/v1/pleroma/chats/${chatId}/read`, { last_read_id: lastReadId })
|
||||
.then(({ data }) => {
|
||||
updatePageItem(ChatKeys.chatSearch(), data, (o, n) => o.id === n.id);
|
||||
const queryData = queryClient.getQueryData<InfiniteData<PaginatedResult<unknown>>>(ChatKeys.chatSearch());
|
||||
|
||||
if (queryData) {
|
||||
const flattenedQueryData: any = flattenPages(queryData)?.map((chat: any) => {
|
||||
if (chat.id === data.id) {
|
||||
|
@ -208,6 +209,8 @@ const useChatActions = (chatId: string) => {
|
|||
});
|
||||
setUnreadChatsCount(sumBy(flattenedQueryData, (chat: IChat) => chat.unread));
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
.catch(() => null);
|
||||
};
|
||||
|
|
55
app/soapbox/utils/__mocks__/queries.ts
Normal file
55
app/soapbox/utils/__mocks__/queries.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
|
||||
import { queryClient } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { PaginatedResult } from '../queries';
|
||||
|
||||
const flattenPages = <T>(queryData: UseInfiniteQueryResult<PaginatedResult<T>>['data']) => {
|
||||
return queryData?.pages.reduce<T[]>(
|
||||
(prev: T[], curr) => [...curr.result, ...prev],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
const updatePageItem = <T>(queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = data.pages.map(page => {
|
||||
const result = page.result.map(item => isItem(item, newItem) ? newItem : item);
|
||||
return { ...page, result };
|
||||
});
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Insert the new item at the beginning of the first page. */
|
||||
const appendPageItem = <T>(queryKey: QueryKey, newItem: T) => {
|
||||
queryClient.setQueryData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = [...data.pages];
|
||||
pages[0] = { ...pages[0], result: [...pages[0].result, newItem] };
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Remove an item inside if found. */
|
||||
const removePageItem = <T>(queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = data.pages.map(page => {
|
||||
const result = page.result.filter(item => !isItem(item, itemToRemove));
|
||||
return { ...page, result };
|
||||
});
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
flattenPages,
|
||||
updatePageItem,
|
||||
appendPageItem,
|
||||
removePageItem,
|
||||
};
|
Loading…
Reference in a new issue