M5-T06: add Energy device management UI and sidebar entry
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
/**
|
||||
* 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' }),
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user