/** * UpdatesSection drives the registry-check cadence from the feature endpoint * (GET /image-updates/status, PUT /image-updates/interval). It must load and * show the current cadence, and present a read-only (disabled) control to * non-admins while keeping the section visible. The PUT round-trip itself is * covered by the backend route tests and the end-to-end check. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; vi.mock('@/lib/api', () => ({ apiFetch: vi.fn() })); vi.mock('@/components/ui/toast-store', () => ({ toast: { error: vi.fn(), success: vi.fn(), warning: vi.fn(), info: vi.fn(), loading: vi.fn(), dismiss: vi.fn() }, })); const authState = { isAdmin: true }; vi.mock('@/context/AuthContext', () => ({ useAuth: () => authState })); vi.mock('@/context/NodeContext', () => ({ useNodes: () => ({ activeNode: { id: 'local' } }) })); vi.mock('../MastheadStatsContext', () => ({ useMastheadStats: () => {} })); import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; import { UpdatesSection } from '../UpdatesSection'; const mockedFetch = apiFetch as unknown as ReturnType; const STATUS = { checking: false, intervalMinutes: 120, lastCheckedAt: Date.now() - 5 * 60 * 1000, nextCheckAt: Date.now() + 115 * 60 * 1000, manualCooldownMinutes: 2, manualCooldownRemainingMs: 0, mode: 'interval' as const, cronExpression: null, }; beforeEach(() => { mockedFetch.mockReset(); authState.isAdmin = true; mockedFetch.mockResolvedValue({ ok: true, json: async () => ({ ...STATUS }) }); }); describe('UpdatesSection', () => { it('loads the cadence status and enables the control for admins', async () => { render(); await waitFor(() => expect(screen.getByText(/Last checked 5m ago/)).toBeInTheDocument()); expect(mockedFetch).toHaveBeenCalledWith('/image-updates/status'); expect(screen.getByRole('combobox', { name: /interval/i })).toBeEnabled(); }); it('shows the section read-only (control disabled) for non-admins', async () => { authState.isAdmin = false; render(); await waitFor(() => expect(screen.getByText(/Last checked/)).toBeInTheDocument()); expect(screen.getByRole('combobox', { name: /interval/i })).toBeDisabled(); }); it('toasts an error and leaves the control disabled when the status load fails', async () => { mockedFetch.mockResolvedValue({ ok: false, json: async () => ({ error: 'boom' }) }); render(); await waitFor(() => expect(toast.error).toHaveBeenCalled()); expect(screen.getByRole('combobox', { name: /interval/i })).toBeDisabled(); }); });