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

242 lines
7.3 KiB
TypeScript

/**
* Tests for energy/DeviceForm.tsx
*
* Coverage:
* 1. Renders empty form in create mode (title "New Device").
* 2. Renders pre-filled form in edit mode (title "Edit Device"), shows UUID.
* 3. Validation: missing friendly_name shows error.
* 4. Profile dropdown loads options from GET /api/modbus/profiles.
* 5. Submit in create mode calls POST /api/modbus/devices.
* 6. Submit in edit mode calls PATCH /api/modbus/devices/{uuid}.
* 7. Cancel button invokes onClose without submitting.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { screen, waitFor, fireEvent } from '@testing-library/react'
import { renderWithProviders } from '../test-utils'
import { DeviceForm } from './DeviceForm'
// ---------------------------------------------------------------------------
// Fixtures
// ---------------------------------------------------------------------------
const DEVICE = {
uuid: 'abc-123',
friendly_name: 'SDM120 Main',
transport: 'tcp',
host: '192.168.1.100',
port: 502,
unit_id: 1,
profile: 'sdm120',
poll_interval_s: 5,
enabled: true,
last_poll_at: null,
last_poll_ok: null,
created_at: '2026-06-01T00:00:00Z',
updated_at: '2026-06-01T00:00:00Z',
}
const PROFILES_RESP = {
profiles: [{ name: 'sdm120', description: 'Eastron SDM120 single-phase energy meter' }],
}
// ---------------------------------------------------------------------------
// 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(),
}))
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function setupProfilesMock() {
mockGet.mockImplementation((path: string) => {
if (path === '/api/modbus/profiles') {
return Promise.resolve({ data: PROFILES_RESP })
}
// modbus-devices for invalidation
return Promise.resolve({ data: { items: [], total: 0 } })
})
}
function renderCreateForm(onClose = vi.fn(), onSaved = vi.fn()) {
return renderWithProviders(<DeviceForm onClose={onClose} onSaved={onSaved} />, {
initialPath: '/',
})
}
function renderEditForm(onClose = vi.fn(), onSaved = vi.fn()) {
return renderWithProviders(<DeviceForm device={DEVICE} onClose={onClose} onSaved={onSaved} />, {
initialPath: '/',
})
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('DeviceForm — create mode', () => {
beforeEach(() => {
vi.clearAllMocks()
setupProfilesMock()
})
it('renders with title "New Device" and empty friendly-name input', async () => {
renderCreateForm()
await waitFor(() => {
expect(screen.getByTestId('device-form-modal')).toBeInTheDocument()
})
// Modal title
expect(screen.getByText('New Device')).toBeInTheDocument()
// UUID display should NOT be present in create mode
expect(screen.queryByTestId('device-uuid-display')).not.toBeInTheDocument()
})
it('loads profile options from GET /api/modbus/profiles', async () => {
renderCreateForm()
await waitFor(() => {
expect(screen.getByTestId('device-form')).toBeInTheDocument()
})
// Profile select should be rendered
expect(screen.getByTestId('device-profile')).toBeInTheDocument()
expect(mockGet).toHaveBeenCalledWith('/api/modbus/profiles')
})
it('shows validation error when friendly name is empty', async () => {
renderCreateForm()
await waitFor(() => expect(screen.getByTestId('device-form')).toBeInTheDocument())
fireEvent.submit(screen.getByTestId('device-form'))
await waitFor(() => {
expect(screen.getByTestId('device-form-error')).toBeInTheDocument()
})
expect(screen.getByTestId('device-form-error').textContent).toContain('Friendly name')
})
it('submit calls POST /api/modbus/devices with correct body (profile auto-selected)', async () => {
mockPost.mockResolvedValue({ data: DEVICE })
const onSaved = vi.fn()
renderCreateForm(vi.fn(), onSaved)
// Wait for profiles to load; sdm120 is auto-selected as first profile.
await waitFor(() => expect(screen.getByTestId('device-form')).toBeInTheDocument())
// Fill form (profile auto-selected to 'sdm120')
fireEvent.change(screen.getByTestId('device-friendly-name'), {
target: { value: 'My Meter' },
})
fireEvent.change(screen.getByTestId('device-host'), {
target: { value: '10.0.0.1' },
})
fireEvent.submit(screen.getByTestId('device-form'))
await waitFor(() => {
expect(mockPost).toHaveBeenCalledWith(
'/api/modbus/devices',
expect.objectContaining({
body: expect.objectContaining({
friendly_name: 'My Meter',
host: '10.0.0.1',
transport: 'tcp',
profile: 'sdm120',
}),
}),
)
})
})
it('cancel calls onClose without POST', async () => {
const onClose = vi.fn()
renderCreateForm(onClose)
await waitFor(() => expect(screen.getByTestId('device-form-cancel')).toBeInTheDocument())
fireEvent.click(screen.getByTestId('device-form-cancel'))
expect(onClose).toHaveBeenCalledOnce()
expect(mockPost).not.toHaveBeenCalled()
})
})
describe('DeviceForm — edit mode', () => {
beforeEach(() => {
vi.clearAllMocks()
setupProfilesMock()
})
it('renders with title "Edit Device" and pre-filled friendly-name', async () => {
renderEditForm()
// Wait for modal to appear
await waitFor(() => {
expect(screen.getByTestId('device-form-modal')).toBeInTheDocument()
})
expect(screen.getByText('Edit Device')).toBeInTheDocument()
// Wait for form to appear (profiles must load first)
await waitFor(() => {
expect(screen.getByTestId('device-form')).toBeInTheDocument()
})
expect(screen.getByTestId('device-uuid-display').textContent).toContain('abc-123')
// Friendly name input should be pre-filled
const input = screen.getByTestId('device-friendly-name') as HTMLInputElement
expect(input.value).toBe('SDM120 Main')
})
it('submit calls PATCH /api/modbus/devices/{uuid}', async () => {
mockPatch.mockResolvedValue({ data: DEVICE })
renderEditForm()
// Wait for profiles to load so the form is visible
await waitFor(() => expect(screen.getByTestId('device-form')).toBeInTheDocument())
fireEvent.change(screen.getByTestId('device-friendly-name'), {
target: { value: 'SDM120 Updated' },
})
fireEvent.submit(screen.getByTestId('device-form'))
await waitFor(() => {
expect(mockPatch).toHaveBeenCalledWith('/api/modbus/devices/{uuid}', {
params: { path: { uuid: 'abc-123' } },
body: expect.objectContaining({ friendly_name: 'SDM120 Updated' }),
})
})
})
})