/**
* 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()
}
})
})