mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-06 08:43:36 +02:00
Use better behavior on disconnected radio and allow deeplinking into settings. Closes #66.
This commit is contained in:
@@ -164,7 +164,10 @@ vi.mock('../components/ui/sonner', () => ({
|
||||
|
||||
vi.mock('../utils/urlHash', () => ({
|
||||
parseHashConversation: () => null,
|
||||
parseHashSettingsSection: () => null,
|
||||
updateUrlHash: vi.fn(),
|
||||
updateSettingsHash: vi.fn(),
|
||||
getSettingsHash: (section: string) => `#settings/${section}`,
|
||||
getMapFocusHash: () => '#map',
|
||||
}));
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@ vi.mock('../components/NewMessageModal', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../components/SettingsModal', () => ({
|
||||
SettingsModal: () => null,
|
||||
SettingsModal: ({ desktopSection }: { desktopSection?: string }) => (
|
||||
<div data-testid="settings-modal-section">{desktopSection ?? 'none'}</div>
|
||||
),
|
||||
SETTINGS_SECTION_ORDER: ['radio', 'local', 'database', 'bot'],
|
||||
SETTINGS_SECTION_LABELS: {
|
||||
radio: 'Radio',
|
||||
@@ -293,4 +295,20 @@ describe('App startup hash resolution', () => {
|
||||
});
|
||||
expect(window.location.hash).toBe('');
|
||||
});
|
||||
|
||||
it('opens settings from a settings hash and falls back away from radio when disconnected', async () => {
|
||||
window.location.hash = '#settings/radio';
|
||||
mocks.api.getRadioConfig.mockRejectedValue(new Error('radio offline'));
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('settings-modal-section')).toHaveTextContent('local');
|
||||
});
|
||||
|
||||
for (const button of screen.getAllByRole('button', { name: 'Radio' })) {
|
||||
expect(button).toBeDisabled();
|
||||
}
|
||||
expect(window.location.hash).toBe('#settings/local');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -63,6 +63,7 @@ const baseSettings: AppSettings = {
|
||||
};
|
||||
|
||||
function renderModal(overrides?: {
|
||||
config?: RadioConfig | null;
|
||||
appSettings?: AppSettings;
|
||||
health?: HealthStatus;
|
||||
onSaveAppSettings?: (update: AppSettingsUpdate) => Promise<void>;
|
||||
@@ -97,7 +98,7 @@ function renderModal(overrides?: {
|
||||
const commonProps = {
|
||||
open: overrides?.open ?? true,
|
||||
pageMode: overrides?.pageMode,
|
||||
config: baseConfig,
|
||||
config: overrides?.config === undefined ? baseConfig : overrides.config,
|
||||
health: overrides?.health ?? baseHealth,
|
||||
appSettings: overrides?.appSettings ?? baseSettings,
|
||||
onClose,
|
||||
@@ -205,6 +206,32 @@ describe('SettingsModal', () => {
|
||||
expect(screen.getByText(/Configured radio contact capacity/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps non-radio settings available when radio config is unavailable', () => {
|
||||
renderModal({ config: null });
|
||||
|
||||
const radioToggle = screen.getByRole('button', { name: /Radio/i });
|
||||
expect(radioToggle).toBeDisabled();
|
||||
|
||||
openLocalSection();
|
||||
expect(screen.getByLabelText('Local label text')).toBeInTheDocument();
|
||||
|
||||
openDatabaseSection();
|
||||
expect(screen.getByText('Delete Undecrypted Packets')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a radio-unavailable message instead of blocking the whole settings page', () => {
|
||||
renderModal({
|
||||
config: null,
|
||||
externalSidebarNav: true,
|
||||
desktopSection: 'radio',
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText('Radio settings are unavailable until a radio connects.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Loading configuration...')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows cached radio firmware and capacity info under the connection status', () => {
|
||||
renderModal({
|
||||
health: {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
parseHashConversation,
|
||||
parseHashSettingsSection,
|
||||
getSettingsHash,
|
||||
getMapFocusHash,
|
||||
resolveChannelFromHashToken,
|
||||
resolveContactFromHashToken,
|
||||
@@ -147,6 +149,34 @@ describe('parseHashConversation', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('settings URL hashes', () => {
|
||||
let originalHash: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalHash = window.location.hash;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.location.hash = originalHash;
|
||||
});
|
||||
|
||||
it('parses a valid settings section hash', () => {
|
||||
window.location.hash = '#settings/database';
|
||||
|
||||
expect(parseHashSettingsSection()).toBe('database');
|
||||
});
|
||||
|
||||
it('returns null for an invalid settings section hash', () => {
|
||||
window.location.hash = '#settings/not-a-section';
|
||||
|
||||
expect(parseHashSettingsSection()).toBeNull();
|
||||
});
|
||||
|
||||
it('builds a stable settings hash', () => {
|
||||
expect(getSettingsHash('local')).toBe('#settings/local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveChannelFromHashToken', () => {
|
||||
const channels: Channel[] = [
|
||||
{
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { useAppShell } from '../hooks/useAppShell';
|
||||
|
||||
describe('useAppShell', () => {
|
||||
let originalHash: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalHash = window.location.hash;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.location.hash = originalHash;
|
||||
});
|
||||
|
||||
it('opens new-message modal and closes the sidebar', () => {
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
|
||||
@@ -34,6 +44,55 @@ describe('useAppShell', () => {
|
||||
expect(result.current.showSettings).toBe(false);
|
||||
});
|
||||
|
||||
it('initializes settings mode from the URL hash', () => {
|
||||
window.location.hash = '#settings/database';
|
||||
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
|
||||
expect(result.current.showSettings).toBe(true);
|
||||
expect(result.current.settingsSection).toBe('database');
|
||||
});
|
||||
|
||||
it('syncs the selected settings section into the URL hash', async () => {
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
|
||||
act(() => {
|
||||
result.current.handleToggleSettingsView();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#settings/radio');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setSettingsSection('fanout');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#settings/fanout');
|
||||
});
|
||||
});
|
||||
|
||||
it('restores the previous hash when settings close', async () => {
|
||||
window.location.hash = '#channel/test/Public';
|
||||
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
|
||||
act(() => {
|
||||
result.current.handleToggleSettingsView();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#settings/radio');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.handleCloseSettingsView();
|
||||
});
|
||||
|
||||
expect(window.location.hash).toBe('#channel/test/Public');
|
||||
});
|
||||
|
||||
it('toggles the cracker shell without affecting sidebar state', () => {
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user