213 lines
6.0 KiB
TypeScript
213 lines
6.0 KiB
TypeScript
/**
|
|||
|
|
* 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()
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|