import { describe, it, expect, vi } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/react'; import type { ComponentProps } from 'react'; import { StackRow } from '../StackRow'; import type { Label } from '@/components/label-types'; function base(overrides: Partial> = {}) { return { file: 'web.yml', displayName: 'web', status: 'running' as const, isBusy: false, isActive: false, labels: [] as Label[], hasUpdate: false, hasGitPending: false, onSelect: vi.fn(), kebabSlot: null, ...overrides, }; } describe('StackRow', () => { it('renders UP for running', () => { render(); expect(screen.getByText('UP')).toBeInTheDocument(); }); it('renders DN for exited', () => { render(); expect(screen.getByText('DN')).toBeInTheDocument(); }); it('renders -- for unknown', () => { render(); expect(screen.getByText('--')).toBeInTheDocument(); }); it('renders PT with the amber class for partial', () => { const { container } = render(); expect(screen.getByText('PT')).toBeInTheDocument(); expect(container.querySelector('.text-warning')).not.toBeNull(); }); it('wraps the partial pill in a hover tooltip', () => { render(); const trigger = screen.getByText('PT'); // Radix TooltipTrigger adds data-state to the wrapped element. expect(trigger.getAttribute('data-state')).toBe('closed'); }); it('does not wrap a non-partial pill in a tooltip', () => { render(); expect(screen.getByText('UP').getAttribute('data-state')).toBeNull(); }); it('renders cyan rail only when active', () => { const { rerender } = render(); expect(screen.getByTestId('stack-row')).not.toHaveClass('bg-accent/[0.07]'); rerender(); expect(screen.getByTestId('stack-row')).toHaveClass('bg-accent/[0.07]'); }); it('fires onSelect on click', () => { const onSelect = vi.fn(); render(); screen.getByTestId('stack-row').click(); expect(onSelect).toHaveBeenCalledWith('web.yml'); }); it('fires onSelect on Enter and Space', () => { const onSelect = vi.fn(); render(); const row = screen.getByTestId('stack-row'); row.focus(); fireEvent.keyDown(row, { key: 'Enter' }); fireEvent.keyDown(row, { key: ' ' }); expect(onSelect).toHaveBeenCalledTimes(2); }); it('kebab click does not trigger onSelect', () => { const onSelect = vi.fn(); render(k })} />); screen.getByTestId('kebab').click(); expect(onSelect).not.toHaveBeenCalled(); }); it('renders loader when isBusy', () => { const { container } = render(); expect(container.querySelector('.animate-spin')).not.toBeNull(); expect(screen.queryByText('UP')).not.toBeInTheDocument(); }); it('does not render trailing label color dots when labels are assigned', () => { const labels: Label[] = [ { id: 1, node_id: 0, name: 'prod', color: 'teal' }, { id: 2, node_id: 0, name: 'media', color: 'blue' }, ]; const { container } = render(); expect(container.querySelectorAll('[style*="--label-"]')).toHaveLength(0); }); // ── Image-update check status indicator ──────────────────────────────── it('shows a muted check-failed indicator when the last check failed and there is no update', () => { const { container } = render(); // The trailing slot renders a tooltip-wrapped AlertCircle icon (an SVG). const trailingSlot = container.querySelector('[data-state="closed"]'); expect(trailingSlot).not.toBeNull(); // It is not the update dot. expect(container.querySelector('.bg-update')).toBeNull(); }); it('prefers the update dot over the check-failed indicator', () => { const { container } = render(); expect(container.querySelector('.bg-update')).not.toBeNull(); }); it('shows no trailing indicator for a clean ok check with no update', () => { const { container } = render(); expect(container.querySelector('.lucide-alert-circle')).toBeNull(); expect(container.querySelector('.bg-update')).toBeNull(); }); });