/** * @vitest-environment happy-dom */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { cleanup, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import SettingsPage, { deepMerge } from './SettingsPage'; import { mockServerConfig, mockServerInfo } from '../test/fixtures'; import { api } from '../api/client'; import { clearStoredAuth, getStoredAuth, setStoredAuth } from '../api/auth'; function renderSettings() { return render(); } describe('deepMerge', () => { it('preserves base keys not present in override', () => { const base = { a: 1, nested: { keep: true, old: 'x' } }; const result = deepMerge(base, { nested: { old: 'y' } }); expect(result.a).toBe(1); expect(result.nested.keep).toBe(true); expect(result.nested.old).toBe('y'); }); it('replaces scalar values from override', () => { const base = { port: 8989, pool: { host: 'a', port: 3333 } }; const result = deepMerge(base, { port: 8080, pool: { host: 'b' } }); expect(result.port).toBe(8080); expect(result.pool.host).toBe('b'); expect(result.pool.port).toBe(3333); }); }); describe('SettingsPage (Calibrate)', () => { beforeEach(() => { vi.clearAllMocks(); sessionStorage.clear(); vi.spyOn(api, 'getConfig').mockResolvedValue(mockServerConfig()); vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo); vi.spyOn(api, 'updateConfig').mockImplementation(async (cfg) => cfg as ReturnType); vi.spyOn(api, 'createUser').mockResolvedValue({ success: true }); }); afterEach(() => { cleanup(); }); it('shows loading state then Calibrate heading', async () => { renderSettings(); expect(screen.getByRole('heading', { level: 1, name: 'Calibrate' })).toBeInTheDocument(); expect(screen.getByText('Loading server calibration...')).toBeInTheDocument(); expect(await screen.findByText('CONTROL SERVER · LOCAL HOST')).toBeInTheDocument(); expect(screen.getAllByRole('heading', { level: 1, name: 'Calibrate' })).toHaveLength(1); }); it('renders key section headings and labels', async () => { renderSettings(); expect(await screen.findByText('Control Server')).toBeInTheDocument(); expect(screen.getByText('Upstream Pool')).toBeInTheDocument(); expect(screen.getByText('Fleet Payout Wallet')).toBeInTheDocument(); expect(screen.getByText('Access Control')).toBeInTheDocument(); expect(screen.getByText('Fleet Security')).toBeInTheDocument(); expect(screen.getByText('Listen Port')).toBeInTheDocument(); expect(screen.getByText(/Public URL \(LAN\)/i)).toBeInTheDocument(); expect(screen.getByText(/XMR Address/i)).toBeInTheDocument(); expect(screen.getByDisplayValue('8080')).toBeInTheDocument(); }); it('shows detected LAN endpoints banner', async () => { renderSettings(); expect(await screen.findByText('DETECTED LAN ENDPOINTS')).toBeInTheDocument(); expect(screen.getByText(mockServerInfo.suggested_url!)).toBeInTheDocument(); expect(screen.getByText(/IPs on this host:/)).toBeInTheDocument(); }); it('saves calibration via updateConfig API', async () => { const updateSpy = vi.spyOn(api, 'updateConfig'); renderSettings(); await screen.findByRole('button', { name: 'Save Calibration' }); await userEvent.setup().click(screen.getByRole('button', { name: 'Save Calibration' })); await waitFor(() => { expect(updateSpy).toHaveBeenCalled(); }); expect(await screen.findByText('Calibration saved — control server updated.')).toBeInTheDocument(); }); it('applies best defaults to public URL from server info', async () => { vi.spyOn(api, 'getConfig').mockResolvedValue( mockServerConfig({ server: { public_url: '' } }) ); renderSettings(); await screen.findByRole('button', { name: 'Use best defaults' }); const publicUrlInput = screen.getByPlaceholderText(mockServerInfo.suggested_url!) as HTMLInputElement; expect(publicUrlInput.value).toBe(''); await userEvent.setup().click(screen.getByRole('button', { name: 'Use best defaults' })); expect( await screen.findByText(/Best defaults applied.*Save Calibration/i) ).toBeInTheDocument(); expect(publicUrlInput.value).toBe(mockServerInfo.suggested_url); }); it('stores browser session credentials', async () => { renderSettings(); const accessSection = (await screen.findByText('Access Control')).closest('.settings-section') as HTMLElement; const user = userEvent.setup(); const sessionUser = within(accessSection).getAllByPlaceholderText('admin')[0]; const sessionPass = accessSection.querySelectorAll('input[type="password"]')[0] as HTMLInputElement; await user.type(sessionUser, 'admin'); await user.type(sessionPass, 'secret-pass'); await user.click(within(accessSection).getByRole('button', { name: 'Save session login' })); expect(getStoredAuth()).toBeTruthy(); expect(await screen.findByText(/Session login saved/i)).toBeInTheDocument(); }); it('clears browser session on logout', async () => { setStoredAuth('admin', 'secret-pass'); renderSettings(); await screen.findByText('Session active'); await userEvent.setup().click(screen.getByRole('button', { name: 'Clear session' })); expect(getStoredAuth()).toBeNull(); expect(await screen.findByText('Session login cleared.')).toBeInTheDocument(); clearStoredAuth(); }); it('adds dashboard user via createUser API', async () => { const createSpy = vi.spyOn(api, 'createUser'); renderSettings(); const accessSection = (await screen.findByText('Access Control')).closest('.settings-section') as HTMLElement; const user = userEvent.setup(); const newUserInput = within(accessSection).getAllByPlaceholderText('admin')[1]; const newPassInput = within(accessSection).getByPlaceholderText('••••••••'); await user.type(newUserInput, 'operator'); await user.type(newPassInput, 'op-pass-123'); await user.click(within(accessSection).getByRole('button', { name: 'Add User' })); await waitFor(() => { expect(createSpy).toHaveBeenCalledWith('operator', 'op-pass-123'); }); expect(await screen.findByText('User "operator" added successfully!')).toBeInTheDocument(); }); it('describes first-run admin credentials in Access Control help', async () => { renderSettings(); expect( await screen.findByText(/first server start, credentials are printed once in the server console/i) ).toBeInTheDocument(); expect(screen.getByText(/admin/i)).toBeInTheDocument(); }); });