/** * Tests for ContractForm component. * * Coverage: * 1. Renders profile fields dynamically from profile structure (not hardcoded) * — verified by passing defaultKind so fields render automatically. * 2. Submit in create mode calls POST /api/energy/contracts. * 3. Submit in add-version mode calls POST /api/energy/contracts/{id}/versions. * 4. Cancel closes modal. */ 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 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' }, }, }, ], } const CREATED_CONTRACT = { id: 1, name: 'Test Contract', kind: 'manual', active: false, currency: 'EUR', created_at: '2026-06-01T00:00:00Z', updated_at: '2026-06-01T00:00:00Z', versions: [], } // --------------------------------------------------------------------------- // Import component // --------------------------------------------------------------------------- import { ContractForm } from './ContractForm' // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('ContractForm', () => { beforeEach(() => vi.clearAllMocks()) it('renders profile fields dynamically from profile structure (not hardcoded)', async () => { mockGet.mockImplementation((path: string) => { if (path === '/api/energy/profiles') { return Promise.resolve({ data: PROFILES_RESPONSE }) } return Promise.resolve({ data: null }) }) const onClose = vi.fn() const onSaved = vi.fn() // Pass defaultKind so the profile fields render automatically in create mode renderWithProviders( , ) // Wait for profiles to load and fields to appear await waitFor(() => { expect(screen.getByTestId('contract-section-energy')).toBeInTheDocument() }, { timeout: 3000 }) // Verify sections are rendered from profile structure (not hardcoded) expect(screen.getByTestId('contract-section-energy')).toBeInTheDocument() expect(screen.getByTestId('contract-section-standing')).toBeInTheDocument() expect(screen.getByTestId('contract-section-credits')).toBeInTheDocument() // Verify specific fields exist from the profile structure // "energy.buy.normal" → label "Buy Normal" expect(screen.getByTestId('contract-field-energy.buy.normal')).toBeInTheDocument() expect(screen.getByTestId('contract-field-energy.buy.dal')).toBeInTheDocument() expect(screen.getByTestId('contract-field-standing.network_fee')).toBeInTheDocument() }) it('calls POST /api/energy/contracts in create mode', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/profiles') { return Promise.resolve({ data: PROFILES_RESPONSE }) } return Promise.resolve({ data: null }) }) mockPost.mockResolvedValue({ data: CREATED_CONTRACT }) const onClose = vi.fn() const onSaved = vi.fn() // Use defaultKind to ensure fields load without needing to interact with Select renderWithProviders( , ) // Wait for form to render await waitFor(() => { expect(screen.getByTestId('contract-form')).toBeInTheDocument() }) // Wait for profile fields to appear await waitFor(() => { expect(screen.getByTestId('contract-name')).toBeInTheDocument() }) // Fill in contract name await user.type(screen.getByTestId('contract-name'), 'Test Contract') // Submit form await user.click(screen.getByTestId('contract-form-submit')) await waitFor(() => { expect(mockPost).toHaveBeenCalledWith( '/api/energy/contracts', expect.objectContaining({ body: expect.objectContaining({ name: 'Test Contract', kind: 'manual', currency: 'EUR', }), }), ) }, { timeout: 3000 }) }) it('calls POST /api/energy/contracts/{id}/versions in add-version mode', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/profiles') { return Promise.resolve({ data: PROFILES_RESPONSE }) } return Promise.resolve({ data: null }) }) mockPost.mockResolvedValue({ data: CREATED_CONTRACT }) const onClose = vi.fn() const onSaved = vi.fn() renderWithProviders( , ) // Wait for form to be ready (add-version mode) await waitFor(() => { expect(screen.getByTestId('contract-form')).toBeInTheDocument() }) // In add-version mode, no name or kind fields shown expect(screen.queryByTestId('contract-name')).not.toBeInTheDocument() expect(screen.queryByTestId('contract-kind')).not.toBeInTheDocument() // Fill in effective_from (required in add-version mode) const effectiveDateInput = screen.getByTestId('contract-effective-from') await user.type(effectiveDateInput, '2026-07-01') // Submit form await user.click(screen.getByTestId('contract-form-submit')) await waitFor(() => { expect(mockPost).toHaveBeenCalledWith( '/api/energy/contracts/{contract_id}/versions', expect.objectContaining({ params: { path: { contract_id: 1 } }, body: expect.objectContaining({ values: expect.any(Object), }), }), ) }, { timeout: 3000 }) }) it('calls onClose when Cancel button is clicked', async () => { const user = userEvent.setup() mockGet.mockImplementation((path: string) => { if (path === '/api/energy/profiles') { return Promise.resolve({ data: PROFILES_RESPONSE }) } return Promise.resolve({ data: null }) }) const onClose = vi.fn() const onSaved = vi.fn() renderWithProviders( , ) await waitFor(() => { expect(screen.getByTestId('contract-form-cancel')).toBeInTheDocument() }) await user.click(screen.getByTestId('contract-form-cancel')) expect(onClose).toHaveBeenCalled() }) it('prefills fields from the open version when in add-version mode', async () => { // FU11: add-version mode should auto-prefill rate fields from the contract's // current open version so the user only has to change what's different. const CONTRACT_DETAIL = { id: 42, name: 'My Contract', kind: 'manual', active: true, currency: 'EUR', created_at: '2026-01-01T00:00:00Z', updated_at: '2026-01-01T00:00:00Z', versions: [ { id: 1, contract_id: 42, effective_from: '2026-01-01T00:00:00Z', effective_to: null, // open version values: { energy: { buy: { normal: 0.133, dal: 0.127 }, sell: { normal: 0.05, dal: 0.05 }, energy_tax: 0.11, ode: 0.0, }, standing: { network_fee: 30.0, management_fee: 60.0, }, credits: { heffingskorting: 365.0, }, }, created_at: '2026-01-01T00:00:00Z', }, ], } mockGet.mockImplementation((path: string) => { if (path === '/api/energy/profiles') { return Promise.resolve({ data: PROFILES_RESPONSE }) } if (path === '/api/energy/contracts/{contract_id}') { return Promise.resolve({ data: CONTRACT_DETAIL }) } return Promise.resolve({ data: null }) }) const onClose = vi.fn() const onSaved = vi.fn() renderWithProviders( , ) const user = userEvent.setup() // Wait for the form to render and profile fields to appear await waitFor(() => { expect(screen.getByTestId('contract-section-energy')).toBeInTheDocument() }, { timeout: 3000 }) // Wait for prefill to apply: both profile and contract detail queries must resolve. // Fill in the required effective_from date and submit; the submitted values // body.values should contain the prefilled rates from the open version. mockPost.mockResolvedValue({ data: {} }) const effectiveInput = screen.getByTestId('contract-effective-from') await user.type(effectiveInput, '2026-07-01') await user.click(screen.getByTestId('contract-form-submit')) // The submitted body should contain the prefilled values from CONTRACT_DETAIL await waitFor(() => { expect(mockPost).toHaveBeenCalledWith( '/api/energy/contracts/{contract_id}/versions', expect.objectContaining({ body: expect.objectContaining({ values: expect.objectContaining({ standing: expect.objectContaining({ network_fee: 30, management_fee: 60, }), }), }), }), ) }, { timeout: 3000 }) }) })