/** * Tests for useContactsAndChannels hook. * * Focuses on pagination logic in fetchAllContacts (which fetches 1000 items * per page and continues until a page returns fewer than pageSize results). */ import { act, renderHook } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { useContactsAndChannels } from '../hooks/useContactsAndChannels'; import type { Contact } from '../types'; // Mock api module vi.mock('../api', () => ({ api: { getContacts: vi.fn(), getChannels: vi.fn(), createContact: vi.fn(), createChannel: vi.fn(), deleteContact: vi.fn(), deleteChannel: vi.fn(), decryptHistoricalPackets: vi.fn(), getUndecryptedPacketCount: vi.fn(), }, })); // Mock prefetch — takePrefetchOrFetch calls the fetcher directly vi.mock('../prefetch', () => ({ takePrefetchOrFetch: vi.fn((_key: string, fetcher: () => Promise) => fetcher()), })); // Mock sonner vi.mock('../components/ui/sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() }, })); function makeContact(suffix: string): Contact { const key = suffix.padStart(64, '0'); return { public_key: key, name: `Contact-${suffix}`, type: 1, flags: 0, direct_path: null, direct_path_len: -1, direct_path_hash_mode: 0, last_advert: null, lat: null, lon: null, last_seen: null, on_radio: false, last_contacted: null, last_read_at: null, first_seen: null, }; } function makeContacts(count: number, startIndex = 0): Contact[] { return Array.from({ length: count }, (_, i) => makeContact(String(startIndex + i).padStart(4, '0')) ); } describe('useContactsAndChannels', () => { const setActiveConversation = vi.fn(); const removeConversationMessages = vi.fn(); const pendingDeleteFallbackRef = { current: false }; const hasSetDefaultConversation = { current: false }; beforeEach(() => { vi.clearAllMocks(); pendingDeleteFallbackRef.current = false; hasSetDefaultConversation.current = false; }); afterEach(() => { vi.restoreAllMocks(); }); function renderUseContactsAndChannels() { return renderHook(() => useContactsAndChannels({ setActiveConversation, pendingDeleteFallbackRef, hasSetDefaultConversation, removeConversationMessages, }) ); } describe('fetchAllContacts pagination', () => { it('returns contacts directly when fewer than page size', async () => { const { api } = await import('../api'); const contacts = makeContacts(50); vi.mocked(api.getContacts).mockResolvedValueOnce(contacts); const { result } = renderUseContactsAndChannels(); let fetched: Contact[] = []; await act(async () => { fetched = await result.current.fetchAllContacts(); }); expect(fetched).toHaveLength(50); // Should only call once (no pagination needed) expect(api.getContacts).toHaveBeenCalledTimes(1); expect(api.getContacts).toHaveBeenCalledWith(1000, 0); }); it('paginates when first page returns exactly page size', async () => { const { api } = await import('../api'); const page1 = makeContacts(1000, 0); const page2 = makeContacts(200, 1000); vi.mocked(api.getContacts) .mockResolvedValueOnce(page1) // First page: full .mockResolvedValueOnce(page2); // Second page: partial (done) const { result } = renderUseContactsAndChannels(); let fetched: Contact[] = []; await act(async () => { fetched = await result.current.fetchAllContacts(); }); expect(fetched).toHaveLength(1200); expect(api.getContacts).toHaveBeenCalledTimes(2); expect(api.getContacts).toHaveBeenNthCalledWith(1, 1000, 0); expect(api.getContacts).toHaveBeenNthCalledWith(2, 1000, 1000); }); it('paginates through multiple full pages', async () => { const { api } = await import('../api'); const page1 = makeContacts(1000, 0); const page2 = makeContacts(1000, 1000); const page3 = makeContacts(500, 2000); vi.mocked(api.getContacts) .mockResolvedValueOnce(page1) .mockResolvedValueOnce(page2) .mockResolvedValueOnce(page3); const { result } = renderUseContactsAndChannels(); let fetched: Contact[] = []; await act(async () => { fetched = await result.current.fetchAllContacts(); }); expect(fetched).toHaveLength(2500); expect(api.getContacts).toHaveBeenCalledTimes(3); expect(api.getContacts).toHaveBeenNthCalledWith(3, 1000, 2000); }); it('handles exactly page size total (boundary case)', async () => { const { api } = await import('../api'); const page1 = makeContacts(1000, 0); const page2: Contact[] = []; // Empty second page vi.mocked(api.getContacts).mockResolvedValueOnce(page1).mockResolvedValueOnce(page2); const { result } = renderUseContactsAndChannels(); let fetched: Contact[] = []; await act(async () => { fetched = await result.current.fetchAllContacts(); }); expect(fetched).toHaveLength(1000); expect(api.getContacts).toHaveBeenCalledTimes(2); }); }); });