/** * Tests for CostView component. * * Coverage: * 1. Loading state. * 2. Empty state. * 3. Renders costs table with data. * 4. Shows summary values. * 5. Recompute button triggers confirmation modal and then recompute call. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { renderWithProviders } from '../test-utils' // --------------------------------------------------------------------------- // Mock apiClient // --------------------------------------------------------------------------- const mockGet = vi.fn() const mockPost = vi.fn() vi.mock('../api/client', () => ({ default: { GET: (...args: unknown[]) => mockGet(...args), POST: (...args: unknown[]) => mockPost(...args), PATCH: vi.fn(), DELETE: vi.fn(), }, ApiError: class ApiError extends Error { status: number body: unknown constructor(status: number, body: unknown) { super(`API error ${status}`) this.name = 'ApiError' this.status = status this.body = body } }, registerLoginRedirect: vi.fn(), })) // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const COST_PERIOD = { period_start: '2026-06-22T10:00:00Z', d1_kwh: 0.1, d2_kwh: 0.2, r1_kwh: 0.0, r2_kwh: 0.05, import_cost: 0.044, export_revenue: 0.005, net_cost: 0.039, currency: 'EUR', degraded: false, contract_version_id: 1, } const SUMMARY = { currency: 'EUR', metered_import: 10.5, metered_export: 2.3, metered_net: 8.2, fixed_costs: 5.0, credits: 50.0, total_payable: 12.5, } // --------------------------------------------------------------------------- // Import component // --------------------------------------------------------------------------- import { CostView } from './CostView' // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('CostView', () => { beforeEach(() => vi.clearAllMocks()) it('renders loading state initially', () => { mockGet.mockImplementation(() => new Promise(() => {})) renderWithProviders() expect(screen.getByTestId('costs-loading')).toBeInTheDocument() }) it('renders empty state when no cost periods available', async () => { mockGet.mockImplementation((path: string) => { if (path === '/api/energy/costs') { return Promise.resolve({ data: { items: [], total: 0 } }) } if (path === '/api/energy/costs/summary') { return Promise.resolve({ data: SUMMARY }) } return Promise.resolve({ data: null }) }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('costs-empty')).toBeInTheDocument() }) }) it('renders costs table when data is available', async () => { mockGet.mockImplementation((path: string) => { if (path === '/api/energy/costs') { return Promise.resolve({ data: { items: [COST_PERIOD], total: 1 } }) } if (path === '/api/energy/costs/summary') { return Promise.resolve({ data: SUMMARY }) } return Promise.resolve({ data: null }) }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('costs-table')).toBeInTheDocument() }) expect(screen.getByTestId('cost-row-0')).toBeInTheDocument() }) it('shows summary values correctly', async () => { mockGet.mockImplementation((path: string) => { if (path === '/api/energy/costs') { return Promise.resolve({ data: { items: [], total: 0 } }) } if (path === '/api/energy/costs/summary') { return Promise.resolve({ data: SUMMARY }) } return Promise.resolve({ data: null }) }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('summary-import')).toBeInTheDocument() }) expect(screen.getByTestId('summary-import')).toHaveTextContent('10.500') expect(screen.getByTestId('summary-export')).toHaveTextContent('2.300') expect(screen.getByTestId('summary-total')).toHaveTextContent('12.50') }) it('shows recompute confirmation modal on button click', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/costs') { return Promise.resolve({ data: { items: [], total: 0 } }) } if (path === '/api/energy/costs/summary') { return Promise.resolve({ data: SUMMARY }) } return Promise.resolve({ data: null }) }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('cost-recompute-button')).toBeInTheDocument() }) await user.click(screen.getByTestId('cost-recompute-button')) await waitFor(() => { expect(screen.getByTestId('recompute-confirm-modal')).toBeInTheDocument() }) }) it('calls recompute mutation when confirmed', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/costs') { return Promise.resolve({ data: { items: [], total: 0 } }) } if (path === '/api/energy/costs/summary') { return Promise.resolve({ data: SUMMARY }) } return Promise.resolve({ data: null }) }) mockPost.mockResolvedValue({ data: { recomputed: 0 } }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('cost-recompute-button')).toBeInTheDocument() }) await user.click(screen.getByTestId('cost-recompute-button')) await waitFor(() => { expect(screen.getByTestId('recompute-confirm')).toBeInTheDocument() }) await user.click(screen.getByTestId('recompute-confirm')) await waitFor(() => { expect(mockPost).toHaveBeenCalledWith( '/api/energy/costs/recompute', expect.any(Object), ) }) }) })