diff --git a/frontend/src/components/ContactInfoPane.tsx b/frontend/src/components/ContactInfoPane.tsx index 65b9cf9..6d1a565 100644 --- a/frontend/src/components/ContactInfoPane.tsx +++ b/frontend/src/components/ContactInfoPane.tsx @@ -23,6 +23,13 @@ const CONTACT_TYPE_LABELS: Record = { 4: 'Sensor', }; +function formatPathHashMode(mode: number): string | null { + if (mode < 0 || mode > 2) { + return null; + } + return `${mode + 1}-byte IDs`; +} + interface ContactInfoPaneProps { contactKey: string | null; fromChannel?: boolean; @@ -99,6 +106,7 @@ export function ContactInfoPane({ isValidLocation(contact.lat, contact.lon) ? calculateDistance(config.lat, config.lon, contact.lat, contact.lon) : null; + const pathHashModeLabel = contact ? formatPathHashMode(contact.out_path_hash_mode) : null; return ( !open && onClose()}> @@ -223,6 +231,7 @@ export function ContactInfoPane({ /> )} {contact.last_path_len === -1 && } + {pathHashModeLabel && } diff --git a/frontend/src/test/contactInfoPane.test.tsx b/frontend/src/test/contactInfoPane.test.tsx new file mode 100644 index 0000000..fadb029 --- /dev/null +++ b/frontend/src/test/contactInfoPane.test.tsx @@ -0,0 +1,109 @@ +import { render, screen, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; + +import { ContactInfoPane } from '../components/ContactInfoPane'; +import type { Contact, ContactDetail } from '../types'; + +const { getContactDetail } = vi.hoisted(() => ({ + getContactDetail: vi.fn(), +})); + +vi.mock('../api', () => ({ + api: { + getContactDetail, + }, +})); + +vi.mock('../components/ui/sheet', () => ({ + Sheet: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, + SheetDescription: ({ children }: { children: React.ReactNode }) =>
{children}
, +})); + +vi.mock('../components/ContactAvatar', () => ({ + ContactAvatar: () =>
, +})); + +vi.mock('../components/ui/sonner', () => ({ + toast: { + error: vi.fn(), + success: vi.fn(), + }, +})); + +function createContact(overrides: Partial = {}): Contact { + return { + public_key: 'AA'.repeat(32), + name: 'Alice', + type: 1, + flags: 0, + last_path: null, + last_path_len: 0, + out_path_hash_mode: 0, + last_advert: null, + lat: null, + lon: null, + last_seen: 1700000000, + on_radio: false, + last_contacted: null, + last_read_at: null, + first_seen: 1699990000, + ...overrides, + }; +} + +function createDetail(contact: Contact): ContactDetail { + return { + contact, + name_history: [], + dm_message_count: 0, + channel_message_count: 0, + most_active_rooms: [], + advert_paths: [], + advert_frequency: null, + nearest_repeaters: [], + }; +} + +const baseProps = { + fromChannel: false, + onClose: () => {}, + contacts: [] as Contact[], + config: null, + favorites: [], + onToggleFavorite: () => {}, +}; + +describe('ContactInfoPane', () => { + beforeEach(() => { + getContactDetail.mockReset(); + }); + + it('shows hop width when contact has a stored path hash mode', async () => { + const contact = createContact({ out_path_hash_mode: 1 }); + getContactDetail.mockResolvedValue(createDetail(contact)); + + render(); + + await screen.findByText('Alice'); + await waitFor(() => { + expect(screen.getByText('Hop Width')).toBeInTheDocument(); + expect(screen.getByText('2-byte IDs')).toBeInTheDocument(); + }); + }); + + it('does not show hop width for flood-routed contacts', async () => { + const contact = createContact({ last_path_len: -1, out_path_hash_mode: -1 }); + getContactDetail.mockResolvedValue(createDetail(contact)); + + render(); + + await screen.findByText('Alice'); + await waitFor(() => { + expect(screen.queryByText('Hop Width')).not.toBeInTheDocument(); + expect(screen.getByText('Flood')).toBeInTheDocument(); + }); + }); +});