/** * 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 ACTIVE_THERMAL_CONTRACT = { id: 3, name: 'Active Heat Contract', kind: 'district_heating', active: true, currency: 'EUR', created_at: '2026-06-03T00:00:00Z', updated_at: '2026-06-03T00: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() }) }) it('keeps the selector available while thermal data loads and requests each scope separately', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string, options?: { params?: { query?: { scope?: string } } }) => { if (path === '/api/energy/contracts' && options?.params?.query?.scope === 'thermal') return new Promise(() => {}) if (path === '/api/energy/contracts') return Promise.resolve({ data: { items: [ACTIVE_CONTRACT], total: 1 } }) return Promise.resolve({ data: PROFILES_RESPONSE }) }) renderWithProviders() await waitFor(() => expect(screen.getByTestId('contracts-scope-selector')).toBeInTheDocument()) await user.click(screen.getByText('Thermal')) expect(screen.getByTestId('contracts-loading')).toBeInTheDocument() expect(screen.getByTestId('contracts-scope-selector')).toBeInTheDocument() await user.click(screen.getByText('Electricity')) await waitFor(() => expect(screen.getByTestId('contracts-table')).toBeInTheDocument()) expect(mockGet).toHaveBeenCalledWith('/api/energy/contracts', { params: { query: { scope: 'thermal' } } }) }) it('keeps simultaneous active electricity and thermal contracts isolated across repeated switches', async () => { const user = userEvent.setup() mockGet.mockImplementation((_path: string, options?: { params?: { query?: { scope?: string } } }) => Promise.resolve({ data: { items: options?.params?.query?.scope === 'thermal' ? [ACTIVE_THERMAL_CONTRACT] : [ACTIVE_CONTRACT], total: 1 } }), ) renderWithProviders() await waitFor(() => expect(screen.getByText('My Active Contract')).toBeInTheDocument()) for (let i = 0; i < 2; i += 1) { await user.click(screen.getByText('Thermal')) await waitFor(() => expect(screen.getByText('Active Heat Contract')).toBeInTheDocument()) expect(screen.queryByText('My Active Contract')).not.toBeInTheDocument() await user.click(screen.getByText('Electricity')) await waitFor(() => expect(screen.getByText('My Active Contract')).toBeInTheDocument()) expect(screen.queryByText('Active Heat Contract')).not.toBeInTheDocument() } }) })