/** * Tests for ContractManager component. * * Coverage: * 1. Loading state rendering. * 2. Contract list rendering. * 3. Empty state rendering. * 4. Activate button calls PATCH with {active: true}. * 5. "New Contract" button opens ContractForm. */ 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() const mockPatch = vi.fn() vi.mock('../api/client', () => ({ default: { GET: (...args: unknown[]) => mockGet(...args), POST: (...args: unknown[]) => mockPost(...args), PATCH: (...args: unknown[]) => mockPatch(...args), 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 ACTIVE_CONTRACT = { id: 1, name: 'My Active Contract', kind: 'manual', active: true, currency: 'EUR', created_at: '2026-06-01T00:00:00Z', updated_at: '2026-06-01T00:00:00Z', } const INACTIVE_CONTRACT = { id: 2, name: 'Inactive Contract', kind: 'tibber', active: false, currency: 'EUR', created_at: '2026-06-02T00:00:00Z', updated_at: '2026-06-02T00:00:00Z', } const PROFILES_RESPONSE = { profiles: [ { kind: 'manual', label: 'Fixed / Variable Rate (NL, dual-tariff)', energy: { dual_tariff: true, buy: { normal: { unit: 'EUR/kWh' }, dal: { unit: 'EUR/kWh' } }, sell: { normal: { unit: 'EUR/kWh' }, dal: { unit: 'EUR/kWh' } }, energy_tax: { unit: 'EUR/kWh' }, ode: { unit: 'EUR/kWh', default: 0 }, }, standing: { network_fee: { unit: 'EUR/month' }, management_fee: { unit: 'EUR/month' }, }, credits: { heffingskorting: { unit: 'EUR/year' }, }, }, ], } // --------------------------------------------------------------------------- // Import component // --------------------------------------------------------------------------- import { ContractManager } from './ContractManager' // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('ContractManager', () => { beforeEach(() => vi.clearAllMocks()) it('renders loading state initially', () => { // Never resolve so we stay in loading state mockGet.mockImplementation(() => new Promise(() => {})) renderWithProviders() expect(screen.getByTestId('contracts-loading')).toBeInTheDocument() }) it('renders contract list when data loads', async () => { mockGet.mockImplementation((path: string) => { if (path === '/api/energy/contracts') { return Promise.resolve({ data: { items: [ACTIVE_CONTRACT, INACTIVE_CONTRACT], total: 2 }, }) } return Promise.resolve({ data: null }) }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('contracts-table')).toBeInTheDocument() }) expect(screen.getByText('My Active Contract')).toBeInTheDocument() expect(screen.getByText('Inactive Contract')).toBeInTheDocument() expect(screen.getByTestId('contract-active-badge-1')).toHaveTextContent('active') expect(screen.getByTestId('contract-active-badge-2')).toHaveTextContent('inactive') }) it('renders empty state when no contracts exist', async () => { mockGet.mockResolvedValue({ data: { items: [], total: 0 } }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('contracts-empty')).toBeInTheDocument() }) }) it('calls PATCH with active=true when Activate button clicked', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/contracts') { return Promise.resolve({ data: { items: [INACTIVE_CONTRACT], total: 1 }, }) } return Promise.resolve({ data: null }) }) mockPatch.mockResolvedValue({ data: { ...INACTIVE_CONTRACT, active: true, versions: [] }, }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId(`contract-activate-${INACTIVE_CONTRACT.id}`)).toBeInTheDocument() }) await user.click(screen.getByTestId(`contract-activate-${INACTIVE_CONTRACT.id}`)) await waitFor(() => { expect(mockPatch).toHaveBeenCalledWith( '/api/energy/contracts/{contract_id}', expect.objectContaining({ params: { path: { contract_id: INACTIVE_CONTRACT.id } }, body: { active: true }, }), ) }) }) it('opens ContractForm when "New Contract" button is clicked', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/contracts') { return Promise.resolve({ data: { items: [], total: 0 } }) } if (path === '/api/energy/profiles') { return Promise.resolve({ data: PROFILES_RESPONSE }) } return Promise.resolve({ data: null }) }) renderWithProviders() await waitFor(() => { expect(screen.getByTestId('contract-new-button')).toBeInTheDocument() }) await user.click(screen.getByTestId('contract-new-button')) await waitFor(() => { expect(screen.getByTestId('contract-form-modal')).toBeInTheDocument() }) }) })