- Pool preset checkboxes with failover in Calibrate and Forge - Tier 1/2 UX: setup banner, forge next worker, LAN defaults, blueprint prompt - USB: auto-install forge tools, seed config.json, sync LAUNCH.bat
149 lines
6.6 KiB
TypeScript
149 lines
6.6 KiB
TypeScript
/**
|
|
* @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(<SettingsPage />);
|
|
}
|
|
|
|
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<typeof mockServerConfig>);
|
|
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();
|
|
});
|
|
});
|