Files
home-automation/frontend/src/energy/ContractForm.test.tsx
T

250 lines
7.8 KiB
TypeScript
Raw Normal View History

/**
* 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(
<ContractForm defaultKind="manual" onClose={onClose} onSaved={onSaved} />,
)
// 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(
<ContractForm defaultKind="manual" onClose={onClose} onSaved={onSaved} />,
)
// 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(
<ContractForm contractId={1} defaultKind="manual" onClose={onClose} onSaved={onSaved} />,
)
// 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(
<ContractForm onClose={onClose} onSaved={onSaved} />,
)
await waitFor(() => {
expect(screen.getByTestId('contract-form-cancel')).toBeInTheDocument()
})
await user.click(screen.getByTestId('contract-form-cancel'))
expect(onClose).toHaveBeenCalled()
})
})