- EnergyPage: Mantine Tabs (Devices kept intact + Contracts/Prices/Costs). - ContractManager/ContractForm: list/activate/add-version + version history; form fields rendered dynamically from /api/energy/profiles structure. - TibberPrices + CostView: Recharts price curve, cost trend/detail/summary, recompute; window-bounded, currency/units from API, empty/error/loading states. - ConfigPage: tri-state Tibber test button (token never shown). - hooks.ts: typed energy hooks; schema.d.ts regenerated via codegen.
206 lines
6.0 KiB
TypeScript
206 lines
6.0 KiB
TypeScript
/**
|
|
* 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(<ContractManager />)
|
|
|
|
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(<ContractManager />)
|
|
|
|
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(<ContractManager />)
|
|
|
|
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(<ContractManager />)
|
|
|
|
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(<ContractManager />)
|
|
|
|
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()
|
|
})
|
|
})
|
|
})
|