aprs.me/test/assets/js/map_helpers_test.ts
Graham McIntire cf78298f46
Fix trail lines not connecting historical positions
When changing trail duration and historical data settings, historical positions
would show up on the map but trail lines wouldn't connect them. This was caused
by a mismatch between trail ID generation and callsign extraction.

## Changes

- Fix getTrailId() to properly extract base callsigns from historical marker IDs
- Add setTrailDuration() method to TrailManager for dynamic duration updates
- Add LiveView event to synchronize trail duration changes with client
- Add comprehensive tests for historical ID handling

## Problem
Historical markers have IDs like "hist_CALLSIGN_123" but getTrailId() was
returning the full ID while TrailManager.extractBaseCallsign() extracted just
"CALLSIGN", creating separate trails instead of connected ones.

## Solution
Updated getTrailId() to extract base callsigns from historical IDs using the
same logic as TrailManager, ensuring all positions from the same station use
the same trail ID and connect properly.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 08:31:16 -05:00

216 lines
No EOL
6.3 KiB
TypeScript

import { describe, test, expect, beforeEach, afterEach, vi } from 'vitest';
import {
parseTimestamp,
getTrailId,
saveMapState,
safePushEvent,
isLiveViewConnected,
getLiveSocket
} from '../../../assets/js/map_helpers';
describe('map_helpers', () => {
describe('parseTimestamp', () => {
test('returns current time for null/undefined', () => {
const before = Date.now();
const result = parseTimestamp(null);
const after = Date.now();
expect(result).toBeGreaterThanOrEqual(before);
expect(result).toBeLessThanOrEqual(after);
});
test('returns number timestamp as-is', () => {
const timestamp = 1234567890;
expect(parseTimestamp(timestamp)).toBe(timestamp);
});
test('parses string timestamp', () => {
const dateStr = '2024-01-01T00:00:00Z';
const expected = new Date(dateStr).getTime();
expect(parseTimestamp(dateStr)).toBe(expected);
});
test('returns current time for invalid input', () => {
const before = Date.now();
const result = parseTimestamp({});
const after = Date.now();
expect(result).toBeGreaterThanOrEqual(before);
expect(result).toBeLessThanOrEqual(after);
});
});
describe('getTrailId', () => {
test('prioritizes callsign_group', () => {
const data = {
callsign_group: 'GROUP-1',
callsign: 'CALL-1',
id: 'ID-1'
};
expect(getTrailId(data)).toBe('GROUP-1');
});
test('falls back to callsign when no callsign_group', () => {
const data = {
callsign: 'CALL-1',
id: 'ID-1'
};
expect(getTrailId(data)).toBe('CALL-1');
});
test('falls back to id when no callsign_group or callsign', () => {
const data = {
id: 'ID-1'
};
expect(getTrailId(data)).toBe('ID-1');
});
test('extracts base callsign from historical ID format', () => {
const data = {
id: 'hist_MYCALL_123'
};
expect(getTrailId(data)).toBe('MYCALL');
});
test('handles historical ID with complex callsign', () => {
const data = {
id: 'hist_KD8ABC-9_456'
};
expect(getTrailId(data)).toBe('KD8ABC-9');
});
test('prioritizes callsign_group over historical ID extraction', () => {
const data = {
callsign_group: 'GROUP-1',
id: 'hist_MYCALL_123'
};
expect(getTrailId(data)).toBe('GROUP-1');
});
test('prioritizes callsign over historical ID extraction', () => {
const data = {
callsign: 'REALCALL',
id: 'hist_MYCALL_123'
};
expect(getTrailId(data)).toBe('REALCALL');
});
});
describe('saveMapState', () => {
let mockMap: any;
let mockPushEvent: any;
beforeEach(() => {
// Mock localStorage
const localStorageMock = {
setItem: vi.fn()
};
Object.defineProperty(window, 'localStorage', {
value: localStorageMock,
writable: true
});
// Mock map object
mockMap = {
getCenter: vi.fn().mockReturnValue({ lat: 40.7128, lng: -74.0060 }),
getZoom: vi.fn().mockReturnValue(10),
getBounds: vi.fn().mockReturnValue({
getNorth: vi.fn().mockReturnValue(41.0),
getSouth: vi.fn().mockReturnValue(40.0),
getEast: vi.fn().mockReturnValue(-73.0),
getWest: vi.fn().mockReturnValue(-75.0)
})
};
mockPushEvent = vi.fn();
});
test('saves truncated coordinates to localStorage', () => {
saveMapState(mockMap, mockPushEvent);
expect(localStorage.setItem).toHaveBeenCalledWith(
'aprs_map_state',
JSON.stringify({ lat: 40.7128, lng: -74.006, zoom: 10 })
);
});
test('pushes event with map state and bounds', () => {
saveMapState(mockMap, mockPushEvent);
expect(mockPushEvent).toHaveBeenCalledWith('update_map_state', {
center: { lat: 40.7128, lng: -74.006 },
zoom: 10,
bounds: {
north: 41.0,
south: 40.0,
east: -73.0,
west: -75.0
}
});
});
test('truncates coordinates to 5 decimal places', () => {
mockMap.getCenter.mockReturnValue({ lat: 40.71281234567, lng: -74.00601234567 });
saveMapState(mockMap, mockPushEvent);
const call = mockPushEvent.mock.calls[0][1];
expect(call.center.lat).toBe(40.71281);
expect(call.center.lng).toBe(-74.00601);
});
});
describe('safePushEvent', () => {
test('calls pushEvent and returns true on success', () => {
const mockPushEvent = vi.fn();
const result = safePushEvent(mockPushEvent, 'test_event', { data: 'test' });
expect(mockPushEvent).toHaveBeenCalledWith('test_event', { data: 'test' });
expect(result).toBe(true);
});
test('returns false when pushEvent is undefined', () => {
const result = safePushEvent(undefined, 'test_event', { data: 'test' });
expect(result).toBe(false);
});
test('catches error and returns false', () => {
const mockPushEvent = vi.fn().mockImplementation(() => {
throw new Error('LiveView not connected');
});
const consoleSpy = vi.spyOn(console, 'debug').mockImplementation(() => {});
const result = safePushEvent(mockPushEvent, 'test_event', { data: 'test' });
expect(result).toBe(false);
expect(consoleSpy).toHaveBeenCalledWith('Unable to send test_event event - LiveView disconnected');
consoleSpy.mockRestore();
});
});
describe('LiveView socket helpers', () => {
afterEach(() => {
// Clean up window.liveSocket
delete (window as any).liveSocket;
});
test('isLiveViewConnected returns true when socket exists', () => {
(window as any).liveSocket = { connected: true };
expect(isLiveViewConnected()).toBe(true);
});
test('isLiveViewConnected returns false when socket missing', () => {
expect(isLiveViewConnected()).toBe(false);
});
test('getLiveSocket returns the socket', () => {
const mockSocket = { connected: true, pushHistoryPatch: vi.fn() };
(window as any).liveSocket = mockSocket;
expect(getLiveSocket()).toBe(mockSocket);
});
test('getLiveSocket returns undefined when missing', () => {
expect(getLiveSocket()).toBeUndefined();
});
});
});