import { expect, test } from '@playwright/test'; import { loginToDashboard } from './fixtures'; const OFFLINE_AGENT = { id: 'e2e-offline-agent', name: 'Offline Node', wallet: '4' + 'A'.repeat(94), ip: '192.168.1.99', version: '1.0.0', status: 'offline', cpu_cores: 4, memory_gb: 8, last_seen: new Date(Date.now() - 3600_000).toISOString(), created_at: new Date().toISOString(), hashrate_15s: 0, hashrate_1m: 0, hashrate_15m: 0, shares_total: 0, shares_good: 0, shares_bad: 0, cpu_usage_pct: 0, memory_usage_pct: 0, uptime_seconds: 0, platform: 'windows', arch: 'amd64', }; test.describe('Remote actions UI', () => { test.beforeEach(async ({ page }) => { // AgentsPage syncs from WebSocket when connected; mock dashboard WS init (HTTP route cannot intercept WS). await page.addInitScript((agent) => { const RealWS = WebSocket; const g = globalThis as typeof globalThis & { __afRealWebSocket?: typeof WebSocket }; g.__afRealWebSocket = RealWS; globalThis.WebSocket = function (url: string | URL, protocols?: string | string[]) { const urlStr = String(url); if (!urlStr.includes('/ws/dashboard')) { return new g.__afRealWebSocket!(url, protocols); } let openHandler: (() => void) | null = null; let messageHandler: ((ev: MessageEvent) => void) | null = null; let closeHandler: (() => void) | null = null; const sock = { readyState: 0, send() {}, close() { sock.readyState = 3; closeHandler?.(); }, set onopen(fn: (() => void) | null) { openHandler = fn; }, get onopen() { return openHandler; }, set onmessage(fn: ((ev: MessageEvent) => void) | null) { messageHandler = fn; }, get onmessage() { return messageHandler; }, set onclose(fn: (() => void) | null) { closeHandler = fn; }, get onclose() { return closeHandler; }, set onerror(_fn: (() => void) | null) {}, get onerror() { return null; }, }; queueMicrotask(() => { sock.readyState = 1; openHandler?.(); messageHandler?.({ data: JSON.stringify({ type: 'init', payload: { agents: [agent] } }), } as MessageEvent); }); return sock as unknown as WebSocket; } as unknown as typeof WebSocket; globalThis.WebSocket.OPEN = 1; globalThis.WebSocket.CONNECTING = 0; globalThis.WebSocket.CLOSED = 3; }, OFFLINE_AGENT); await page.route(/\/api\/v1\/agents$/, async (route) => { if (route.request().method() !== 'GET') { await route.continue(); return; } await route.fulfill({ json: [OFFLINE_AGENT] }); }); await page.route('**/api/v1/agents/*/stats*', async (route) => { await route.fulfill({ json: [] }); }); await page.route('**/api/v1/builds', async (route) => { await route.fulfill({ json: [] }); }); await page.route('**/api/v1/server/info', async (route) => { await route.fulfill({ json: { version: 'test', suggested_url: 'http://127.0.0.1:8989', agent_count: 1, online_count: 0, }, }); }); await loginToDashboard(page); await page.getByRole('link', { name: /Fleet Roster/i }).click(); await expect(page.getByRole('heading', { name: 'Fleet Roster' })).toBeVisible({ timeout: 10_000 }); await expect(page.getByText('Offline Node')).toBeVisible({ timeout: 10_000 }); }); test('detail panel disables remote actions for offline agent', async ({ page }) => { await page.getByText('Offline Node').click(); const detail = page.locator('.agent-detail'); await expect(detail.getByRole('heading', { name: 'Remote Control' })).toBeVisible({ timeout: 10_000 }); await expect(detail.getByRole('button', { name: 'Screenshot' })).toBeDisabled(); await expect(detail.getByRole('button', { name: 'Pause' })).toBeDisabled(); }); test('compact row remote actions disabled when offline', async ({ page }) => { await page.getByText('Offline Node').click(); const compact = page.locator('.agent-list-item.expanded'); await expect(compact.getByRole('button', { name: 'Pause' })).toBeDisabled(); }); });