mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Improve test coverage
This commit is contained in:
@@ -318,7 +318,11 @@ export function Sidebar({
|
||||
(item: FavoriteItem) =>
|
||||
item.type === 'channel'
|
||||
? item.channel.name
|
||||
: getContactDisplayName(item.contact.name, item.contact.public_key, item.contact.last_advert),
|
||||
: getContactDisplayName(
|
||||
item.contact.name,
|
||||
item.contact.public_key,
|
||||
item.contact.last_advert
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -504,8 +508,6 @@ export function Sidebar({
|
||||
filteredNonRepeaterContacts,
|
||||
filteredRepeaters,
|
||||
favorites,
|
||||
getContactRecentTime,
|
||||
getLastMessageTime,
|
||||
sectionSortOrders.favorites,
|
||||
sortFavoriteItemsByOrder,
|
||||
]);
|
||||
|
||||
51
frontend/src/test/rawPacketDetailModal.test.tsx
Normal file
51
frontend/src/test/rawPacketDetailModal.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { RawPacketDetailModal } from '../components/RawPacketDetailModal';
|
||||
import type { Channel, RawPacket } from '../types';
|
||||
|
||||
const BOT_CHANNEL: Channel = {
|
||||
key: 'eb50a1bcb3e4e5d7bf69a57c9dada211',
|
||||
name: '#bot',
|
||||
is_hashtag: true,
|
||||
on_radio: false,
|
||||
last_read_at: null,
|
||||
};
|
||||
|
||||
const BOT_PACKET: RawPacket = {
|
||||
id: 1,
|
||||
observation_id: 10,
|
||||
timestamp: 1_700_000_000,
|
||||
data: '15833fa002860ccae0eed9ca78b9ab0775d477c1f6490a398bf4edc75240',
|
||||
decrypted: false,
|
||||
payload_type: 'GroupText',
|
||||
rssi: -72,
|
||||
snr: 5.5,
|
||||
decrypted_info: null,
|
||||
};
|
||||
|
||||
describe('RawPacketDetailModal', () => {
|
||||
it('renders path hops as nowrap arrow-delimited groups and links hover state to the full packet hex', () => {
|
||||
render(<RawPacketDetailModal packet={BOT_PACKET} channels={[BOT_CHANNEL]} onClose={vi.fn()} />);
|
||||
|
||||
const pathDescription = screen.getByText(
|
||||
'Historical route taken (3-byte hashes added as packet floods through network)'
|
||||
);
|
||||
const pathFieldBox = pathDescription.closest('[class*="rounded-lg"]');
|
||||
expect(pathFieldBox).not.toBeNull();
|
||||
|
||||
const pathField = within(pathFieldBox as HTMLElement);
|
||||
expect(pathField.getByText('3FA002 →')).toHaveClass('whitespace-nowrap');
|
||||
expect(pathField.getByText('860CCA →')).toHaveClass('whitespace-nowrap');
|
||||
expect(pathField.getByText('E0EED9')).toHaveClass('whitespace-nowrap');
|
||||
|
||||
const pathRun = screen.getByText('3F A0 02 86 0C CA E0 EE D9');
|
||||
const idleClassName = pathRun.className;
|
||||
|
||||
fireEvent.mouseEnter(pathFieldBox as HTMLElement);
|
||||
expect(pathRun.className).not.toBe(idleClassName);
|
||||
|
||||
fireEvent.mouseLeave(pathFieldBox as HTMLElement);
|
||||
expect(pathRun.className).toBe(idleClassName);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { SettingsModal } from '../components/SettingsModal';
|
||||
import type {
|
||||
@@ -177,6 +177,10 @@ function openDatabaseSection() {
|
||||
}
|
||||
|
||||
describe('SettingsModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(api, 'getFanoutConfigs').mockResolvedValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
localStorage.clear();
|
||||
@@ -365,17 +369,21 @@ describe('SettingsModal', () => {
|
||||
desktopSection: 'fanout',
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api.getFanoutConfigs).toHaveBeenCalled();
|
||||
});
|
||||
expect(screen.getByRole('button', { name: 'Add Integration' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Local Configuration/i })).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Preset')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not clip the fanout add-integration menu in external desktop mode', () => {
|
||||
it('does not clip the fanout add-integration menu in external desktop mode', async () => {
|
||||
renderModal({
|
||||
externalSidebarNav: true,
|
||||
desktopSection: 'fanout',
|
||||
});
|
||||
|
||||
const addIntegrationButton = screen.getByRole('button', { name: 'Add Integration' });
|
||||
const addIntegrationButton = await screen.findByRole('button', { name: 'Add Integration' });
|
||||
const wrapperSection = addIntegrationButton.closest('section');
|
||||
expect(wrapperSection).not.toHaveClass('overflow-hidden');
|
||||
});
|
||||
@@ -428,30 +436,35 @@ describe('SettingsModal', () => {
|
||||
expect(screen.getByText('Save failed')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
view.rerender(
|
||||
<SettingsModal
|
||||
open
|
||||
externalSidebarNav
|
||||
desktopSection="fanout"
|
||||
config={baseConfig}
|
||||
health={baseHealth}
|
||||
appSettings={baseSettings}
|
||||
onClose={vi.fn()}
|
||||
onSave={vi.fn(async () => {})}
|
||||
onSaveAppSettings={onSaveAppSettings}
|
||||
onSetPrivateKey={vi.fn(async () => {})}
|
||||
onReboot={vi.fn(async () => {})}
|
||||
onDisconnect={vi.fn(async () => {})}
|
||||
onReconnect={vi.fn(async () => {})}
|
||||
onAdvertise={vi.fn(async () => {})}
|
||||
meshDiscovery={null}
|
||||
meshDiscoveryLoadingTarget={null}
|
||||
onDiscoverMesh={vi.fn(async () => {})}
|
||||
onHealthRefresh={vi.fn(async () => {})}
|
||||
onRefreshAppSettings={vi.fn(async () => {})}
|
||||
/>
|
||||
);
|
||||
await act(async () => {
|
||||
view.rerender(
|
||||
<SettingsModal
|
||||
open
|
||||
externalSidebarNav
|
||||
desktopSection="fanout"
|
||||
config={baseConfig}
|
||||
health={baseHealth}
|
||||
appSettings={baseSettings}
|
||||
onClose={vi.fn()}
|
||||
onSave={vi.fn(async () => {})}
|
||||
onSaveAppSettings={onSaveAppSettings}
|
||||
onSetPrivateKey={vi.fn(async () => {})}
|
||||
onReboot={vi.fn(async () => {})}
|
||||
onDisconnect={vi.fn(async () => {})}
|
||||
onReconnect={vi.fn(async () => {})}
|
||||
onAdvertise={vi.fn(async () => {})}
|
||||
meshDiscovery={null}
|
||||
meshDiscoveryLoadingTarget={null}
|
||||
onDiscoverMesh={vi.fn(async () => {})}
|
||||
onHealthRefresh={vi.fn(async () => {})}
|
||||
onRefreshAppSettings={vi.fn(async () => {})}
|
||||
/>
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(api.getFanoutConfigs).toHaveBeenCalled();
|
||||
expect(screen.getByRole('button', { name: 'Add Integration' })).toBeInTheDocument();
|
||||
expect(screen.queryByText('Save failed')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -514,4 +514,43 @@ describe('Sidebar section summaries', () => {
|
||||
|
||||
expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
|
||||
});
|
||||
|
||||
it('seeds favorites sort from the legacy global sort order when section prefs are missing', () => {
|
||||
localStorage.setItem('remoteterm-sortOrder', 'alpha');
|
||||
|
||||
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
||||
const zed = makeContact('11'.repeat(32), 'Zed', 1, { last_advert: 150 });
|
||||
const amy = makeContact('22'.repeat(32), 'Amy');
|
||||
|
||||
render(
|
||||
<Sidebar
|
||||
contacts={[zed, amy]}
|
||||
channels={[publicChannel]}
|
||||
activeConversation={null}
|
||||
onSelectConversation={vi.fn()}
|
||||
onNewMessage={vi.fn()}
|
||||
lastMessageTimes={{
|
||||
[getStateKey('contact', zed.public_key)]: 200,
|
||||
}}
|
||||
unreadCounts={{}}
|
||||
mentions={{}}
|
||||
showCracker={false}
|
||||
crackerRunning={false}
|
||||
onToggleCracker={vi.fn()}
|
||||
onMarkAllRead={vi.fn()}
|
||||
favorites={[
|
||||
{ type: 'contact', id: zed.public_key },
|
||||
{ type: 'contact', id: amy.public_key },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const favoriteRows = screen
|
||||
.getAllByText(/^(Amy|Zed)$/)
|
||||
.map((node) => node.textContent)
|
||||
.filter((text): text is string => Boolean(text));
|
||||
|
||||
expect(favoriteRows).toEqual(['Amy', 'Zed']);
|
||||
expect(screen.getByRole('button', { name: 'Sort Favorites by recent' })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -217,7 +217,9 @@ describe('useConversationMessages conversation switch', () => {
|
||||
|
||||
// Switch to conv B while older-messages fetch is still pending
|
||||
mockGetMessages.mockResolvedValueOnce([createMessage({ id: 999, conversation_key: 'conv_b' })]);
|
||||
rerender({ conv: convB });
|
||||
await act(async () => {
|
||||
rerender({ conv: convB });
|
||||
});
|
||||
|
||||
// loadingOlder must reset immediately — no phantom spinner in conv B
|
||||
await waitFor(() => expect(result.current.loadingOlder).toBe(false));
|
||||
@@ -226,12 +228,13 @@ describe('useConversationMessages conversation switch', () => {
|
||||
expect(result.current.messages[0].conversation_key).toBe('conv_b');
|
||||
|
||||
// Resolve the stale older-messages fetch — should not affect conv B's state
|
||||
olderDeferred.resolve([
|
||||
createMessage({ id: 500, conversation_key: 'conv_a', text: 'stale-old' }),
|
||||
]);
|
||||
await act(async () => {
|
||||
olderDeferred.resolve([
|
||||
createMessage({ id: 500, conversation_key: 'conv_a', text: 'stale-old' }),
|
||||
]);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Give the stale response time to be processed (it should be discarded)
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
expect(result.current.messages).toHaveLength(1);
|
||||
expect(result.current.messages[0].conversation_key).toBe('conv_b');
|
||||
});
|
||||
|
||||
@@ -4,9 +4,12 @@ Verifies that the primary RF packet entry point correctly extracts hex payload,
|
||||
SNR, and RSSI from MeshCore events and passes them to process_raw_packet.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from meshcore import EventType
|
||||
from meshcore.packets import PacketType
|
||||
from meshcore.reader import MessageReader
|
||||
|
||||
|
||||
class TestOnRxLogData:
|
||||
@@ -90,3 +93,42 @@ class TestOnRxLogData:
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await on_rx_log_data(MockEvent())
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_real_meshcore_reader_forwards_3byte_log_data_to_handler(self):
|
||||
"""The meshcore reader emits usable RX_LOG_DATA for 3-byte-hop packets."""
|
||||
from app.event_handlers import on_rx_log_data
|
||||
|
||||
payload_hex = "15833fa002860ccae0eed9ca78b9ab0775d477c1f6490a398bf4edc75240"
|
||||
dispatcher = MagicMock()
|
||||
dispatcher.dispatch = AsyncMock()
|
||||
reader = MessageReader(dispatcher)
|
||||
|
||||
frame = bytes(
|
||||
[
|
||||
PacketType.LOG_DATA.value,
|
||||
int(7.5 * 4),
|
||||
(-85) & 0xFF,
|
||||
]
|
||||
) + bytes.fromhex(payload_hex)
|
||||
|
||||
await reader.handle_rx(bytearray(frame))
|
||||
|
||||
dispatcher.dispatch.assert_awaited_once()
|
||||
event = dispatcher.dispatch.await_args.args[0]
|
||||
assert event.type == EventType.RX_LOG_DATA
|
||||
assert event.payload["payload"] == payload_hex.lower()
|
||||
assert event.payload["path_hash_size"] == 3
|
||||
assert event.payload["path_len"] == 3
|
||||
assert event.payload["path"] == "3fa002860ccae0eed9"
|
||||
assert event.payload["snr"] == 7.5
|
||||
assert event.payload["rssi"] == -85
|
||||
|
||||
with patch("app.event_handlers.process_raw_packet", new_callable=AsyncMock) as mock_process:
|
||||
await on_rx_log_data(event)
|
||||
|
||||
mock_process.assert_called_once_with(
|
||||
raw_bytes=bytes.fromhex(payload_hex),
|
||||
snr=7.5,
|
||||
rssi=-85,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user