From 1d6127fed30ba1d77bade46aa5f6f6dd96d10cf2 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Wed, 22 Apr 2026 23:44:05 +0200 Subject: [PATCH] fix(detail-panel): replace pixel math and fixed tooltip in ServiceBadge - Remove magic-number width calculations (maxTotalWidth, nameCharWidth) - Remove useState tooltip + position:fixed custom tooltip div - Use CSS flex/truncate for layout and Shadcn Tooltip for path hover - Add ServiceBadge rendering tests (name, port, path, link vs plain text) --- .../src/components/panels/DetailPanel.tsx | 8 ++-- .../panels/__tests__/DetailPanel.test.tsx | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/panels/DetailPanel.tsx b/frontend/src/components/panels/DetailPanel.tsx index ce832f1..6bc8c5a 100644 --- a/frontend/src/components/panels/DetailPanel.tsx +++ b/frontend/src/components/panels/DetailPanel.tsx @@ -660,10 +660,10 @@ const CATEGORY_COLORS: Record = { } function ServiceBadge({ svc, host, onEdit, onRemove }: { svc: ServiceInfo; host?: string; onEdit: () => void; onRemove: () => void }) { - const url = getServiceUrl(svc, host); - const color = CATEGORY_COLORS[svc.category ?? ''] ?? '#8b949e'; - const hasPort = svc.port != null; - const portLabel = hasPort ? String(svc.port) : ''; + const url = getServiceUrl(svc, host) + const color = CATEGORY_COLORS[svc.category ?? ''] ?? '#8b949e' + const hasPort = svc.port != null + const portLabel = hasPort ? String(svc.port) : '' const pathLabel = svc.path?.trim() ? svc.path.trim() : '' return ( diff --git a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx index e90c400..52c3c5f 100644 --- a/frontend/src/components/panels/__tests__/DetailPanel.test.tsx +++ b/frontend/src/components/panels/__tests__/DetailPanel.test.tsx @@ -445,4 +445,46 @@ describe('DetailPanel', () => { expect(screen.getByText(/192\.168\.1\.10, 192\.168\.1\.11/)).toBeDefined() }) }) + + describe('ServiceBadge rendering', () => { + it('renders service name and port/protocol label', () => { + setupStore({ services: [{ port: 8080, protocol: 'tcp', service_name: 'nginx', path: '' }] }) + render() + expect(screen.getByText('nginx')).toBeDefined() + expect(screen.getByText('8080/tcp')).toBeDefined() + }) + + it('renders path label when path is set', () => { + setupStore({ services: [{ port: 80, protocol: 'tcp', service_name: 'web', path: '/admin' }] }) + render() + expect(screen.getByText('/admin')).toBeDefined() + }) + + it('renders no path text when path is empty', () => { + setupStore({ services: [{ port: 80, protocol: 'tcp', service_name: 'web', path: '' }] }) + render() + expect(screen.queryByText('/')).toBeNull() + }) + + it('renders port/protocol omitted when port is absent', () => { + setupStore({ services: [{ protocol: 'tcp', service_name: 'health', path: '' }] }) + render() + expect(screen.getByText('health')).toBeDefined() + expect(screen.queryByText(/\/tcp/)).toBeNull() + }) + + it('renders service name as link when ip and port are set', () => { + setupStore({ ip: '192.168.1.10', services: [{ port: 8080, protocol: 'tcp', service_name: 'nginx', path: '' }] }) + render() + const link = screen.getByRole('link', { name: 'nginx' }) + expect(link.getAttribute('href')).toContain('192.168.1.10') + expect(link.getAttribute('target')).toBe('_blank') + }) + + it('renders service name as plain text when no url can be built', () => { + setupStore({ ip: undefined, services: [{ protocol: 'tcp', service_name: 'health', path: '' }] }) + render() + expect(screen.getByText('health').tagName).not.toBe('A') + }) + }) })