M6-T10: frontend contract management + price/cost views + Tibber test
- 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.
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Tests for Tibber test button in ConfigPage.
|
||||
*
|
||||
* Coverage:
|
||||
* 1. Tibber test button appears when Tibber section is in config.
|
||||
* 2. Success tri-state shows green alert.
|
||||
* 3. Config-error tri-state shows orange alert.
|
||||
* 4. Failed tri-state shows red alert.
|
||||
*/
|
||||
|
||||
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 mockPut = vi.fn()
|
||||
|
||||
vi.mock('../api/client', () => ({
|
||||
default: {
|
||||
GET: (...args: unknown[]) => mockGet(...args),
|
||||
POST: (...args: unknown[]) => mockPost(...args),
|
||||
PUT: (...args: unknown[]) => mockPut(...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 CONFIG_WITH_TIBBER = {
|
||||
sections: [
|
||||
{
|
||||
name: 'Tibber',
|
||||
fields: [
|
||||
{
|
||||
env_name: 'TIBBER_TOKEN',
|
||||
label: 'Tibber API Token',
|
||||
secret: true,
|
||||
input_type: 'text',
|
||||
configured: true,
|
||||
value: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { ConfigPage } from './ConfigPage'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper to suppress TOTP and Expose Settings sub-queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function mockConfigDependencies() {
|
||||
mockGet.mockImplementation((path: string) => {
|
||||
if (path === '/api/config') {
|
||||
return Promise.resolve({ data: CONFIG_WITH_TIBBER })
|
||||
}
|
||||
if (path === '/api/expose') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
catalog: [],
|
||||
mqtt_status: { connected: false, broker: null },
|
||||
},
|
||||
})
|
||||
}
|
||||
if (path === '/api/auth/totp/status') {
|
||||
return Promise.resolve({ data: { enabled: false } })
|
||||
}
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ConfigPage — Tibber test button', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('renders Tibber test button when Tibber section is present', async () => {
|
||||
mockConfigDependencies()
|
||||
|
||||
renderWithProviders(<ConfigPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-test-button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows success alert on successful Tibber test', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockConfigDependencies()
|
||||
|
||||
mockPost.mockImplementation((path: string) => {
|
||||
if (path === '/api/energy/tibber/test') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
result: 'success',
|
||||
message: 'Connected to Tibber API',
|
||||
price: {
|
||||
starts_at: '2026-06-22T10:00:00Z',
|
||||
total: 0.1337,
|
||||
energy: 0.08,
|
||||
tax: 0.0537,
|
||||
currency: 'EUR',
|
||||
level: 'NORMAL',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
renderWithProviders(<ConfigPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-test-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('tibber-test-button'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-result-success')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('tibber-result-success')).toHaveTextContent(
|
||||
'Tibber connection successful',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows config-error alert when Tibber is misconfigured', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockConfigDependencies()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ApiErrorClass = (await import('../api/client')).ApiError as any
|
||||
mockPost.mockImplementation((path: string) => {
|
||||
if (path === '/api/energy/tibber/test') {
|
||||
throw new ApiErrorClass(400, {
|
||||
result: 'config-error',
|
||||
message: 'Tibber token not configured',
|
||||
})
|
||||
}
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
renderWithProviders(<ConfigPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-test-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('tibber-test-button'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-result-config-error')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows failed alert when Tibber test fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockConfigDependencies()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ApiErrorClass = (await import('../api/client')).ApiError as any
|
||||
mockPost.mockImplementation((path: string) => {
|
||||
if (path === '/api/energy/tibber/test') {
|
||||
throw new ApiErrorClass(500, {
|
||||
result: 'failed',
|
||||
message: 'Connection refused',
|
||||
})
|
||||
}
|
||||
return Promise.resolve({ data: null })
|
||||
})
|
||||
|
||||
renderWithProviders(<ConfigPage />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-test-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
await user.click(screen.getByTestId('tibber-test-button'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('tibber-result-failed')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user