Files
Remote-Terminal-for-MeshCore/frontend/src/test/useContactsAndChannels.test.ts
2026-03-18 19:59:31 -07:00

175 lines
5.1 KiB
TypeScript

/**
* 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<unknown>) => 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);
});
});
});