Files
ToBePlayed-Frontend/src/hooks/useLibrarySync.test.jsx
T
lobo f9f0d94e2c feat(onboarding): remove dead ends for new users (#55)
Registration shows the address used and can start over.
Confirm page says an admin approves; no login link.
Welcome puts next steps first; Steam link returns to the dashboard.
Dashboard and library explain an empty or private library.
First sync shows queued, polls slower up to 10 min, then a calm text.
Login: simple checks, resend offer after 401, password toggles.
Only a real expired link counts as invalid; typed passwords stay.
Default language follows the browser; texts are platform-neutral.
2026-10-21 09:37:17 +02:00

156 lines
7.0 KiB
React

// @vitest-environment jsdom
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest';
import {act, cleanup, renderHook} from '@testing-library/react';
import useLibrarySync from './useLibrarySync.js';
import {fetchRoutes, json} from '../test/fetchRoutes.js';
const status = (lastSyncStatus, lastSyncedAt) => json(200, {lastSyncStatus, lastSyncedAt});
beforeEach(() => {
vi.useFakeTimers();
localStorage.setItem('tbp_session', '1');
});
afterEach(() => {
cleanup();
vi.useRealTimers();
localStorage.clear();
});
describe('useLibrarySync', () => {
it('shows a sync that runs longer than the poll limit as stale, not as running forever', async () => {
const fetch = fetchRoutes({'GET /api/library/sync-status': status('RUNNING', null)});
const {result} = renderHook(() => useLibrarySync({enabled: true, onComplete: () => {}}));
await act(() => vi.advanceTimersByTimeAsync(0));
expect(result.current.isRunning).toBe(true);
await act(() => vi.advanceTimersByTimeAsync(605_000));
expect(result.current.isRunning).toBe(false);
expect(result.current.stale).toBe(true);
const calls = fetch.mock.calls.length;
await act(() => vi.advanceTimersByTimeAsync(30_000));
expect(fetch.mock.calls.length).toBe(calls);
});
it('isNotQueuedWhenDisabled', async () => {
fetchRoutes({});
const {result} = renderHook(() => useLibrarySync({enabled: false, expectInitialSync: true, onComplete: () => {}}));
await act(() => vi.advanceTimersByTimeAsync(0));
expect(result.current.queued).toBe(false);
});
it('is queued while a first sync is expected and Core has no status row', async () => {
fetchRoutes({'GET /api/library/sync-status': status(null, null)});
const {result} = renderHook(() => useLibrarySync({enabled: true, expectInitialSync: true, onComplete: () => {}}));
await act(() => vi.advanceTimersByTimeAsync(0));
expect(result.current.queued).toBe(true);
});
it('is not queued without expectInitialSync, or once a row exists', async () => {
fetchRoutes({'GET /api/library/sync-status': status(null, null)});
const first = renderHook(() => useLibrarySync({enabled: true, onComplete: () => {}}));
await act(() => vi.advanceTimersByTimeAsync(0));
expect(first.result.current.queued).toBe(false);
cleanup();
fetchRoutes({'GET /api/library/sync-status': status('RUNNING', null)});
const second = renderHook(() => useLibrarySync({enabled: true, expectInitialSync: true, onComplete: () => {}}));
await act(() => vi.advanceTimersByTimeAsync(0));
expect(second.result.current.queued).toBe(false);
});
it('polls every 3 s for 15 s while queued, then every 15 s, and stops after 10 minutes', async () => {
const fetch = fetchRoutes({'GET /api/library/sync-status': status(null, null)});
renderHook(() => useLibrarySync({enabled: true, expectInitialSync: true, onComplete: () => {}}));
await act(() => vi.advanceTimersByTimeAsync(0));
expect(fetch.mock.calls.length).toBe(1);
await act(() => vi.advanceTimersByTimeAsync(15_000));
expect(fetch.mock.calls.length).toBe(6);
await act(() => vi.advanceTimersByTimeAsync(60_000));
expect(fetch.mock.calls.length).toBe(10);
await act(() => vi.advanceTimersByTimeAsync(600_000));
const calls = fetch.mock.calls.length;
await act(() => vi.advanceTimersByTimeAsync(60_000));
expect(fetch.mock.calls.length).toBe(calls);
});
it('still sees a first sync that starts after one minute', async () => {
let answer = status(null, null);
fetchRoutes({'GET /api/library/sync-status': () => answer});
const onComplete = vi.fn();
const {result} = renderHook(() => useLibrarySync({enabled: true, expectInitialSync: true, onComplete}));
await act(() => vi.advanceTimersByTimeAsync(60_000));
expect(result.current.isRunning).toBe(false);
answer = status('RUNNING', null);
await act(() => vi.advanceTimersByTimeAsync(15_000));
expect(result.current.isRunning).toBe(true);
answer = status('OK', '2026-10-21T08:00:00Z');
await act(() => vi.advanceTimersByTimeAsync(15_000));
expect(onComplete).toHaveBeenCalledTimes(1);
});
it('after watch() reports the end of a sync once, also when it never saw it running', async () => {
let answer = status('OK', '2026-09-30T08:00:00Z');
fetchRoutes({'GET /api/library/sync-status': () => answer});
const onComplete = vi.fn();
const {result} = renderHook(() => useLibrarySync({enabled: true, onComplete}));
await act(() => vi.advanceTimersByTimeAsync(0));
act(() => result.current.watch());
await act(() => vi.advanceTimersByTimeAsync(0));
// The sync was so fast that the next poll already sees it done.
answer = status('OK', '2026-09-30T08:05:00Z');
await act(() => vi.advanceTimersByTimeAsync(3_000));
await act(() => vi.advanceTimersByTimeAsync(30_000));
expect(onComplete).toHaveBeenCalledTimes(1);
});
it('reports the end of a running sync once', async () => {
let answer = status('RUNNING', '2026-09-30T08:00:00Z');
fetchRoutes({'GET /api/library/sync-status': () => answer});
const onComplete = vi.fn();
const {result} = renderHook(() => useLibrarySync({enabled: true, onComplete}));
await act(() => vi.advanceTimersByTimeAsync(0));
answer = status('FAILED', '2026-09-30T08:01:00Z');
await act(() => vi.advanceTimersByTimeAsync(3_000));
await act(() => vi.advanceTimersByTimeAsync(30_000));
expect(onComplete).toHaveBeenCalledTimes(1);
expect(result.current.status).toBe('FAILED');
});
it('fires onComplete only once when expectInitialSync flips after the watched run ended', async () => {
let answer = status('OK', '2026-09-30T08:00:00Z');
fetchRoutes({'GET /api/library/sync-status': () => answer});
const onComplete = vi.fn();
const {result, rerender} = renderHook(
({expectInitialSync}) => useLibrarySync({enabled: true, expectInitialSync, onComplete}),
{initialProps: {expectInitialSync: false}});
await act(() => vi.advanceTimersByTimeAsync(0));
act(() => result.current.watch());
await act(() => vi.advanceTimersByTimeAsync(0));
answer = status('OK', '2026-09-30T08:05:00Z');
await act(() => vi.advanceTimersByTimeAsync(3_000));
expect(onComplete).toHaveBeenCalledTimes(1);
// onComplete loads the list: isLoading goes true and back to false, and with 0 games expectInitialSync follows.
rerender({expectInitialSync: true});
await act(() => vi.advanceTimersByTimeAsync(0));
rerender({expectInitialSync: false});
await act(() => vi.advanceTimersByTimeAsync(0));
expect(onComplete).toHaveBeenCalledTimes(1);
});
});