796 lines
25 KiB
TypeScript
796 lines
25 KiB
TypeScript
/**
|
||
* @vitest-environment happy-dom
|
||
*/
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
import { cleanup, render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||
import userEvent from '@testing-library/user-event';
|
||
import { MemoryRouter } from 'react-router-dom';
|
||
import { type ReactNode } from 'react';
|
||
import { mockAgent, mockServerInfo } from '../test/fixtures';
|
||
import { api } from '../api/client';
|
||
import { downloadApiFile, downloadAuthedFile } from '../api/download';
|
||
import { getStoredAuth } from '../api/auth';
|
||
import { useWebSocket } from '../hooks/useWebSocket';
|
||
import { DEFAULT_FLEET_FILTERS } from '../help/fleetFilters';
|
||
|
||
import NeonCard from './NeonCard/NeonCard';
|
||
import { HelpTip, FieldHint } from './HelpTip';
|
||
import DownloadButton from './DownloadButton';
|
||
import AuthDownloadButton from './AuthDownloadButton';
|
||
import ErrorBoundary from './ErrorBoundary';
|
||
import SessionGate from './SessionGate';
|
||
import GaugeRing from './Charts/GaugeRing';
|
||
import HashrateChart from './Charts/HashrateChart';
|
||
import AgentRemoteActions from './Fleet/AgentRemoteActions';
|
||
import AgentListItem from './Fleet/AgentListItem';
|
||
import FleetToolbar from './Fleet/FleetToolbar';
|
||
import { LanDownloadQR } from './Fleet/LanDownloadQR';
|
||
import {
|
||
AlertBanner,
|
||
PoolStatusPanel,
|
||
AIActivityPanel,
|
||
EarningsEstimator,
|
||
FleetHealthCard,
|
||
ContributionBars,
|
||
UnderperformerList,
|
||
OSArchBreakdown,
|
||
LANGroupView,
|
||
} from './Fleet/FleetPanels';
|
||
import { ForgeLockedHint, ForgeFieldBadge, ForgeSectionHeader } from './Forge/ForgeFieldHints';
|
||
import {
|
||
PipelineFlow,
|
||
FleetPipelineStatus,
|
||
ActivityPulse,
|
||
ForgeCalibrateCompare,
|
||
RoadmapGrid,
|
||
} from './Visual/VisualComponents';
|
||
import MatrixStreamOverlay from './Visual/MatrixStreamOverlay';
|
||
import SystemStatusBar from './Visual/SystemStatusBar';
|
||
import FleetTopologyMap from './Visual/3D/FleetTopologyMap';
|
||
import Layout from './Layout/Layout';
|
||
import AmbientBackground from './Ambient/AmbientBackground';
|
||
import CursorFire from './Visual/CursorFire';
|
||
import MatrixRain from './Layout/MatrixRain';
|
||
|
||
vi.mock('../hooks/useWebSocket', () => ({
|
||
useWebSocket: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('../context/ForgeContext', () => ({
|
||
useForge: vi.fn(() => ({ forging: false, stage: '' })),
|
||
}));
|
||
|
||
vi.mock('../api/download', () => ({
|
||
downloadApiFile: vi.fn(),
|
||
downloadAuthedFile: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('../api/auth', () => ({
|
||
getStoredAuth: vi.fn(),
|
||
setStoredAuth: vi.fn(),
|
||
}));
|
||
|
||
vi.mock('qrcode', () => ({
|
||
default: {
|
||
toCanvas: vi.fn().mockResolvedValue(undefined),
|
||
},
|
||
}));
|
||
|
||
vi.mock('recharts', async () => {
|
||
const actual = await vi.importActual<typeof import('recharts')>('recharts');
|
||
return {
|
||
...actual,
|
||
ResponsiveContainer: ({ children }: { children: ReactNode }) => (
|
||
<div data-testid="recharts-responsive">{children}</div>
|
||
),
|
||
};
|
||
});
|
||
|
||
vi.mock('@react-three/fiber', () => ({
|
||
Canvas: ({ children }: { children: ReactNode }) => <div data-testid="three-canvas">{children}</div>,
|
||
useFrame: () => {},
|
||
}));
|
||
|
||
vi.mock('@react-three/drei', () => ({
|
||
OrbitControls: () => null,
|
||
Stars: () => null,
|
||
Line: () => null,
|
||
Sphere: () => null,
|
||
}));
|
||
|
||
const useWebSocketMock = vi.mocked(useWebSocket);
|
||
const downloadApiFileMock = vi.mocked(downloadApiFile);
|
||
const downloadAuthedFileMock = vi.mocked(downloadAuthedFile);
|
||
const getStoredAuthMock = vi.mocked(getStoredAuth);
|
||
|
||
function ThrowOnce({ shouldThrow }: { shouldThrow: boolean }) {
|
||
if (shouldThrow) throw new Error('render boom');
|
||
return <span>child ok</span>;
|
||
}
|
||
|
||
describe('NeonCard', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('renders children with default brass accent and 3d class', () => {
|
||
render(<NeonCard>Inner</NeonCard>);
|
||
const card = screen.getByText('Inner').closest('.neon-card');
|
||
expect(card).toHaveClass('neon-card-brass', 'neon-card-3d');
|
||
expect(card?.querySelector('.neon-card-rim')).toBeTruthy();
|
||
});
|
||
|
||
it('applies accent, hud, tilt3d off, and custom className', () => {
|
||
render(
|
||
<NeonCard accent="cyan" hud tilt3d={false} className="extra">
|
||
X
|
||
</NeonCard>
|
||
);
|
||
const card = screen.getByText('X').closest('.neon-card');
|
||
expect(card).toHaveClass('neon-card-cyan', 'hud-corners', 'extra');
|
||
expect(card).not.toHaveClass('neon-card-3d');
|
||
});
|
||
});
|
||
|
||
describe('HelpTip', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('returns null for unknown field', () => {
|
||
const { container } = render(<HelpTip field="nonexistent_field_xyz" />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('shows help popup on hover for known field', async () => {
|
||
render(<HelpTip field="calibrate_wallet" label="Wallet" />);
|
||
expect(screen.getByRole('button', { name: /Help: calibrate_wallet/ })).toBeInTheDocument();
|
||
await userEvent.setup().hover(screen.getByRole('button'));
|
||
await waitFor(() => {
|
||
expect(screen.getByRole('tooltip')).toHaveTextContent(/Monero payout address/i);
|
||
});
|
||
});
|
||
|
||
it('FieldHint export is deprecated no-op', () => {
|
||
const { container } = render(<FieldHint field="calibrate_wallet" />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('DownloadButton', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
downloadApiFileMock.mockReset();
|
||
});
|
||
|
||
it('downloads on click and shows busy label', async () => {
|
||
downloadApiFileMock.mockImplementation(() => new Promise((r) => setTimeout(r, 50)));
|
||
render(
|
||
<DownloadButton apiPath="/api/x" filename="a.bin">
|
||
Save
|
||
</DownloadButton>
|
||
);
|
||
const btn = screen.getByRole('button', { name: 'Save' });
|
||
await userEvent.setup().click(btn);
|
||
expect(screen.getByRole('button', { name: 'Downloading…' })).toBeDisabled();
|
||
await waitFor(() => expect(downloadApiFileMock).toHaveBeenCalledWith('/api/x', 'a.bin'));
|
||
});
|
||
|
||
it('alerts on download failure', async () => {
|
||
downloadApiFileMock.mockRejectedValue(new Error('network down'));
|
||
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||
render(
|
||
<DownloadButton apiPath="/api/x" filename="a.bin">
|
||
Save
|
||
</DownloadButton>
|
||
);
|
||
await userEvent.setup().click(screen.getByRole('button'));
|
||
await waitFor(() => expect(alertSpy).toHaveBeenCalledWith('network down'));
|
||
alertSpy.mockRestore();
|
||
});
|
||
});
|
||
|
||
describe('AuthDownloadButton', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
downloadAuthedFileMock.mockReset();
|
||
});
|
||
|
||
it('calls downloadAuthedFile and shows ellipsis while busy', async () => {
|
||
downloadAuthedFileMock.mockImplementation(() => new Promise((r) => setTimeout(r, 30)));
|
||
render(
|
||
<AuthDownloadButton apiPath="/api/build/1" filename="b.exe">
|
||
DL
|
||
</AuthDownloadButton>
|
||
);
|
||
await userEvent.setup().click(screen.getByRole('button', { name: 'DL' }));
|
||
expect(screen.getByRole('button', { name: '…' })).toBeDisabled();
|
||
await waitFor(() =>
|
||
expect(downloadAuthedFileMock).toHaveBeenCalledWith('/api/build/1', 'b.exe')
|
||
);
|
||
});
|
||
});
|
||
|
||
describe('ErrorBoundary', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('renders children when no error', () => {
|
||
render(
|
||
<ErrorBoundary>
|
||
<ThrowOnce shouldThrow={false} />
|
||
</ErrorBoundary>
|
||
);
|
||
expect(screen.getByText('child ok')).toBeInTheDocument();
|
||
});
|
||
|
||
it('shows fallback UI and clears error on retry', async () => {
|
||
let throwNow = true;
|
||
function MaybeThrow() {
|
||
if (throwNow) throw new Error('render boom');
|
||
return <span>child ok</span>;
|
||
}
|
||
render(
|
||
<ErrorBoundary>
|
||
<MaybeThrow />
|
||
</ErrorBoundary>
|
||
);
|
||
expect(screen.getByRole('heading', { name: 'Something failed to render' })).toBeInTheDocument();
|
||
throwNow = false;
|
||
await userEvent.setup().click(screen.getByRole('button', { name: 'Retry' }));
|
||
expect(screen.getByText('child ok')).toBeInTheDocument();
|
||
});
|
||
|
||
it('uses custom fallback when provided', () => {
|
||
render(
|
||
<ErrorBoundary fallback={<p>Custom fail</p>}>
|
||
<ThrowOnce shouldThrow />
|
||
</ErrorBoundary>
|
||
);
|
||
expect(screen.getByText('Custom fail')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('SessionGate', () => {
|
||
afterEach(() => {
|
||
cleanup();
|
||
vi.unstubAllGlobals();
|
||
});
|
||
|
||
beforeEach(() => {
|
||
getStoredAuthMock.mockReturnValue(null);
|
||
});
|
||
|
||
it('shows login form when unauthenticated', async () => {
|
||
vi.stubGlobal(
|
||
'fetch',
|
||
vi.fn().mockResolvedValue({ ok: false })
|
||
);
|
||
render(
|
||
<SessionGate>
|
||
<div>protected</div>
|
||
</SessionGate>
|
||
);
|
||
await waitFor(() => {
|
||
expect(screen.getByRole('heading', { name: 'AetherForge' })).toBeInTheDocument();
|
||
});
|
||
expect(screen.queryByText('protected')).not.toBeInTheDocument();
|
||
});
|
||
|
||
it('renders children when stored auth validates', async () => {
|
||
getStoredAuthMock.mockReturnValue('dGVzdA==');
|
||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true }));
|
||
render(
|
||
<SessionGate>
|
||
<div>protected</div>
|
||
</SessionGate>
|
||
);
|
||
await waitFor(() => expect(screen.getByText('protected')).toBeInTheDocument());
|
||
});
|
||
});
|
||
|
||
describe('GaugeRing', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('renders label and percentage for percent-scale max', () => {
|
||
render(<GaugeRing value={42} max={100} label="CPU" sublabel="load" />);
|
||
expect(screen.getByText('42%')).toBeInTheDocument();
|
||
expect(screen.getByText('CPU')).toBeInTheDocument();
|
||
expect(screen.getByText('load')).toBeInTheDocument();
|
||
});
|
||
|
||
it('shows raw value in center while ring arc clamps to 0–100%', () => {
|
||
const { rerender, container } = render(<GaugeRing value={-10} max={100} label="X" />);
|
||
expect(screen.getByText('-10%')).toBeInTheDocument();
|
||
const fill = container.querySelector('.gauge-ring-fill') as SVGCircleElement;
|
||
expect(fill.getAttribute('stroke-dashoffset')).toBe(String(2 * Math.PI * 42));
|
||
rerender(<GaugeRing value={200} max={100} label="X" />);
|
||
expect(screen.getByText('200%')).toBeInTheDocument();
|
||
expect((container.querySelector('.gauge-ring-fill') as SVGCircleElement).getAttribute('stroke-dashoffset')).toBe('0');
|
||
});
|
||
});
|
||
|
||
describe('HashrateChart', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('shows empty state when data is empty', () => {
|
||
render(<HashrateChart data={[]} title="Fleet Hash" />);
|
||
expect(screen.getByText('Fleet Hash')).toBeInTheDocument();
|
||
expect(screen.getByText(/Awaiting signal from fleet/i)).toBeInTheDocument();
|
||
});
|
||
|
||
it('renders chart with data points', () => {
|
||
render(
|
||
<HashrateChart
|
||
data={[
|
||
{ time: '12:00', value: 1500 },
|
||
{ time: '12:01', value: 2000 },
|
||
]}
|
||
title="Live"
|
||
/>
|
||
);
|
||
expect(screen.getByText('Live')).toBeInTheDocument();
|
||
expect(screen.getByText('● LIVE')).toBeInTheDocument();
|
||
expect(screen.getByTestId('recharts-responsive')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('AgentRemoteActions', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
|
||
vi.spyOn(api, 'sendAgentCommand').mockResolvedValue({ success: true });
|
||
});
|
||
|
||
it('compact mode disables actions when offline', () => {
|
||
render(
|
||
<AgentRemoteActions agent={mockAgent({ status: 'offline' })} compact online={false} />
|
||
);
|
||
expect(screen.getByRole('button', { name: 'Pause' })).toBeDisabled();
|
||
expect(screen.getByRole('button', { name: 'Resume' })).toBeDisabled();
|
||
});
|
||
|
||
it('compact dispatches pause when online', async () => {
|
||
const cmd = vi.spyOn(api, 'sendAgentCommand').mockResolvedValue({ success: true });
|
||
render(
|
||
<AgentRemoteActions
|
||
agent={mockAgent({ id: 'a1', status: 'online' })}
|
||
compact
|
||
online
|
||
/>
|
||
);
|
||
await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
|
||
await waitFor(() => expect(cmd).toHaveBeenCalledWith('a1', 'pause', {}));
|
||
});
|
||
|
||
it('full panel shows Target heading and recon section', async () => {
|
||
render(<AgentRemoteActions agent={mockAgent({ name: 'Node A' })} online />);
|
||
await waitFor(() => {
|
||
expect(screen.getByRole('heading', { name: 'Target: Node A' })).toBeInTheDocument();
|
||
});
|
||
expect(screen.getByRole('heading', { name: 'Recon & Intel' })).toBeInTheDocument();
|
||
expect(screen.getByText(/Awaiting telemetry/i)).toBeInTheDocument();
|
||
});
|
||
|
||
it('disables recon buttons when agent offline', async () => {
|
||
render(<AgentRemoteActions agent={mockAgent({ status: 'offline' })} online={false} />);
|
||
await waitFor(() => {
|
||
expect(screen.getByRole('heading', { name: /Target:/i })).toBeInTheDocument();
|
||
});
|
||
expect(screen.getByRole('button', { name: 'Screenshot' })).toBeDisabled();
|
||
});
|
||
});
|
||
|
||
describe('AgentListItem', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
const baseProps = {
|
||
agent: mockAgent({ name: 'List Node', tags: ['rack'], notes: 'a'.repeat(90) }),
|
||
selected: false,
|
||
expanded: false,
|
||
onToggleExpand: vi.fn(),
|
||
onSelect: vi.fn(),
|
||
};
|
||
|
||
it('renders name, status, and click hint when collapsed', () => {
|
||
render(<AgentListItem {...baseProps} />);
|
||
expect(screen.getByText('List Node')).toBeInTheDocument();
|
||
expect(screen.getByText('online')).toBeInTheDocument();
|
||
expect(screen.getByText('click for details')).toBeInTheDocument();
|
||
expect(screen.getByText('rack')).toBeInTheDocument();
|
||
});
|
||
|
||
it('truncates long notes preview when collapsed', () => {
|
||
render(<AgentListItem {...baseProps} />);
|
||
const preview = screen.getByText(/…$/);
|
||
expect(preview.textContent!.length).toBeLessThanOrEqual(81);
|
||
});
|
||
|
||
it('expands meta and remote actions', async () => {
|
||
vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
|
||
render(<AgentListItem {...baseProps} expanded selectable checked onCheck={vi.fn()} />);
|
||
expect(screen.getByText(/Shares:/)).toBeInTheDocument();
|
||
expect(screen.getByRole('button', { name: 'Pause' })).toBeInTheDocument();
|
||
});
|
||
|
||
it('checkbox stops row navigation', async () => {
|
||
const onCheck = vi.fn();
|
||
render(<AgentListItem {...baseProps} selectable onCheck={onCheck} />);
|
||
await userEvent.setup().click(screen.getByRole('checkbox'));
|
||
expect(onCheck).toHaveBeenCalledWith(true);
|
||
expect(baseProps.onSelect).not.toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('FleetToolbar', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
const filters = { ...DEFAULT_FLEET_FILTERS };
|
||
const agents = [
|
||
mockAgent({ tags: ['prod'], ip: '10.0.0.5' }),
|
||
mockAgent({ id: 'a2', name: 'B', tags: ['dev'], ip: '10.0.1.2' }),
|
||
];
|
||
|
||
it('renders search and filter controls', () => {
|
||
render(
|
||
<FleetToolbar
|
||
agents={agents}
|
||
filters={filters}
|
||
onChange={vi.fn()}
|
||
selectedCount={0}
|
||
onBulkAction={vi.fn()}
|
||
bulkBusy={false}
|
||
/>
|
||
);
|
||
expect(screen.getByPlaceholderText('Search name, IP, notes, tags…')).toBeInTheDocument();
|
||
expect(screen.getByText('All tags')).toBeInTheDocument();
|
||
});
|
||
|
||
it('shows bulk bar when agents selected', async () => {
|
||
const onBulk = vi.fn();
|
||
render(
|
||
<FleetToolbar
|
||
agents={agents}
|
||
filters={filters}
|
||
onChange={vi.fn()}
|
||
selectedCount={2}
|
||
onBulkAction={onBulk}
|
||
bulkBusy={false}
|
||
/>
|
||
);
|
||
expect(screen.getByText('2 selected')).toBeInTheDocument();
|
||
await userEvent.setup().click(screen.getByRole('button', { name: 'Pause' }));
|
||
expect(onBulk).toHaveBeenCalledWith('pause');
|
||
});
|
||
|
||
it('shows select-all filtered when props provided', () => {
|
||
const onSelectAll = vi.fn();
|
||
render(
|
||
<FleetToolbar
|
||
agents={agents}
|
||
filters={filters}
|
||
onChange={vi.fn()}
|
||
selectedCount={0}
|
||
onBulkAction={vi.fn()}
|
||
bulkBusy={false}
|
||
filteredCount={3}
|
||
onSelectAllFiltered={onSelectAll}
|
||
/>
|
||
);
|
||
fireEvent.click(screen.getByRole('button', { name: 'Select all filtered (3)' }));
|
||
expect(onSelectAll).toHaveBeenCalled();
|
||
});
|
||
});
|
||
|
||
describe('LanDownloadQR', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('returns null without url', () => {
|
||
const { container } = render(<LanDownloadQR url="" />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('renders canvas when url provided', () => {
|
||
const { container } = render(<LanDownloadQR url="http://192.168.1.5:8080/get" />);
|
||
expect(container.querySelector('canvas')).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
describe('FleetPanels', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('AlertBanner returns null when empty', () => {
|
||
const { container } = render(<AlertBanner alerts={[]} />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('AlertBanner shows up to five alerts', () => {
|
||
const alerts = [
|
||
{ id: '1', level: 'warning', type: 'offline', message: 'Node down', timestamp: '2026-05-30T12:00:00Z' },
|
||
];
|
||
render(<AlertBanner alerts={alerts} />);
|
||
expect(screen.getByText('OFFLINE')).toBeInTheDocument();
|
||
expect(screen.getByText('Node down')).toBeInTheDocument();
|
||
});
|
||
|
||
it('PoolStatusPanel empty hint', () => {
|
||
render(<PoolStatusPanel pools={[]} />);
|
||
expect(screen.getByRole('heading', { name: /Pool Stratum Status/i })).toBeInTheDocument();
|
||
expect(screen.getByText(/No forged pool connections yet/i)).toBeInTheDocument();
|
||
});
|
||
|
||
it('AIActivityPanel maps agent names', () => {
|
||
render(
|
||
<AIActivityPanel
|
||
entries={[
|
||
{
|
||
agent_id: 'agent-001-uuid',
|
||
last_tool: 'pause',
|
||
last_success: true,
|
||
},
|
||
]}
|
||
agentNames={{ 'agent-001-uuid': 'Friendly' }}
|
||
/>
|
||
);
|
||
expect(screen.getByText('Friendly')).toBeInTheDocument();
|
||
});
|
||
|
||
it('FleetHealthCard shows score and label', () => {
|
||
render(
|
||
<FleetHealthCard
|
||
health={{
|
||
score: 88,
|
||
color: 'green',
|
||
label: 'NOMINAL',
|
||
sentence: 'All good',
|
||
issues: [],
|
||
}}
|
||
/>
|
||
);
|
||
expect(screen.getByText('NOMINAL')).toBeInTheDocument();
|
||
expect(screen.getByText('88')).toBeInTheDocument();
|
||
expect(screen.getByText('All good')).toBeInTheDocument();
|
||
});
|
||
|
||
it('ContributionBars returns null when empty', () => {
|
||
const { container } = render(<ContributionBars bars={[]} />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('ContributionBars renders rows', () => {
|
||
render(
|
||
<ContributionBars
|
||
bars={[{ id: '1', name: 'A', hashrate: 1000, pct: 50 }]}
|
||
xmrPerDay={0.01}
|
||
xmrPrice={100}
|
||
/>
|
||
);
|
||
expect(screen.getByRole('heading', { name: /Contribution Map/i })).toBeInTheDocument();
|
||
expect(screen.getByText('A')).toBeInTheDocument();
|
||
});
|
||
|
||
it('OSArchBreakdown returns null when empty', () => {
|
||
const { container } = render(<OSArchBreakdown platforms={[]} />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('LANGroupView needs at least two groups', () => {
|
||
const { container } = render(
|
||
<LANGroupView groups={[{ subnet: '10.0.0.0/24', agents: [mockAgent()] }]} />
|
||
);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('EarningsEstimator returns null when hashrate zero', () => {
|
||
const { container } = render(<EarningsEstimator hashrate={0} xmrPrice={100} />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('UnderperformerList restart calls bulk API', async () => {
|
||
const agents = [mockAgent({ id: 'u1', name: 'Slow' })];
|
||
const bulk = vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({ success: true, sent: 1, failed: 0, action: 'restart' });
|
||
render(<UnderperformerList underperformers={agents} medianHashrate={10000} />);
|
||
await userEvent.setup().click(screen.getByRole('button', { name: /Restart All/i }));
|
||
await waitFor(() => expect(bulk).toHaveBeenCalledWith(['u1'], 'restart'));
|
||
expect(await screen.findByText(/Restart sent to 1/i)).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('ForgeFieldHints', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('ForgeLockedHint null without meta', () => {
|
||
const { container } = render(<ForgeLockedHint />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('ForgeFieldBadge shows baked label', () => {
|
||
render(<ForgeFieldBadge meta={{ badge: 'baked' }} />);
|
||
expect(screen.getByText('⛏ baked')).toBeInTheDocument();
|
||
});
|
||
|
||
it('ForgeSectionHeader renders title and badge', () => {
|
||
render(
|
||
<ForgeSectionHeader title="Pool" description="Pool settings" badge="server-only" />
|
||
);
|
||
expect(screen.getByRole('heading', { name: 'Pool' })).toBeInTheDocument();
|
||
expect(screen.getByText(/server/i)).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('VisualComponents', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('PipelineFlow highlights active step', () => {
|
||
render(<PipelineFlow activeStep="forge" />);
|
||
expect(document.querySelector('.pipeline-active')).toBeTruthy();
|
||
});
|
||
|
||
it('FleetPipelineStatus shows step labels', () => {
|
||
render(
|
||
<FleetPipelineStatus
|
||
hasBuilds
|
||
agentCount={2}
|
||
onlineCount={1}
|
||
hasHashrate
|
||
hasShares={false}
|
||
/>
|
||
);
|
||
expect(screen.getByText('Forged')).toBeInTheDocument();
|
||
expect(screen.getByText('Shares')).toBeInTheDocument();
|
||
});
|
||
|
||
it('ActivityPulse empty message', () => {
|
||
render(<ActivityPulse items={[]} />);
|
||
expect(screen.getByText(/Awaiting fleet activity/i)).toBeInTheDocument();
|
||
});
|
||
|
||
it('ForgeCalibrateCompare links to routes', () => {
|
||
render(
|
||
<MemoryRouter>
|
||
<ForgeCalibrateCompare />
|
||
</MemoryRouter>
|
||
);
|
||
expect(screen.getByText(/Forge — baked into each binary/i)).toBeInTheDocument();
|
||
});
|
||
|
||
it('RoadmapGrid lists features', () => {
|
||
render(<RoadmapGrid />);
|
||
expect(document.querySelectorAll('.roadmap-card').length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('MatrixStreamOverlay', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
useWebSocketMock.mockReturnValue({
|
||
isConnected: true,
|
||
agents: [],
|
||
recentShares: [],
|
||
fleetAlerts: [],
|
||
poolStatus: [],
|
||
aiActivity: [],
|
||
agentLogs: {},
|
||
commandResults: [],
|
||
latestMessage: null,
|
||
});
|
||
});
|
||
|
||
it('returns null when inactive', () => {
|
||
const { container } = render(<MatrixStreamOverlay active={false} onClose={vi.fn()} />);
|
||
expect(container.firstChild).toBeNull();
|
||
});
|
||
|
||
it('shows overlay heading when active', () => {
|
||
render(<MatrixStreamOverlay active onClose={vi.fn()} />);
|
||
expect(screen.getByText('RAW_SOCKET_STREAM [ACTIVE]')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('SystemStatusBar', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
vi.spyOn(api, 'healthCheck').mockResolvedValue(undefined);
|
||
vi.spyOn(api, 'listAgents').mockResolvedValue([mockAgent(), mockAgent({ id: 'a2', status: 'offline' })]);
|
||
vi.spyOn(api, 'listBuilds').mockResolvedValue([{ id: 'b1' } as never]);
|
||
});
|
||
|
||
it('shows server and fleet pills after poll', async () => {
|
||
render(
|
||
<MemoryRouter>
|
||
<SystemStatusBar />
|
||
</MemoryRouter>
|
||
);
|
||
await waitFor(() => {
|
||
expect(screen.getByText(/SERVER UP/i)).toBeInTheDocument();
|
||
});
|
||
expect(screen.getByText(/FLEET 1\/2 ONLINE/i)).toBeInTheDocument();
|
||
expect(screen.getByText(/1 BUILD/i)).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('FleetTopologyMap', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('renders canvas wrapper for agents', () => {
|
||
render(<FleetTopologyMap agents={[mockAgent()]} />);
|
||
expect(screen.getByTestId('three-canvas')).toBeInTheDocument();
|
||
});
|
||
});
|
||
|
||
describe('AmbientBackground', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('renders ambient layers and sacred geometry svg', () => {
|
||
const { container } = render(<AmbientBackground />);
|
||
expect(container.querySelector('.ambient-bg')).toBeTruthy();
|
||
expect(container.querySelector('.ambient-sacred-geo')).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
describe('CursorFire', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
it('mounts fullscreen canvas', () => {
|
||
const { container } = render(<CursorFire />);
|
||
expect(container.querySelector('canvas')).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
describe('MatrixRain', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
useWebSocketMock.mockReturnValue({
|
||
isConnected: true,
|
||
agents: [],
|
||
recentShares: [],
|
||
fleetAlerts: [],
|
||
poolStatus: [],
|
||
aiActivity: [],
|
||
agentLogs: {},
|
||
commandResults: [],
|
||
latestMessage: null,
|
||
});
|
||
});
|
||
|
||
it('renders matrix rain canvas wrapper', () => {
|
||
const { container } = render(<MatrixRain />);
|
||
expect(container.querySelector('canvas')).toBeTruthy();
|
||
});
|
||
});
|
||
|
||
describe('Layout', () => {
|
||
afterEach(() => cleanup());
|
||
|
||
beforeEach(() => {
|
||
useWebSocketMock.mockReturnValue({
|
||
isConnected: true,
|
||
agents: [mockAgent()],
|
||
recentShares: [],
|
||
fleetAlerts: [],
|
||
poolStatus: [],
|
||
aiActivity: [],
|
||
agentLogs: {},
|
||
commandResults: [],
|
||
latestMessage: null,
|
||
});
|
||
vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo);
|
||
});
|
||
|
||
it('renders nav links and children', async () => {
|
||
render(
|
||
<MemoryRouter initialEntries={['/dashboard']}>
|
||
<Layout>
|
||
<div>page body</div>
|
||
</Layout>
|
||
</MemoryRouter>
|
||
);
|
||
await waitFor(() => {
|
||
expect(screen.getByText('page body')).toBeInTheDocument();
|
||
});
|
||
expect(screen.getByRole('link', { name: /Command Deck/i })).toBeInTheDocument();
|
||
expect(screen.getByRole('link', { name: /Fleet Roster/i })).toBeInTheDocument();
|
||
});
|
||
});
|