/** * Tests for URL hash utilities. * * These tests verify the URL hash parsing and generation * for deep linking to conversations. */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { parseHashConversation, getMapFocusHash, resolveChannelFromHashToken, resolveContactFromHashToken, } from '../utils/urlHash'; import type { Channel, Contact } from '../types'; describe('parseHashConversation', () => { let originalHash: string; beforeEach(() => { originalHash = window.location.hash; }); afterEach(() => { window.location.hash = originalHash; }); it('returns null for empty hash', () => { window.location.hash = ''; const result = parseHashConversation(); expect(result).toBeNull(); }); it('parses #raw as raw type', () => { window.location.hash = '#raw'; const result = parseHashConversation(); expect(result).toEqual({ type: 'raw', name: 'raw' }); }); it('parses #map as map type', () => { window.location.hash = '#map'; const result = parseHashConversation(); expect(result).toEqual({ type: 'map', name: 'map' }); }); it('parses #map/focus/PUBKEY with focus key', () => { window.location.hash = '#map/focus/ABCD1234'; const result = parseHashConversation(); expect(result).toEqual({ type: 'map', name: 'map', mapFocusKey: 'ABCD1234' }); }); it('parses #map/focus/ with empty focus as plain map', () => { window.location.hash = '#map/focus/'; const result = parseHashConversation(); expect(result).toEqual({ type: 'map', name: 'map' }); }); it('decodes URL-encoded map focus key', () => { window.location.hash = '#map/focus/AB%20CD'; const result = parseHashConversation(); expect(result).toEqual({ type: 'map', name: 'map', mapFocusKey: 'AB CD' }); }); it('parses channel hash', () => { window.location.hash = '#channel/ABCDEF0123456789ABCDEF0123456789'; const result = parseHashConversation(); expect(result).toEqual({ type: 'channel', name: 'ABCDEF0123456789ABCDEF0123456789' }); }); it('parses contact hash', () => { window.location.hash = '#contact/abc123def4567890abc123def4567890abc123def4567890abc123def4567890'; const result = parseHashConversation(); expect(result).toEqual({ type: 'contact', name: 'abc123def4567890abc123def4567890abc123def4567890abc123def4567890', }); }); it('parses id plus readable label and preserves id token', () => { window.location.hash = '#channel/ABCDEF0123456789ABCDEF0123456789/Public%20Room'; const result = parseHashConversation(); expect(result).toEqual({ type: 'channel', name: 'ABCDEF0123456789ABCDEF0123456789', label: 'Public Room', }); }); it('decodes URL-encoded names', () => { window.location.hash = '#contact/John%20Doe'; const result = parseHashConversation(); expect(result).toEqual({ type: 'contact', name: 'John Doe' }); }); it('returns null for invalid type', () => { window.location.hash = '#invalid/Test'; const result = parseHashConversation(); expect(result).toBeNull(); }); it('returns null for hash without slash', () => { window.location.hash = '#channelPublic'; const result = parseHashConversation(); expect(result).toBeNull(); }); it('returns null for hash with empty name', () => { window.location.hash = '#channel/'; const result = parseHashConversation(); expect(result).toBeNull(); }); it('handles channel names with special characters', () => { window.location.hash = '#channel/Test%20Channel%21'; const result = parseHashConversation(); expect(result).toEqual({ type: 'channel', name: 'Test Channel!' }); }); }); describe('resolveChannelFromHashToken', () => { const channels: Channel[] = [ { key: 'ABCDEF0123456789ABCDEF0123456789', name: 'Public', is_hashtag: false, on_radio: true, last_read_at: null, }, { key: '11111111111111111111111111111111', name: '#mesh', is_hashtag: true, on_radio: false, last_read_at: null, }, { key: '22222222222222222222222222222222', name: 'Public', is_hashtag: false, on_radio: false, last_read_at: null, }, ]; it('prefers stable key lookup (case-insensitive)', () => { const result = resolveChannelFromHashToken('abcdef0123456789abcdef0123456789', channels); expect(result?.key).toBe('ABCDEF0123456789ABCDEF0123456789'); }); it('supports legacy name-based hash lookup', () => { const result = resolveChannelFromHashToken('Public', channels); expect(result?.key).toBe('ABCDEF0123456789ABCDEF0123456789'); }); it('supports legacy hashtag hash without leading #', () => { const result = resolveChannelFromHashToken('mesh', channels); expect(result?.key).toBe('11111111111111111111111111111111'); }); }); describe('resolveContactFromHashToken', () => { const contacts: Contact[] = [ { public_key: 'abc123def4567890abc123def4567890abc123def4567890abc123def4567890', name: 'Alice', type: 1, flags: 0, last_path: null, last_path_len: -1, out_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, }, { public_key: 'def456abc1237890def456abc1237890def456abc1237890def456abc1237890', name: 'Alice', type: 1, flags: 0, last_path: null, last_path_len: -1, out_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, }, { public_key: 'eeeeee111111222222333333444444555555666666777777888888999999aaaa', name: null, type: 1, flags: 0, last_path: null, last_path_len: -1, out_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, }, ]; it('prefers stable public-key lookup (case-insensitive)', () => { const result = resolveContactFromHashToken( 'ABC123DEF4567890ABC123DEF4567890ABC123DEF4567890ABC123DEF4567890', contacts ); expect(result?.public_key).toBe( 'abc123def4567890abc123def4567890abc123def4567890abc123def4567890' ); }); it('supports legacy display-name hash lookup', () => { const result = resolveContactFromHashToken('Alice', contacts); expect(result?.public_key).toBe( 'abc123def4567890abc123def4567890abc123def4567890abc123def4567890' ); }); it('supports legacy unnamed-contact prefix hash lookup', () => { const result = resolveContactFromHashToken('eeeeee111111', contacts); expect(result?.public_key).toBe( 'eeeeee111111222222333333444444555555666666777777888888999999aaaa' ); }); }); describe('getMapFocusHash', () => { it('generates hash with focus key', () => { const result = getMapFocusHash('ABCD1234'); expect(result).toBe('#map/focus/ABCD1234'); }); it('encodes special characters in key', () => { const result = getMapFocusHash('AB CD/12'); expect(result).toBe('#map/focus/AB%20CD%2F12'); }); });