Overhaul repeater interaction to better deal with login failure clearly

This commit is contained in:
Jack Kingsman
2026-03-14 22:58:14 -07:00
parent ac65943263
commit 0881998e5b
6 changed files with 244 additions and 33 deletions
+43 -5
View File
@@ -35,6 +35,8 @@ vi.mock('../components/ui/sonner', () => ({
// Get mock reference — cast to Record<string, Mock> for type-safe mock method access
const { api: _rawApi } = await import('../api');
const mockApi = _rawApi as unknown as Record<string, Mock>;
const { toast } = await import('../components/ui/sonner');
const mockToast = toast as unknown as Record<string, Mock>;
const REPEATER_KEY = 'aa'.repeat(32);
@@ -58,7 +60,11 @@ describe('useRepeaterDashboard', () => {
});
it('login sets loggedIn on success', async () => {
mockApi.repeaterLogin.mockResolvedValueOnce({ status: 'ok' });
mockApi.repeaterLogin.mockResolvedValueOnce({
status: 'ok',
authenticated: true,
message: null,
});
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
@@ -72,7 +78,11 @@ describe('useRepeaterDashboard', () => {
});
it('login sets error on failure', async () => {
mockApi.repeaterLogin.mockRejectedValueOnce(new Error('Auth failed'));
mockApi.repeaterLogin.mockResolvedValueOnce({
status: 'error',
authenticated: false,
message: 'Auth failed',
});
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
@@ -80,12 +90,19 @@ describe('useRepeaterDashboard', () => {
await result.current.login('bad');
});
expect(result.current.loggedIn).toBe(false);
expect(result.current.loggedIn).toBe(true);
expect(result.current.loginError).toBe('Auth failed');
expect(mockToast.error).toHaveBeenCalledWith('Login not confirmed', {
description: 'Auth failed',
});
});
it('loginAsGuest calls login with empty password', async () => {
mockApi.repeaterLogin.mockResolvedValueOnce({ status: 'ok' });
mockApi.repeaterLogin.mockResolvedValueOnce({
status: 'ok',
authenticated: true,
message: null,
});
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
@@ -97,6 +114,23 @@ describe('useRepeaterDashboard', () => {
expect(result.current.loggedIn).toBe(true);
});
it('login still opens dashboard when request rejects', async () => {
mockApi.repeaterLogin.mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useRepeaterDashboard(repeaterConversation));
await act(async () => {
await result.current.login('secret');
});
expect(result.current.loggedIn).toBe(true);
expect(result.current.loginError).toBe('Network error');
expect(mockToast.error).toHaveBeenCalledWith('Login request failed', {
description:
'Network error. The dashboard is still available, but repeater operations may fail until a login succeeds.',
});
});
it('refreshPane stores data on success', async () => {
const statusData = {
battery_volts: 4.2,
@@ -376,7 +410,11 @@ describe('useRepeaterDashboard', () => {
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' });
mockApi.repeaterLogin.mockResolvedValueOnce({
status: 'ok',
authenticated: true,
message: null,
});
mockApi.repeaterStatus.mockResolvedValueOnce(statusData);
mockApi.sendRepeaterCommand.mockResolvedValueOnce({
command: 'ver',