Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Fix macOS agent cross-compile (SilentAVExclusion) and Calibrate E2E nav selector; expand tests and docs; refresh portable usb binary and spread/wiki assets.
200 lines
7.5 KiB
TypeScript
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();
|
|
});
|
|
});
|