import { InfiniteData, QueryClientProvider, notifyManager } from '@tanstack/react-query' import { act, renderHook } from '@testing-library/react-native' import React from 'react' import { chatQueryClient } from '../../contexts/api_provider' import type { ApiCollection, MessageResource } from '../../types' import { getMessagesQueryKey } from '../../utils/request/get_messages' import { useMessageCreateOrUpdate } from '../use_message_create_or_update' jest.mock('../../contexts/api_provider', () => { const { buildTestQueryClient } = require('../../__utils__/query_client') return { chatQueryClient: buildTestQueryClient() } }) const mockPost = jest.fn() jest.mock('../use_api_client', () => ({ useApiClient: () => ({ chat: { post: mockPost, patch: jest.fn() } }), })) jest.mock('../use_current_person', () => ({ useCurrentPerson: () => ({ type: 'Person', id: 1, name: 'Me', avatar: null }), })) // Flush TanStack's batched state updates inside act, as its own tests do notifyManager.setNotifyFunction(fn => act(fn)) type MessagesQueryData = InfiniteData> const conversationId = 7 const queryKey = getMessagesQueryKey({ conversation_id: conversationId }) const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) const pendingMessagesInCache = () => (chatQueryClient.getQueryData(queryKey)?.pages[0].data ?? []).filter( m => m.pending ) const sentKey = (call: number): string => mockPost.mock.calls[call][0].data.data.attributes.idempotent_key // What the cache held when each request went out, i.e. after onMutate ran let pendingAtSend: MessageResource[][] describe('useMessageCreateOrUpdate idempotent key', () => { beforeEach(() => { chatQueryClient.clear() chatQueryClient.setQueryData(queryKey, { pages: [{ data: [], links: {}, meta: { count: 0, totalCount: 0 } }], pageParams: [], }) pendingAtSend = [] mockPost.mockReset() mockPost.mockImplementation(async request => { pendingAtSend.push(pendingMessagesInCache()) const key = request.data.data.attributes.idempotent_key return { data: { type: 'Message', id: `real-${key}`, idempotentKey: key, mine: true } } }) }) it('gives the optimistic message the same key that is sent to the server', async () => { const { result } = renderHook(() => useMessageCreateOrUpdate({ conversationId }), { wrapper }) await act(() => result.current.mutateAsync({ text: 'ok' })) expect(sentKey(0)).toEqual(expect.any(String)) expect(pendingAtSend[0].map(m => m.idempotentKey)).toEqual([sentKey(0)]) }) it('mints a distinct key for each send, even with identical text', async () => { const { result } = renderHook(() => useMessageCreateOrUpdate({ conversationId }), { wrapper }) await act(() => Promise.all([ result.current.mutateAsync({ text: 'ok' }), result.current.mutateAsync({ text: 'ok' }), ]) ) expect(sentKey(0)).not.toBe(sentKey(1)) expect(pendingAtSend[1].map(m => m.idempotentKey).sort()).toEqual( [sentKey(0), sentKey(1)].sort() ) }) it('reuses the original key when retrying a failed send', async () => { const failed = { type: 'Message', id: '01FAILED-temp', text: 'ok', idempotentKey: 'original-key', error: 'Failed to send message', } as MessageResource const { result } = renderHook( () => useMessageCreateOrUpdate({ conversationId, message: failed }), { wrapper } ) await act(() => result.current.mutateAsync({ text: 'ok' })) expect(sentKey(0)).toBe('original-key') expect(pendingAtSend[0].map(m => m.idempotentKey)).toEqual(['original-key']) }) })