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()
})
})