Files
AetherForge/server/web/src/pages/DashboardPage.test.tsx
AetherForge 415b5dc6a3
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Release validation: tests green, USB pack, fleet UX and API hardening.
Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
2026-06-06 16:57:39 -07:00

200 lines
7.5 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 { MemoryRouter } from 'react-router-dom';
import DashboardPage, { formatShareTime } from './DashboardPage';
import { mockAgent, mockShare } from '../test/fixtures';
import { routerFuture } from '../routerFuture';
import { useWebSocket } from '../hooks/useWebSocket';
import { api } from '../api/client';
vi.mock('../hooks/useWebSocket', () => ({
useWebSocket: vi.fn(),
}));
vi.mock('../components/Visual/3D/FleetTopologyMap', () => ({
default: () => <div data-testid="fleet-topology-map" />,
}));
vi.mock('../components/Visual/MatrixStreamOverlay', () => ({
default: () => null,
}));
const useWebSocketMock = vi.mocked(useWebSocket);
function wsValue(overrides: Partial<ReturnType<typeof useWebSocket>> = {}) {
return {
isConnected: true,
agents: [],
recentShares: [],
fleetAlerts: [],
poolStatus: [],
aiActivity: [],
agentLogs: {},
commandResults: [],
latestMessage: null,
...overrides,
};
}
function renderDashboard() {
return render(
<MemoryRouter future={routerFuture}>
<DashboardPage />
</MemoryRouter>
);
}
describe('formatShareTime', () => {
it('formats ISO timestamps as locale time strings', () => {
const iso = '2026-05-30T12:00:00.000Z';
expect(formatShareTime(iso)).toBe(new Date(iso).toLocaleTimeString());
});
});
describe('DashboardPage', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
useWebSocketMock.mockReturnValue(wsValue());
vi.spyOn(api, 'getRecentShares').mockResolvedValue([]);
vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
vi.spyOn(api, 'getConfig').mockResolvedValue({
port: 8080,
data_dir: '',
pool: {} as never,
wallet: {} as never,
server: { dashboard_subtitle: 'custom subtitle from server' },
alerts: {} as never,
});
vi.spyOn(api, 'getAlerts').mockResolvedValue([]);
vi.spyOn(api, 'getPoolStatus').mockResolvedValue([]);
vi.spyOn(api, 'getAIActivity').mockResolvedValue([]);
vi.spyOn(api, 'getXmrPrice').mockResolvedValue({ usd: 165.5, updated_at: '' });
vi.spyOn(api, 'getEarningsEstimate').mockResolvedValue({
xmr_per_day: 0.01,
usd_per_day: 1.65,
network_hashrate: 1e9,
});
vi.spyOn(api, 'sendBulkCommand').mockResolvedValue({
success: true,
sent: 1,
failed: 0,
action: 'restart',
});
});
afterEach(() => {
cleanup();
});
it('renders hero heading and key section titles', () => {
renderDashboard();
expect(screen.getByRole('heading', { level: 1, name: 'Command Deck' })).toBeInTheDocument();
expect(screen.getByText('Fleet Pipeline')).toBeInTheDocument();
expect(screen.getByText('Share Activity Pulse')).toBeInTheDocument();
expect(screen.getByText('Machine Roster')).toBeInTheDocument();
expect(screen.getByText('PERSONAL NETWORK · LIVE TELEMETRY')).toBeInTheDocument();
});
it('shows reconnecting label when websocket is down', () => {
useWebSocketMock.mockReturnValue(wsValue({ isConnected: false }));
renderDashboard();
expect(screen.getByText('RECONNECTING')).toBeInTheDocument();
});
it('shows signal locked when websocket is connected', () => {
renderDashboard();
expect(screen.getByText('SIGNAL LOCKED')).toBeInTheDocument();
expect(screen.getByText('0 nodes registered')).toBeInTheDocument();
});
it('loads dashboard subtitle from config API', async () => {
renderDashboard();
expect(await screen.findByText('custom subtitle from server')).toBeInTheDocument();
expect(api.getConfig).toHaveBeenCalled();
});
it('shows empty roster state when no agents', async () => {
renderDashboard();
expect(await screen.findByText('No miners on the wire')).toBeInTheDocument();
});
it('does not show projection or fake earnings with no agents', async () => {
renderDashboard();
expect(await screen.findByText('Command Deck')).toBeInTheDocument();
expect(screen.queryByText(/Projection mode/i)).not.toBeInTheDocument();
expect(screen.queryByText('Target Fleet Earnings')).not.toBeInTheDocument();
expect(screen.queryByText('Vault-01')).not.toBeInTheDocument();
});
it('renders stat labels and top agent card', async () => {
const agent = mockAgent({ name: 'Alpha Node', hashrate_15m: 1200 });
useWebSocketMock.mockReturnValue(wsValue({ agents: [agent] }));
renderDashboard();
expect(await screen.findByText('Top Miner')).toBeInTheDocument();
expect(screen.getByText('Fleet Compute')).toBeInTheDocument();
expect(screen.getAllByText('Fleet Hash')[0]).toBeInTheDocument();
const roster = screen.getByText('Machine Roster').closest('section') as HTMLElement;
expect(within(roster).getByText('Alpha Node')).toBeInTheDocument();
expect(within(roster).getByText('online')).toBeInTheDocument();
});
it('toggles advanced mode and reveals share log section', async () => {
const user = userEvent.setup();
vi.spyOn(api, 'getRecentShares').mockResolvedValue([mockShare()]);
renderDashboard();
expect(screen.queryByText('Share Log')).not.toBeInTheDocument();
await user.click(screen.getByRole('button', { name: '[ADVANCED]' }));
expect(screen.getByText('Share Log')).toBeInTheDocument();
expect(localStorage.getItem('aether-dash-advanced')).toBe('1');
});
it('shows share log rows in advanced mode with stable keys', async () => {
const share = mockShare({ id: undefined as unknown as number, hash: 'deadbeef' });
vi.spyOn(api, 'getRecentShares').mockResolvedValue([share]);
localStorage.setItem('aether-dash-advanced', '1');
useWebSocketMock.mockReturnValue(wsValue({ agents: [mockAgent()] }));
renderDashboard();
expect(await screen.findByText('Share Log')).toBeInTheDocument();
expect(screen.getByText('Accepted')).toBeInTheDocument();
});
it('alerts when bulk command API fails', async () => {
const agent = mockAgent();
useWebSocketMock.mockReturnValue(wsValue({ agents: [agent] }));
vi.spyOn(api, 'sendBulkCommand').mockRejectedValue(new Error('network down'));
const alertSpy = vi.spyOn(window, 'alert').mockImplementation(() => {});
renderDashboard();
const roster = await screen.findByText('Machine Roster');
const section = roster.closest('section') as HTMLElement;
await waitFor(() => expect(within(section).getByText(agent.name)).toBeInTheDocument());
const user = userEvent.setup();
const grid = section.querySelector('.agent-grid') as HTMLElement;
await user.click(within(grid).getByRole('checkbox'));
const bulkBar = section.querySelector('.fleet-bulk-bar') as HTMLElement;
await user.click(within(bulkBar).getByRole('button', { name: 'Pause' }));
await waitFor(() => {
expect(alertSpy).toHaveBeenCalledWith('network down');
});
alertSpy.mockRestore();
});
it('prefers live websocket alerts over REST fallback', async () => {
useWebSocketMock.mockReturnValue(
wsValue({
fleetAlerts: [{ id: '1', level: 'warn', type: 'pool_down', message: 'live alert', timestamp: '' }],
})
);
vi.spyOn(api, 'getAlerts').mockResolvedValue([
{ id: '2', level: 'warn', type: 'stale', message: 'rest alert', timestamp: '' },
]);
renderDashboard();
expect(await screen.findByText('live alert')).toBeInTheDocument();
expect(screen.queryByText('rest alert')).not.toBeInTheDocument();
});
});