diff --git a/frontend/src/components/panels/__tests__/SettingsPanel.test.tsx b/frontend/src/components/panels/__tests__/SettingsPanel.test.tsx new file mode 100644 index 0000000..08a4a04 --- /dev/null +++ b/frontend/src/components/panels/__tests__/SettingsPanel.test.tsx @@ -0,0 +1,104 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, fireEvent, waitFor } from '@testing-library/react' +import { Sidebar } from '../Sidebar' +import * as canvasStore from '@/stores/canvasStore' +import { TooltipProvider } from '@/components/ui/tooltip' + +vi.mock('@/stores/canvasStore') +vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })) +vi.mock('@/api/client', () => ({ + scanApi: { + trigger: vi.fn(), + pending: vi.fn().mockResolvedValue({ data: [] }), + hidden: vi.fn().mockResolvedValue({ data: [] }), + runs: vi.fn().mockResolvedValue({ data: [] }), + getConfig: vi.fn().mockResolvedValue({ data: { ranges: [] } }), + }, + settingsApi: { + get: vi.fn(), + save: vi.fn(), + }, +})) + +import { settingsApi } from '@/api/client' +import { toast } from 'sonner' + +function renderSidebar() { + vi.mocked(canvasStore.useCanvasStore).mockReturnValue({ + nodes: [], + hasUnsavedChanges: false, + hideIp: false, + toggleHideIp: vi.fn(), + addNode: vi.fn(), + scanEventTs: 0, + } as unknown as ReturnType) + + return render( + + + + ) +} + +describe('SettingsPanel', () => { + beforeEach(() => { + vi.mocked(settingsApi.get).mockResolvedValue({ data: { interval_seconds: 60 } } as never) + vi.mocked(settingsApi.save).mockResolvedValue({ data: { interval_seconds: 60 } } as never) + vi.mocked(toast.success).mockReset() + vi.mocked(toast.error).mockReset() + }) + + it('opens when Settings item is clicked', async () => { + renderSidebar() + fireEvent.click(screen.getByRole('button', { name: 'Settings' })) + await waitFor(() => { + expect(settingsApi.get).toHaveBeenCalledOnce() + }) + expect(screen.getByText('Status check interval (s)')).toBeDefined() + }) + + it('displays interval loaded from API', async () => { + vi.mocked(settingsApi.get).mockResolvedValue({ data: { interval_seconds: 120 } } as never) + renderSidebar() + fireEvent.click(screen.getByRole('button', { name: 'Settings' })) + const input = await screen.findByDisplayValue('120') + expect(input).toBeDefined() + }) + + it('saves interval via settingsApi on Save click', async () => { + renderSidebar() + fireEvent.click(screen.getByRole('button', { name: 'Settings' })) + const input = await screen.findByDisplayValue('60') + fireEvent.change(input, { target: { value: '180' } }) + fireEvent.click(screen.getByRole('button', { name: 'Save' })) + await waitFor(() => { + expect(settingsApi.save).toHaveBeenCalledWith({ interval_seconds: 180 }) + expect(toast.success).toHaveBeenCalledWith('Settings saved') + }) + }) + + it('shows error toast when save fails', async () => { + vi.mocked(settingsApi.save).mockRejectedValue(new Error('network')) + renderSidebar() + fireEvent.click(screen.getByRole('button', { name: 'Settings' })) + await screen.findByDisplayValue('60') + fireEvent.click(screen.getByRole('button', { name: 'Save' })) + await waitFor(() => { + expect(toast.error).toHaveBeenCalledWith('Failed to save settings') + }) + }) + + it('closes panel when Settings is clicked again', async () => { + renderSidebar() + fireEvent.click(screen.getByRole('button', { name: 'Settings' })) + await screen.findByText('Status check interval (s)') + fireEvent.click(screen.getByRole('button', { name: 'Settings' })) + expect(screen.queryByText('Status check interval (s)')).toBeNull() + }) +})