/** * 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(, { initialPath: '/', }) } function renderEditForm(onClose = vi.fn(), onSaved = vi.fn()) { return renderWithProviders(, { 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' }), }) }) }) })