import { render, screen } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import CameraCard from './CameraCard' import type { CameraStatus } from '../types' function makeCamera(overrides: Partial = {}): CameraStatus { return { camera_id: 'cam-1', friendly_name: 'Front Camera', battery_pct: 85, video_remaining_sec: 3600, recording: false, mode: 'video', resolution: '1080p', fps: 30, online: true, last_seen: new Date().toISOString(), ...overrides, } } const noop = vi.fn() const renderCard = (overrides?: Partial) => render() const renderCardContainer = (camera: CameraStatus) => render() describe('CameraCard', () => { // ── Basic rendering ──────────────────────────────────────────────────── it('renders camera name', () => { renderCard() expect(screen.getByText('Front Camera')).toBeInTheDocument() }) it('shows resolution and FPS', () => { renderCard() expect(screen.getByText(/1080p/)).toBeInTheDocument() expect(screen.getByText(/30\s*FPS/)).toBeInTheDocument() }) it('shows battery percentage', () => { renderCard({ battery_pct: 85 }) expect(screen.getByText('85%')).toBeInTheDocument() }) it('shows N/A when battery is null', () => { renderCard({ battery_pct: null }) expect(screen.getByText('N/A')).toBeInTheDocument() }) // ── Battery bar colors ───────────────────────────────────────────────── it('uses green bar for high battery (>=50%)', () => { const { container } = renderCard({ battery_pct: 85 }) const bar = container.querySelector('[role="progressbar"] div') expect(bar?.className).toContain('bg-rig-success') }) it('uses yellow bar for medium battery (15-49%)', () => { const { container } = renderCard({ battery_pct: 30 }) const bar = container.querySelector('[role="progressbar"] div') expect(bar?.className).toContain('bg-rig-warning') }) it('uses red bar for low battery (<15%)', () => { const { container } = renderCard({ battery_pct: 8 }) const bar = container.querySelector('[role="progressbar"] div') expect(bar?.className).toContain('bg-rig-danger') }) // ── Recording state ──────────────────────────────────────────────────── it('shows REC badge when recording', () => { renderCard({ recording: true }) expect(screen.getByText('REC')).toBeInTheDocument() }) it('shows IDLE badge when not recording', () => { renderCard({ recording: false }) expect(screen.getByText('IDLE')).toBeInTheDocument() }) // ── Online / Offline badges ──────────────────────────────────────────── it('shows Online badge when camera is online', () => { renderCard({ online: true }) expect(screen.getByText('Online')).toBeInTheDocument() }) it('shows Offline badge when camera is offline', () => { renderCard({ online: false }) const offlineElements = screen.getAllByText('Offline') expect(offlineElements.length).toBeGreaterThanOrEqual(1) }) // ── Video remaining ──────────────────────────────────────────────────── it('shows video remaining time when available', () => { renderCard({ video_remaining_sec: 125 }) // formatTimeLeft(125) → "2m 5s left" expect(screen.getByText(/2m 5s left/)).toBeInTheDocument() }) it('does not show video remaining when null', () => { renderCard({ video_remaining_sec: null }) // The Radio icon and time text should not be present expect(screen.queryByText(/m\s+\d+s left/)).not.toBeInTheDocument() }) // ── Footer ───────────────────────────────────────────────────────────── it('shows Live + timestamp in footer when online', () => { renderCard({ online: true }) expect(screen.getByText('Live')).toBeInTheDocument() }) it('shows Offline in footer when offline', () => { renderCard({ online: false }) const offlineElements = screen.getAllByText('Offline') expect(offlineElements.length).toBeGreaterThanOrEqual(1) }) it('shows "unknown" when last_seen is malformed', () => { renderCard({ last_seen: 'not-a-date' }) expect(screen.getByText('unknown')).toBeInTheDocument() }) it('shows "unknown" when last_seen is in the future', () => { const future = new Date(Date.now() + 86400000).toISOString() // +1 day const cam = makeCamera({ last_seen: future }) renderCardContainer(cam) expect(screen.getByText('unknown')).toBeInTheDocument() }) // ── Edge cases ────────────────────────────────────────────────────────── it('clamps negative battery_pct to 0%', () => { renderCard({ battery_pct: -5 }) expect(screen.getByText('0%')).toBeInTheDocument() }) it('shows exact boundary: 15% battery → yellow bar', () => { const { container } = renderCard({ battery_pct: 15 }) const bar = container.querySelector('[role="progressbar"] div') expect(bar?.className).toContain('bg-rig-warning') }) it('shows exact boundary: 50% battery → green bar', () => { const { container } = renderCard({ battery_pct: 50 }) const bar = container.querySelector('[role="progressbar"] div') expect(bar?.className).toContain('bg-rig-success') }) // ── New prop-driven tests ────────────────────────────────────────────── it('calls onStart when Record button is clicked', () => { const onStart = vi.fn() render() screen.getByText('Record').click() expect(onStart).toHaveBeenCalledWith('cam-1') }) it('calls onStop when Stop button is clicked', () => { const onStop = vi.fn() render() screen.getByText('Stop').click() expect(onStop).toHaveBeenCalledWith('cam-1') }) it('calls onViewHistory when History button is clicked', () => { const onViewHistory = vi.fn() render() screen.getByText('History').click() expect(onViewHistory).toHaveBeenCalledWith('cam-1') }) })