From 399f6f0bed3ca518f81c8b27bf106266c4c528fa Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 09:36:53 -0800 Subject: [PATCH] test(admin-web): Playwright E2E tests for diagnostics (Phase 4.4) - Debug session CRUD: create, view, pause, resume, cancel - Session filtering and search - Logs viewing with level filtering and search - Trace span viewing with expansion - Screenshot gallery and lightbox navigation - End-to-end workflow tests - Error threshold and crash-triggered auto-session tests --- dashboards/admin-web/e2e/diagnostics.spec.ts | 368 ++++++++++++++++++ .../app/(dashboard)/debug-sessions/page.tsx | 359 +++++++++++++++++ 2 files changed, 727 insertions(+) create mode 100644 dashboards/admin-web/e2e/diagnostics.spec.ts create mode 100644 dashboards/admin-web/src/app/(dashboard)/debug-sessions/page.tsx diff --git a/dashboards/admin-web/e2e/diagnostics.spec.ts b/dashboards/admin-web/e2e/diagnostics.spec.ts new file mode 100644 index 00000000..fbe23a65 --- /dev/null +++ b/dashboards/admin-web/e2e/diagnostics.spec.ts @@ -0,0 +1,368 @@ +import { test, expect } from '@playwright/test'; + +const ADMIN_EMAIL = 'admin@example.com'; +const ADMIN_PASSWORD = 'Admin123!'; + +async function loginAsAdmin(page: any) { + await page.goto('/login'); + await page.getByLabel('Email').fill(ADMIN_EMAIL); + await page.getByLabel('Password').fill(ADMIN_PASSWORD); + await page.getByRole('button', { name: 'Sign In' }).click(); + await page.waitForURL('**/dashboard', { timeout: 10000 }); +} + +test.describe('Diagnostics - Debug Sessions', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('navigates to diagnostics page', async ({ page }) => { + await page.click('text=Diagnostics'); + await expect(page.getByText('Debug Sessions')).toBeVisible(); + await expect(page.getByText('Create Session')).toBeVisible(); + }); + + test('shows session list with filters', async ({ page }) => { + await page.click('text=Diagnostics'); + + // Check filter dropdowns exist + await expect(page.getByLabel('Status')).toBeVisible(); + await expect(page.getByLabel('Collection Level')).toBeVisible(); + + // Check table headers + await expect(page.getByText('Session ID')).toBeVisible(); + await expect(page.getByText('Status')).toBeVisible(); + await expect(page.getByText('Target')).toBeVisible(); + await expect(page.getByText('Logs')).toBeVisible(); + await expect(page.getByText('Traces')).toBeVisible(); + await expect(page.getByText('Created')).toBeVisible(); + }); + + test('creates new debug session', async ({ page }) => { + await page.click('text=Diagnostics'); + await page.click('text=Create Session'); + + // Should open modal + await expect(page.getByText('Create Debug Session')).toBeVisible(); + + // Fill form + await page.getByLabel('Target User ID').fill('user_test_123'); + await page.getByLabel('Target Device ID').fill('device_test_456'); + + // Select collection level + await page.getByLabel('Collection Level').selectOption('debug'); + + // Enable capture options + await page.getByLabel('Capture Logs').check(); + await page.getByLabel('Capture Network').check(); + await page.getByLabel('Capture Screenshots').check(); + + // Set duration + await page.getByLabel('Max Duration (minutes)').fill('30'); + + // Submit + await page.click('text=Start Session'); + + // Should show success and close modal + await expect(page.getByText('Session created')).toBeVisible({ timeout: 5000 }); + }); + + test('views session details', async ({ page }) => { + await page.click('text=Diagnostics'); + + // Click on first session + await page.locator('table tbody tr').first().click(); + + // Should show detail panel + await expect(page.getByText('Session Details')).toBeVisible(); + await expect(page.getByText('Status')).toBeVisible(); + await expect(page.getByText('Collection Level')).toBeVisible(); + await expect(page.getByText('Logs')).toBeVisible(); + await expect(page.getByText('Traces')).toBeVisible(); + await expect(page.getByText('Screenshots')).toBeVisible(); + }); + + test('pauses and resumes session', async ({ page }) => { + await page.click('text=Diagnostics'); + + // Find an active session + const activeRow = page.locator('tr:has-text("Active")').first(); + + if (await activeRow.isVisible().catch(() => false)) { + // Click pause + await activeRow.getByRole('button', { name: 'Pause' }).click(); + await expect(page.getByText('Session paused')).toBeVisible(); + + // Resume + await page.locator('tr:has-text("Paused")').first().getByRole('button', { name: 'Resume' }).click(); + await expect(page.getByText('Session resumed')).toBeVisible(); + } + }); + + test('cancels session', async ({ page }) => { + await page.click('text=Diagnostics'); + + // Find a pending or active session + const sessionRow = page.locator('tr:has-text("Pending"), tr:has-text("Active")').first(); + + if (await sessionRow.isVisible().catch(() => false)) { + await sessionRow.getByRole('button', { name: 'Cancel' }).click(); + + // Confirm cancel + await page.getByLabel('Reason (optional)').fill('Test cancellation'); + await page.click('text=Confirm Cancel'); + + await expect(page.getByText('Session cancelled')).toBeVisible(); + } + }); + + test('filters sessions by status', async ({ page }) => { + await page.click('text=Diagnostics'); + + // Filter by Active + await page.getByLabel('Status').selectOption('active'); + + // Should only show active sessions + const rows = page.locator('table tbody tr'); + const count = await rows.count(); + + for (let i = 0; i < count; i++) { + await expect(rows.nth(i).locator('td').nth(1)).toHaveText('Active'); + } + }); + + test('searches sessions by user ID', async ({ page }) => { + await page.click('text=Diagnostics'); + + // Search + await page.getByPlaceholder('Search by user/device ID').fill('test_user'); + await page.click('text=Search'); + + // Results should contain search term + const firstRow = page.locator('table tbody tr').first(); + if (await firstRow.isVisible().catch(() => false)) { + await expect(firstRow).toContainText('test_user'); + } + }); +}); + +test.describe('Diagnostics - Logs & Traces', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.click('text=Diagnostics'); + }); + + test('views session logs', async ({ page }) => { + // Click on a session with logs + const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first(); + if (await sessionWithLogs.isVisible().catch(() => false)) { + await sessionWithLogs.click(); + + // Click Logs tab + await page.click('text=Logs'); + + // Should show log entries + await expect(page.locator('[data-testid="log-entry"]').first()).toBeVisible(); + + // Check log level badges + await expect(page.getByText('INFO').or(page.getByText('ERROR')).or(page.getByText('DEBUG'))).toBeVisible(); + } + }); + + test('filters logs by level', async ({ page }) => { + // Open a session and go to logs + const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first(); + if (await sessionWithLogs.isVisible().catch(() => false)) { + await sessionWithLogs.click(); + await page.click('text=Logs'); + + // Filter by ERROR + await page.getByLabel('Log Level').selectOption('error'); + + // All visible logs should be ERROR level + const logs = page.locator('[data-testid="log-entry"]'); + const count = await logs.count(); + for (let i = 0; i < count; i++) { + await expect(logs.nth(i).locator('[data-level]')).toHaveAttribute('data-level', 'error'); + } + } + }); + + test('searches logs by message', async ({ page }) => { + const sessionWithLogs = page.locator('tr:has([data-logs="true"])').first(); + if (await sessionWithLogs.isVisible().catch(() => false)) { + await sessionWithLogs.click(); + await page.click('text=Logs'); + + // Search + await page.getByPlaceholder('Search logs...').fill('error'); + await page.click('text=Search'); + + // Results should contain search term + const firstLog = page.locator('[data-testid="log-entry"]').first(); + if (await firstLog.isVisible().catch(() => false)) { + await expect(firstLog).toContainText('error'); + } + } + }); + + test('views trace spans', async ({ page }) => { + const sessionWithTraces = page.locator('tr:has([data-traces="true"])').first(); + if (await sessionWithTraces.isVisible().catch(() => false)) { + await sessionWithTraces.click(); + await page.click('text=Traces'); + + // Should show trace tree + await expect(page.locator('[data-testid="trace-span"]').first()).toBeVisible(); + + // Should show span details + await expect(page.getByText('Duration')).toBeVisible(); + await expect(page.getByText('Status')).toBeVisible(); + } + }); + + test('expands trace span details', async ({ page }) => { + const sessionWithTraces = page.locator('tr:has([data-traces="true"])').first(); + if (await sessionWithTraces.isVisible().catch(() => false)) { + await sessionWithTraces.click(); + await page.click('text=Traces'); + + // Click to expand first span + const firstSpan = page.locator('[data-testid="trace-span"]').first(); + await firstSpan.click(); + + // Should show span details + await expect(page.getByText('Attributes')).toBeVisible(); + await expect(page.getByText('Events')).toBeVisible(); + } + }); +}); + +test.describe('Diagnostics - Screenshots', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.click('text=Diagnostics'); + }); + + test('views screenshots gallery', async ({ page }) => { + const sessionWithScreenshots = page.locator('tr:has([data-screenshots="true"])').first(); + if (await sessionWithScreenshots.isVisible().catch(() => false)) { + await sessionWithScreenshots.click(); + await page.click('text=Screenshots'); + + // Should show screenshot thumbnails + await expect(page.locator('[data-testid="screenshot-thumb"]').first()).toBeVisible(); + } + }); + + test('opens screenshot lightbox', async ({ page }) => { + const sessionWithScreenshots = page.locator('tr:has([data-screenshots="true"])').first(); + if (await sessionWithScreenshots.isVisible().catch(() => false)) { + await sessionWithScreenshots.click(); + await page.click('text=Screenshots'); + + // Click first screenshot + await page.locator('[data-testid="screenshot-thumb"]').first().click(); + + // Lightbox should open + await expect(page.locator('[data-testid="screenshot-lightbox"]')).toBeVisible(); + await expect(page.locator('[data-testid="screenshot-full"]').first()).toBeVisible(); + } + }); + + test('navigates between screenshots', async ({ page }) => { + const sessionWithScreenshots = page.locator('tr:has([data-screenshots="true"])').first(); + if (await sessionWithScreenshots.isVisible().catch(() => false)) { + await sessionWithScreenshots.click(); + await page.click('text=Screenshots'); + + // Open lightbox + await page.locator('[data-testid="screenshot-thumb"]').first().click(); + + // Navigate next + await page.click('text=Next'); + await expect(page.getByText('2 /')).toBeVisible(); + + // Navigate prev + await page.click('text=Previous'); + await expect(page.getByText('1 /')).toBeVisible(); + } + }); +}); + +test.describe('Diagnostics - End-to-End Flow', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('full workflow: create → capture → analyze', async ({ page }) => { + // 1. Create session + await page.click('text=Diagnostics'); + await page.click('text=Create Session'); + + const targetUserId = `test_user_${Date.now()}`; + await page.getByLabel('Target User ID').fill(targetUserId); + await page.getByLabel('Collection Level').selectOption('debug'); + await page.getByLabel('Capture Logs').check(); + await page.click('text=Start Session'); + + await expect(page.getByText('Session created')).toBeVisible(); + + // 2. Verify session appears in list + await expect(page.locator('table tbody tr').first()).toContainText(targetUserId); + + // 3. Open session details + await page.locator(`tr:has-text("${targetUserId}")`).first().click(); + await expect(page.getByText('Session Details')).toBeVisible(); + + // 4. View logs (if any captured) + await page.click('text=Logs'); + + // 5. Pause session + await page.click('text=Actions'); + await page.click('text=Pause'); + await expect(page.getByText('Session paused')).toBeVisible(); + + // 6. Resume and complete + await page.click('text=Actions'); + await page.click('text=Resume'); + await page.click('text=Actions'); + await page.click('text=Complete'); + await expect(page.getByText('Session completed')).toBeVisible(); + }); + + test('error threshold triggers auto-notification', async ({ page }) => { + // This would require backend simulation of error threshold + // For E2E, we verify the UI handles the notification + await page.click('text=Diagnostics'); + + // Look for notification about high error rate + const notification = page.locator('[data-testid="error-threshold-alert"]'); + if (await notification.isVisible().catch(() => false)) { + await expect(notification).toContainText('High error rate detected'); + + // Should offer to create debug session + await notification.getByRole('button', { name: 'Start Debug Session' }).click(); + await expect(page.getByText('Create Debug Session')).toBeVisible(); + } + }); + + test('crash-triggered auto-session', async ({ page }) => { + // Look for auto-created session after crash + await page.click('text=Diagnostics'); + + // Filter to show auto-created sessions + await page.getByLabel('Source').selectOption('auto_crash'); + + // Should show auto-created sessions + const autoSession = page.locator('tr:has-text("Auto-created")').first(); + if (await autoSession.isVisible().catch(() => false)) { + await expect(autoSession).toContainText('Crash detected'); + + // Open and verify it has crash data + await autoSession.click(); + await page.click('text=Logs'); + await expect(page.locator('[data-testid="log-entry"]')).toContainText('CRASH'); + } + }); +}); diff --git a/dashboards/admin-web/src/app/(dashboard)/debug-sessions/page.tsx b/dashboards/admin-web/src/app/(dashboard)/debug-sessions/page.tsx new file mode 100644 index 00000000..1ab512b4 --- /dev/null +++ b/dashboards/admin-web/src/app/(dashboard)/debug-sessions/page.tsx @@ -0,0 +1,359 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { Plus, Search, RefreshCw, MoreHorizontal } from 'lucide-react'; +import { createDiagnosticsClient, type DebugSession, type CreateSessionRequest } from '@/lib/diagnostics-client'; + +const statusColors: Record = { + pending: 'bg-yellow-500', + active: 'bg-green-500', + paused: 'bg-orange-500', + completed: 'bg-blue-500', + cancelled: 'bg-red-500', +}; + +export default function DebugSessionsPage() { + const [sessions, setSessions] = useState([]); + const [loading, setLoading] = useState(true); + const [statusFilter, setStatusFilter] = useState('all'); + const [searchQuery, setSearchQuery] = useState(''); + const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); + + // Helper to get auth token from localStorage + const getAuthToken = async () => { + if (typeof window === 'undefined') return ''; + return localStorage.getItem('admin_access_token') || ''; + }; + + // New session form state + const [newSession, setNewSession] = useState({ + targetUserId: '', + targetDeviceId: '', + collectionLevel: 'standard' as const, + maxDurationMinutes: 60, + captureLogs: true, + captureNetwork: true, + captureScreenshots: false, + screenshotOnError: true, + }); + + const client = createDiagnosticsClient({ + baseURL: process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL || 'http://localhost:4003', + productId: 'lysnrai', + getAuthToken, + }); + + const fetchSessions = async () => { + setLoading(true); + try { + const result = await client.querySessions({ + productId: 'lysnrai', + status: statusFilter === 'all' ? undefined : statusFilter, + }); + setSessions(result.sessions); + } catch (error) { + console.error('Failed to fetch sessions:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchSessions(); + // Auto-refresh every 5 seconds + const interval = setInterval(fetchSessions, 5000); + return () => clearInterval(interval); + }, [statusFilter]); + + const handleCreateSession = async () => { + try { + await client.createSession(newSession); + setIsCreateModalOpen(false); + fetchSessions(); + // Reset form + setNewSession({ + targetUserId: '', + targetDeviceId: '', + collectionLevel: 'standard', + maxDurationMinutes: 60, + captureLogs: true, + captureNetwork: true, + captureScreenshots: false, + screenshotOnError: true, + }); + } catch (error) { + console.error('Failed to create session:', error); + } + }; + + const filteredSessions = sessions.filter((session) => { + if (searchQuery) { + const query = searchQuery.toLowerCase(); + return ( + session.id.toLowerCase().includes(query) || + session.targetUserId?.toLowerCase().includes(query) || + session.targetDeviceId?.toLowerCase().includes(query) + ); + } + return true; + }); + + return ( +
+
+
+

Debug Sessions

+

+ Remote diagnostics and debug tracing sessions +

+
+ + + + + + + Create Debug Session + + Start a remote debug session to collect logs, traces, and screenshots from a target device. + + +
+
+
+ + + setNewSession({ ...newSession, targetUserId: e.target.value }) + } + placeholder="user_123" + /> +
+
+ + + setNewSession({ ...newSession, targetDeviceId: e.target.value }) + } + placeholder="device_abc" + /> +
+
+
+ + +
+
+ + + setNewSession({ ...newSession, maxDurationMinutes: parseInt(e.target.value) }) + } + /> +
+
+
+ + setNewSession({ ...newSession, captureLogs: checked }) + } + /> + +
+
+ + setNewSession({ ...newSession, captureNetwork: checked }) + } + /> + +
+
+ + setNewSession({ ...newSession, captureScreenshots: checked }) + } + /> + +
+
+ + setNewSession({ ...newSession, screenshotOnError: checked }) + } + /> + +
+
+
+ + + + +
+
+
+ + + +
+ Sessions +
+
+ + setSearchQuery(e.target.value)} + /> +
+ + +
+
+
+ + + + + Session ID + Status + User + Device + Level + Duration + Started + + + + + {loading ? ( + + + Loading... + + + ) : filteredSessions.length === 0 ? ( + + + No sessions found + + + ) : ( + filteredSessions.map((session) => ( + + {session.id} + + + {session.status} + + + {session.targetUserId || '-'} + {session.targetDeviceId || '-'} + {session.collectionLevel} + {session.maxDurationMinutes} min + + {session.startedAt + ? new Date(session.startedAt).toLocaleString() + : '-'} + + + + + + )) + )} + +
+
+
+
+ ); +}