Files
MindSift/frontend/src/lib/__tests__/useConnectionState.test.tsx
T
sandrodachandClaude Opus 5.5 282a9af557 feat(ui): show a limited connection instead of offline behind proxies
When no realtime connection gets through (proxy buffering SSE, notes on
long polling) but the server answers, the profile shows a yellow
"eingeschränkt" instead of "offline". A full connection now shows a
green "online" on the desktop too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:21:14 +01:00

109 lines
3.6 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { act, renderHook } from '@testing-library/react';
let statusListener: ((connected: boolean) => void) | null = null;
let currentConnected = false;
vi.mock('../api', () => ({
onRealtimeStatus: (cb: (connected: boolean) => void) => {
statusListener = cb;
cb(currentConnected);
return () => (statusListener = null);
},
}));
import { useConnectionState, SERVER_OFFLINE_GRACE_MS, SERVER_PROBE_INTERVAL_MS } from '../useConnectionState';
function setDeviceOnline(value: boolean) {
Object.defineProperty(navigator, 'onLine', { value, configurable: true });
}
let serverAnswers = false;
const fetchMock = vi.fn(async () => {
if (!serverAnswers) throw new TypeError('Failed to fetch');
return { ok: true } as Response;
});
// Runs the timers and lets the health request settle
async function wait(ms: number) {
await act(async () => {
await vi.advanceTimersByTimeAsync(ms);
});
}
describe('useConnectionState', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.stubGlobal('fetch', fetchMock);
fetchMock.mockClear();
currentConnected = false;
serverAnswers = false;
setDeviceOnline(true);
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
});
it('stays online during the grace period at app start', async () => {
const { result } = renderHook(() => useConnectionState());
expect(result.current).toBe('online');
act(() => statusListener?.(true));
await wait(SERVER_OFFLINE_GRACE_MS + 100);
expect(result.current).toBe('online');
expect(fetchMock).not.toHaveBeenCalled();
});
it('turns offline once the server stays unreachable and back online on reconnect', async () => {
currentConnected = true;
const { result } = renderHook(() => useConnectionState());
act(() => statusListener?.(false));
expect(result.current).toBe('online');
await wait(SERVER_OFFLINE_GRACE_MS + 100);
expect(result.current).toBe('offline');
act(() => statusListener?.(true));
expect(result.current).toBe('online');
});
it('is limited when the server answers but no realtime connection gets through', async () => {
serverAnswers = true;
const { result } = renderHook(() => useConnectionState());
await wait(SERVER_OFFLINE_GRACE_MS + 100);
expect(result.current).toBe('limited');
expect(fetchMock).toHaveBeenCalledWith('/api/v1/health', expect.anything());
// Asked again while the connection is missing
serverAnswers = false;
await wait(SERVER_PROBE_INTERVAL_MS + 100);
expect(result.current).toBe('offline');
serverAnswers = true;
await wait(SERVER_PROBE_INTERVAL_MS + 100);
expect(result.current).toBe('limited');
act(() => statusListener?.(true));
expect(result.current).toBe('online');
const calls = fetchMock.mock.calls.length;
await wait(SERVER_PROBE_INTERVAL_MS * 2);
expect(fetchMock.mock.calls.length).toBe(calls);
});
it('repeated failed reconnects do not postpone the check', async () => {
currentConnected = true;
const { result } = renderHook(() => useConnectionState());
for (let i = 0; i < 4; i++) {
act(() => statusListener?.(false));
await wait(SERVER_OFFLINE_GRACE_MS / 2);
}
expect(result.current).toBe('offline');
});
it('is offline right away without device network', () => {
currentConnected = true;
const { result } = renderHook(() => useConnectionState());
act(() => {
setDeviceOnline(false);
statusListener?.(false);
});
expect(result.current).toBe('offline');
});
});