mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 01:03:34 +02:00
(feat) chain app nav to browser history state. Closes #250.
Hook browser back/forward to nav state
This commit is contained in:
@@ -192,7 +192,9 @@ vi.mock('../utils/urlHash', () => ({
|
||||
parseHashConversation: () => null,
|
||||
parseHashSettingsSection: () => null,
|
||||
updateUrlHash: vi.fn(),
|
||||
pushUrlHash: vi.fn(),
|
||||
updateSettingsHash: vi.fn(),
|
||||
pushSettingsHash: vi.fn(),
|
||||
getSettingsHash: (section: string) => `#settings/${section}`,
|
||||
getMapFocusHash: () => '#map',
|
||||
}));
|
||||
|
||||
@@ -134,6 +134,7 @@ vi.mock('../components/ui/sonner', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
import { act } from '@testing-library/react';
|
||||
import { App } from '../App';
|
||||
import {
|
||||
LAST_VIEWED_CONVERSATION_KEY,
|
||||
@@ -344,6 +345,7 @@ describe('App startup hash resolution', () => {
|
||||
flags: 0,
|
||||
direct_path: null,
|
||||
direct_path_len: -1,
|
||||
direct_path_hash_mode: 0,
|
||||
last_advert: null,
|
||||
lat: null,
|
||||
lon: null,
|
||||
@@ -377,6 +379,83 @@ describe('App startup hash resolution', () => {
|
||||
expect(window.location.hash).toBe('');
|
||||
});
|
||||
|
||||
describe('Browser back/forward navigation', () => {
|
||||
it('navigates to a channel conversation when popstate fires', async () => {
|
||||
const opsChannel = {
|
||||
key: 'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
||||
name: 'Ops',
|
||||
is_hashtag: false,
|
||||
on_radio: false,
|
||||
last_read_at: null,
|
||||
favorite: false,
|
||||
};
|
||||
|
||||
window.location.hash = `#channel/${opsChannel.key}/Ops`;
|
||||
mocks.api.getChannels.mockResolvedValue([publicChannel, opsChannel]);
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
for (const node of screen.getAllByTestId('active-conversation')) {
|
||||
expect(node).toHaveTextContent(`channel:${opsChannel.key}:Ops`);
|
||||
}
|
||||
});
|
||||
|
||||
act(() => {
|
||||
window.location.hash = `#channel/${publicChannel.key}/Public`;
|
||||
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
for (const node of screen.getAllByTestId('active-conversation')) {
|
||||
expect(node).toHaveTextContent(`channel:${publicChannel.key}:Public`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to a contact conversation when popstate fires with a contact hash', async () => {
|
||||
const aliceContact = {
|
||||
public_key: 'b'.repeat(64),
|
||||
name: 'Alice',
|
||||
type: 1,
|
||||
flags: 0,
|
||||
direct_path: null,
|
||||
direct_path_len: -1,
|
||||
last_advert: null,
|
||||
lat: null,
|
||||
lon: null,
|
||||
last_seen: null,
|
||||
on_radio: false,
|
||||
favorite: false,
|
||||
last_contacted: null,
|
||||
last_read_at: null,
|
||||
first_seen: null,
|
||||
};
|
||||
|
||||
window.location.hash = '';
|
||||
mocks.api.getContacts.mockResolvedValue([aliceContact]);
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
for (const node of screen.getAllByTestId('active-conversation')) {
|
||||
expect(node).toHaveTextContent(`channel:${publicChannel.key}:Public`);
|
||||
}
|
||||
});
|
||||
|
||||
act(() => {
|
||||
window.location.hash = `#contact/${aliceContact.public_key}/Alice`;
|
||||
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
for (const node of screen.getAllByTestId('active-conversation')) {
|
||||
expect(node).toHaveTextContent(`contact:${aliceContact.public_key}:Alice`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('stays on radio settings section even when radio is disconnected', async () => {
|
||||
window.location.hash = '#settings/radio';
|
||||
mocks.api.getRadioConfig.mockRejectedValue(new Error('radio offline'));
|
||||
|
||||
@@ -90,7 +90,41 @@ describe('useAppShell', () => {
|
||||
result.current.handleCloseSettingsView();
|
||||
});
|
||||
|
||||
expect(window.location.hash).toBe('#channel/test/Public');
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#channel/test/Public');
|
||||
});
|
||||
});
|
||||
|
||||
it('pushes a new history entry when opening settings', async () => {
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
const lengthBefore = window.history.length;
|
||||
|
||||
act(() => {
|
||||
result.current.handleToggleSettingsView();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe('#settings/radio');
|
||||
});
|
||||
|
||||
expect(window.history.length).toBe(lengthBefore + 1);
|
||||
});
|
||||
|
||||
it('closes settings when popstate fires with a non-settings hash', async () => {
|
||||
window.location.hash = '#settings/radio';
|
||||
|
||||
const { result } = renderHook(() => useAppShell());
|
||||
|
||||
expect(result.current.showSettings).toBe(true);
|
||||
|
||||
act(() => {
|
||||
window.location.hash = '#channel/abc/Public';
|
||||
window.dispatchEvent(new PopStateEvent('popstate', { state: null }));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.showSettings).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles the cracker shell without affecting sidebar state', () => {
|
||||
|
||||
Reference in New Issue
Block a user