import { render, screen, fireEvent } from '@testing-library/react' import { SessionCard, formatRelative } from '../session/SessionCard' import { SessionList } from '../session/SessionList' import { SessionStatusBadge } from '../session/SessionStatusBadge' import { resolveSessionStatus, mapSocketStatus } from '../session/status' import type { SessionSummary } from '../session/types' const base: SessionSummary = { id: 's1', number: 7, name: 'Add billing page', isActive: true, projectName: 'raise-simpli', agentName: 'Claude', branch: 'feat/billing', createdAt: '2026-07-01T00:00:00.000Z', } // --------------------------------------------------------------------------- // status helpers (pure) // --------------------------------------------------------------------------- describe('resolveSessionStatus', () => { it('prefers an explicit status kind + label', () => { const r = resolveSessionStatus({ status: 'error', statusLabel: 'Crashed', isActive: false }) expect(r.kind).toBe('error') expect(r.label).toBe('Crashed') expect(r.className).toContain('red') }) it('derives active/idle from isActive when status is absent', () => { expect(resolveSessionStatus({ isActive: true }).kind).toBe('active') expect(resolveSessionStatus({ isActive: false }).kind).toBe('idle') }) it('supplies a default label per kind', () => { expect(resolveSessionStatus({ status: 'success', isActive: false }).label).toBe('Success') }) }) describe('mapSocketStatus', () => { it('maps agent socket status onto the terminal status', () => { expect(mapSocketStatus('open')).toBe('connected') expect(mapSocketStatus('connecting')).toBe('connecting') expect(mapSocketStatus('reconnecting')).toBe('connecting') expect(mapSocketStatus('idle')).toBe('disconnected') expect(mapSocketStatus('closed')).toBe('disconnected') }) }) describe('formatRelative', () => { it('formats a valid timestamp and passes an unparseable one through', () => { expect(formatRelative('2026-07-01T00:00:00.000Z')).toMatch(/ago|in /) expect(formatRelative('not-a-date')).toBe('not-a-date') }) }) // --------------------------------------------------------------------------- // SessionStatusBadge // --------------------------------------------------------------------------- describe('SessionStatusBadge', () => { it('renders the resolved status label and tags the kind', () => { render() const badge = screen.getByRole('status') expect(badge).toHaveAttribute('data-status', 'warning') expect(badge).toHaveTextContent('Needs review') }) it('falls back to isActive when no status is given', () => { render() expect(screen.getByRole('status')).toHaveAttribute('data-status', 'active') }) }) // --------------------------------------------------------------------------- // SessionCard // --------------------------------------------------------------------------- describe('SessionCard', () => { it('renders name, branch, project, agent, number, and status', () => { render() expect(screen.getByText('Add billing page')).toBeInTheDocument() expect(screen.getByText('feat/billing')).toBeInTheDocument() expect(screen.getByText('raise-simpli')).toBeInTheDocument() expect(screen.getByText('Claude')).toBeInTheDocument() expect(screen.getByText('007')).toBeInTheDocument() // zero-padded number expect(screen.getByRole('status')).toHaveTextContent('Active') }) it('renders the title as a link when href is provided', () => { render( `/sessions/${id}`} />) const link = screen.getByRole('link', { name: 'Add billing page' }) expect(link).toHaveAttribute('href', '/sessions/s1') }) it('calls onOpen when clicked (no href)', () => { const onOpen = jest.fn() render() fireEvent.click(screen.getByTestId('session-card')) expect(onOpen).toHaveBeenCalledWith('s1') }) it('calls onOpenTerminal from the Terminal action without bubbling to the card', () => { const onOpen = jest.fn() const onOpenTerminal = jest.fn() render() fireEvent.click(screen.getByRole('button', { name: /terminal/i })) expect(onOpenTerminal).toHaveBeenCalledWith('s1') expect(onOpen).not.toHaveBeenCalled() }) it('shows an unread dot when hasUnread', () => { render() expect(screen.getByTestId('session-card-unread')).toBeInTheDocument() }) }) // --------------------------------------------------------------------------- // SessionList // --------------------------------------------------------------------------- const sessions: SessionSummary[] = [ base, { id: 's2', name: 'Fix auth bug', isActive: false, status: 'success', projectName: 'auth-web', branch: 'fix/auth', createdAt: '2026-07-02T00:00:00.000Z' }, { id: 's3', name: 'Draft README', isActive: false, status: 'idle', projectName: 'raise-simpli', createdAt: '2026-07-03T00:00:00.000Z' }, ] describe('SessionList', () => { it('renders a card grid by default', () => { render() expect(screen.getAllByTestId('session-card')).toHaveLength(3) expect(screen.getByText('Fix auth bug')).toBeInTheDocument() }) it('renders a flat table layout', () => { render() expect(screen.getAllByTestId('session-row')).toHaveLength(3) }) it('groups rows by project when groupByProject in table layout', () => { render() // two project groups: raise-simpli (2) + auth-web (1) expect(screen.getByText('raise-simpli')).toBeInTheDocument() expect(screen.getByText('auth-web')).toBeInTheDocument() expect(screen.getByText('2 sessions')).toBeInTheDocument() expect(screen.getByText('1 session')).toBeInTheDocument() }) it('collapses / expands a project group on header click', () => { render() expect(screen.getAllByTestId('session-row')).toHaveLength(3) // collapse the raise-simpli group (its header carries the "2 sessions" count) fireEvent.click(screen.getByText('raise-simpli')) expect(screen.getAllByTestId('session-row')).toHaveLength(1) }) it('fires onSessionClick from a row', () => { const onSessionClick = jest.fn() render() fireEvent.click(screen.getByText('Fix auth bug')) expect(onSessionClick).toHaveBeenCalledWith(expect.objectContaining({ id: 's2' })) }) it('renders loading skeletons', () => { const { container } = render() expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0) }) it('renders an error state', () => { render() expect(screen.getByTestId('session-list-error')).toHaveTextContent('nope') }) it('renders an empty state with a custom label', () => { render() expect(screen.getByTestId('session-list-empty')).toHaveTextContent('No runs yet') }) it('renders injected filters above the list', () => { render(my-filters} />) expect(screen.getByText('my-filters')).toBeInTheDocument() }) })