M8-T18: add source and multi-commodity meter UI

This commit is contained in:
2026-08-23 21:22:06 +02:00
parent 39c11ae606
commit 963e43e3e4
15 changed files with 319 additions and 18 deletions
+37
View File
@@ -84,6 +84,7 @@ const METERS_RESPONSE = {
// ---------------------------------------------------------------------------
describe('MeterManager — loading / error / empty states', () => {
// M8 keeps the existing Modbus-facing meter regressions alongside commodity additions.
beforeEach(() => vi.clearAllMocks())
it('renders loading state initially', () => {
@@ -115,6 +116,24 @@ describe('MeterManager — loading / error / empty states', () => {
})
})
describe('MeterManager — binding switch safety', () => {
beforeEach(() => vi.clearAllMocks())
it('does not offer switching for a closed meter epoch', async () => {
mockGet.mockResolvedValue({ data: { items: [CLOSED_METER], total: 1 } })
renderWithProviders(<MeterManager />)
await waitFor(() => expect(screen.getByTestId('meters-table')).toBeInTheDocument())
expect(screen.queryByRole('button', { name: 'Switch source' })).not.toBeInTheDocument()
})
it('disables switch submit until the binding timeline has loaded', async () => {
const user = userEvent.setup()
mockGet.mockImplementation((path: string) => path === '/api/energy/meters' ? Promise.resolve({ data: { items: [ACTIVE_METER], total: 1 } }) : new Promise(() => {}))
renderWithProviders(<MeterManager />)
await user.click(await screen.findByRole('button', { name: 'Switch source' }))
expect(await screen.findByText('Loading binding timeline…')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Switch binding' })).toBeDisabled()
})
})
describe('MeterManager — meter list', () => {
beforeEach(() => vi.clearAllMocks())
@@ -140,6 +159,24 @@ describe('MeterManager — meter list', () => {
expect(screen.getByText('meter_swap')).toBeInTheDocument()
})
it('renders every binding timeline segment with source, channel, and half-open boundaries', async () => {
const meterWithBindings = {
...ACTIVE_METER,
bindings: [
{ uuid: 'binding-closed', source_uuid: 'source-old', source_channel_uuid: 'channel-old', started_at: '2025-01-01T00:00:00Z', ended_at: '2025-02-01T00:00:00Z' },
{ uuid: 'binding-active', source_uuid: 'source-new', source_channel_uuid: 'channel-new', started_at: '2025-02-01T00:00:00Z', ended_at: null },
],
}
mockGet.mockResolvedValue({ data: { items: [meterWithBindings], total: 1 } })
renderWithProviders(<MeterManager />)
const closed = await screen.findByTestId('binding-timeline-binding-closed')
const active = screen.getByTestId('binding-timeline-binding-active')
expect(closed).toHaveTextContent('source-old → channel-old')
expect(closed).toHaveTextContent('[1/1/2025, 00:00:00, 2/1/2025, 00:00:00) (closed)')
expect(active).toHaveTextContent('source-new → channel-new')
expect(active).toHaveTextContent('[2/1/2025, 00:00:00, open-ended) (active)')
})
it('renders "Declare New Meter" button', async () => {
mockGet.mockResolvedValue({ data: METERS_RESPONSE })