Files
AetherForge/server/web/src/components/components.test.tsx

796 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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 0100%', () => {
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();
});
});