mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-04 16:02:29 +02:00
Make repeater neighbor display need a GPS fix to show map + distance, and fetch before display. Closes #58.
This commit is contained in:
@@ -13,6 +13,7 @@ const mockHook: {
|
||||
loginError: null,
|
||||
paneData: {
|
||||
status: null,
|
||||
nodeInfo: null,
|
||||
neighbors: null,
|
||||
acl: null,
|
||||
radioSettings: null,
|
||||
@@ -23,6 +24,7 @@ const mockHook: {
|
||||
},
|
||||
paneStates: {
|
||||
status: { loading: false, attempt: 0, error: null },
|
||||
nodeInfo: { loading: false, attempt: 0, error: null },
|
||||
neighbors: { loading: false, attempt: 0, error: null },
|
||||
acl: { loading: false, attempt: 0, error: null },
|
||||
radioSettings: { loading: false, attempt: 0, error: null },
|
||||
@@ -63,6 +65,7 @@ vi.mock('react-leaflet', () => ({
|
||||
TileLayer: () => null,
|
||||
CircleMarker: () => null,
|
||||
Popup: () => null,
|
||||
Polyline: () => null,
|
||||
}));
|
||||
|
||||
const REPEATER_KEY = 'aa'.repeat(32);
|
||||
@@ -120,6 +123,7 @@ describe('RepeaterDashboard', () => {
|
||||
mockHook.loginError = null;
|
||||
mockHook.paneData = {
|
||||
status: null,
|
||||
nodeInfo: null,
|
||||
neighbors: null,
|
||||
acl: null,
|
||||
radioSettings: null,
|
||||
@@ -130,6 +134,7 @@ describe('RepeaterDashboard', () => {
|
||||
};
|
||||
mockHook.paneStates = {
|
||||
status: { loading: false, attempt: 0, error: null },
|
||||
nodeInfo: { loading: false, attempt: 0, error: null },
|
||||
neighbors: { loading: false, attempt: 0, error: null },
|
||||
acl: { loading: false, attempt: 0, error: null },
|
||||
radioSettings: { loading: false, attempt: 0, error: null },
|
||||
@@ -157,6 +162,7 @@ describe('RepeaterDashboard', () => {
|
||||
render(<RepeaterDashboard {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Telemetry')).toBeInTheDocument();
|
||||
expect(screen.getByText('Node Info')).toBeInTheDocument();
|
||||
expect(screen.getByText('Neighbors')).toBeInTheDocument();
|
||||
expect(screen.getByText('ACL')).toBeInTheDocument();
|
||||
expect(screen.getByText('Radio Settings')).toBeInTheDocument();
|
||||
@@ -226,6 +232,102 @@ describe('RepeaterDashboard', () => {
|
||||
expect(screen.getByText('Timeout')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows GPS unavailable message for neighbors when repeater coords are missing', () => {
|
||||
mockHook.loggedIn = true;
|
||||
mockHook.paneData.neighbors = {
|
||||
neighbors: [
|
||||
{ pubkey_prefix: 'bbbbbbbbbbbb', name: 'Neighbor', snr: 7.2, last_heard_seconds: 9 },
|
||||
],
|
||||
};
|
||||
mockHook.paneData.nodeInfo = {
|
||||
name: 'TestRepeater',
|
||||
lat: '0',
|
||||
lon: '0',
|
||||
clock_utc: null,
|
||||
};
|
||||
mockHook.paneStates.neighbors = {
|
||||
loading: false,
|
||||
attempt: 1,
|
||||
error: null,
|
||||
fetched_at: Date.now(),
|
||||
};
|
||||
mockHook.paneStates.nodeInfo = {
|
||||
loading: false,
|
||||
attempt: 1,
|
||||
error: null,
|
||||
fetched_at: Date.now(),
|
||||
};
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} />);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'GPS info failed to fetch; map and distance data not available. This may be due to missing or zero-zero GPS data on the repeater, or due to transient fetch failure. Try refreshing.'
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Dist')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows neighbor distance when repeater radio settings include valid coords', () => {
|
||||
mockHook.loggedIn = true;
|
||||
mockHook.paneData.neighbors = {
|
||||
neighbors: [
|
||||
{ pubkey_prefix: 'bbbbbbbbbbbb', name: 'Neighbor', snr: 7.2, last_heard_seconds: 9 },
|
||||
],
|
||||
};
|
||||
mockHook.paneData.nodeInfo = {
|
||||
name: 'TestRepeater',
|
||||
lat: '-31.9500',
|
||||
lon: '115.8600',
|
||||
clock_utc: null,
|
||||
};
|
||||
mockHook.paneStates.neighbors = {
|
||||
loading: false,
|
||||
attempt: 1,
|
||||
error: null,
|
||||
fetched_at: Date.now(),
|
||||
};
|
||||
mockHook.paneStates.nodeInfo = {
|
||||
loading: false,
|
||||
attempt: 1,
|
||||
error: null,
|
||||
fetched_at: Date.now(),
|
||||
};
|
||||
|
||||
const contactsWithNeighbor = [
|
||||
...contacts,
|
||||
{
|
||||
public_key: 'bbbbbbbbbbbb0000000000000000000000000000000000000000000000000000',
|
||||
name: 'Neighbor',
|
||||
type: 1,
|
||||
flags: 0,
|
||||
last_path: null,
|
||||
last_path_len: 0,
|
||||
out_path_hash_mode: 0,
|
||||
route_override_path: null,
|
||||
route_override_len: null,
|
||||
route_override_hash_mode: null,
|
||||
last_advert: null,
|
||||
lat: -31.94,
|
||||
lon: 115.87,
|
||||
last_seen: null,
|
||||
on_radio: false,
|
||||
last_contacted: null,
|
||||
last_read_at: null,
|
||||
first_seen: null,
|
||||
},
|
||||
];
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} contacts={contactsWithNeighbor} />);
|
||||
|
||||
expect(screen.getByText('Dist')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'GPS info failed to fetch; map and distance data not available. This may be due to missing or zero-zero GPS data on the repeater, or due to transient fetch failure. Try refreshing.'
|
||||
)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows fetching state with attempt counter', () => {
|
||||
mockHook.loggedIn = true;
|
||||
mockHook.paneStates.status = { loading: true, attempt: 2, error: null };
|
||||
@@ -264,6 +366,24 @@ describe('RepeaterDashboard', () => {
|
||||
expect(screen.getByText('7.5 dB')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats the radio tuple and preserves the raw tuple in a tooltip', () => {
|
||||
mockHook.loggedIn = true;
|
||||
mockHook.paneData.radioSettings = {
|
||||
firmware_version: 'v1.0',
|
||||
radio: '910.5250244,62.5,7,5',
|
||||
tx_power: '20',
|
||||
airtime_factor: '0',
|
||||
repeat_enabled: '1',
|
||||
flood_max: '3',
|
||||
};
|
||||
|
||||
render(<RepeaterDashboard {...defaultProps} />);
|
||||
|
||||
const formatted = screen.getByText('910.525 MHz, BW 62.5 kHz, SF7, CR5');
|
||||
expect(formatted).toBeInTheDocument();
|
||||
expect(formatted).toHaveAttribute('title', '910.5250244,62.5,7,5');
|
||||
});
|
||||
|
||||
it('shows fetched time and relative age when pane data has been loaded', () => {
|
||||
mockHook.loggedIn = true;
|
||||
mockHook.paneStates.status = {
|
||||
|
||||
@@ -12,6 +12,7 @@ vi.mock('../api', () => ({
|
||||
api: {
|
||||
repeaterLogin: vi.fn(),
|
||||
repeaterStatus: vi.fn(),
|
||||
repeaterNodeInfo: vi.fn(),
|
||||
repeaterNeighbors: vi.fn(),
|
||||
repeaterAcl: vi.fn(),
|
||||
repeaterRadioSettings: vi.fn(),
|
||||
@@ -284,8 +285,12 @@ describe('useRepeaterDashboard', () => {
|
||||
|
||||
it('loadAll calls refreshPane for all panes serially', async () => {
|
||||
mockApi.repeaterStatus.mockResolvedValueOnce({ battery_volts: 4.0 });
|
||||
mockApi.repeaterNeighbors.mockResolvedValueOnce({ neighbors: [] });
|
||||
mockApi.repeaterAcl.mockResolvedValueOnce({ acl: [] });
|
||||
mockApi.repeaterNodeInfo.mockResolvedValueOnce({
|
||||
name: null,
|
||||
lat: null,
|
||||
lon: null,
|
||||
clock_utc: null,
|
||||
});
|
||||
mockApi.repeaterRadioSettings.mockResolvedValueOnce({
|
||||
firmware_version: 'v1.0',
|
||||
radio: null,
|
||||
@@ -293,11 +298,9 @@ describe('useRepeaterDashboard', () => {
|
||||
airtime_factor: null,
|
||||
repeat_enabled: null,
|
||||
flood_max: null,
|
||||
name: null,
|
||||
lat: null,
|
||||
lon: null,
|
||||
clock_utc: null,
|
||||
});
|
||||
mockApi.repeaterNeighbors.mockResolvedValueOnce({ neighbors: [] });
|
||||
mockApi.repeaterAcl.mockResolvedValueOnce({ acl: [] });
|
||||
mockApi.repeaterAdvertIntervals.mockResolvedValueOnce({
|
||||
advert_interval: null,
|
||||
flood_advert_interval: null,
|
||||
@@ -315,6 +318,7 @@ describe('useRepeaterDashboard', () => {
|
||||
});
|
||||
|
||||
expect(mockApi.repeaterStatus).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterNodeInfo).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterNeighbors).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterAcl).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterRadioSettings).toHaveBeenCalledTimes(1);
|
||||
@@ -323,6 +327,53 @@ describe('useRepeaterDashboard', () => {
|
||||
expect(mockApi.repeaterLppTelemetry).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('refreshing neighbors fetches node info first', async () => {
|
||||
mockApi.repeaterNodeInfo.mockResolvedValueOnce({
|
||||
name: 'Repeater',
|
||||
lat: '-31.9523',
|
||||
lon: '115.8613',
|
||||
clock_utc: null,
|
||||
});
|
||||
mockApi.repeaterNeighbors.mockResolvedValueOnce({ neighbors: [] });
|
||||
|
||||
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshPane('neighbors');
|
||||
});
|
||||
|
||||
expect(mockApi.repeaterNodeInfo).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterNeighbors).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterNodeInfo.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
mockApi.repeaterNeighbors.mock.invocationCallOrder[0]
|
||||
);
|
||||
expect(result.current.paneData.nodeInfo?.lat).toBe('-31.9523');
|
||||
expect(result.current.paneData.neighbors).toEqual({ neighbors: [] });
|
||||
});
|
||||
|
||||
it('refreshing neighbors reuses already-fetched node info', async () => {
|
||||
mockApi.repeaterNodeInfo.mockResolvedValueOnce({
|
||||
name: 'Repeater',
|
||||
lat: '-31.9523',
|
||||
lon: '115.8613',
|
||||
clock_utc: null,
|
||||
});
|
||||
mockApi.repeaterNeighbors.mockResolvedValueOnce({ neighbors: [] });
|
||||
mockApi.repeaterNeighbors.mockResolvedValueOnce({ neighbors: [] });
|
||||
|
||||
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshPane('neighbors');
|
||||
});
|
||||
await act(async () => {
|
||||
await result.current.refreshPane('neighbors');
|
||||
});
|
||||
|
||||
expect(mockApi.repeaterNodeInfo).toHaveBeenCalledTimes(1);
|
||||
expect(mockApi.repeaterNeighbors).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('restores dashboard state when navigating away and back to the same repeater', async () => {
|
||||
const statusData = { battery_volts: 4.2 };
|
||||
mockApi.repeaterLogin.mockResolvedValueOnce({ status: 'ok' });
|
||||
|
||||
Reference in New Issue
Block a user