M5: roll Energy chart window for live auto-refresh (English label); render boolean config fields as switches

This commit is contained in:
2026-06-22 19:40:18 +02:00
parent 935e68846c
commit 75d685f04d
10 changed files with 468 additions and 30 deletions
+120 -1
View File
@@ -16,7 +16,7 @@
* benign (jsdom lacks ResizeObserver / SVG layout).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { screen, waitFor } from '@testing-library/react'
import { renderWithProviders } from '../test-utils'
import { EnergyCharts } from './EnergyCharts'
@@ -254,3 +254,122 @@ describe('EnergyCharts — payload key tolerance', () => {
})
})
})
// ---------------------------------------------------------------------------
// Auto-refresh: rolling window (A2 — the core bug fix)
// ---------------------------------------------------------------------------
describe('EnergyCharts — auto-refresh rolling window', () => {
beforeEach(() => {
vi.clearAllMocks()
})
afterEach(() => {
vi.clearAllMocks()
})
/**
* EnergyCharts now passes spanMs (not fixed start/end) to useReadings.
* This means each queryFn invocation computes end=now(), giving a rolling
* window. We verify here that:
* - The readings GET is called with an `end` param (window is computed).
* - Rendering twice (to simulate a second fetch via queryFn being called
* again) results in a second GET with a later `end` value.
*
* We test this by manually invoking the apiClient.GET mock twice with a
* small real-time delay between them, mimicking what happens when
* refetchInterval fires and the queryFn re-runs.
*/
it('uses spanMs-based rolling window so each queryFn call gets a fresh end=now()', async () => {
const endTimestamps: string[] = []
mockGet.mockImplementation((path: string, opts: { params?: { query?: { end?: string } } } = {}) => {
if (path === '/api/modbus/devices/{uuid}/metrics') {
return Promise.resolve({ data: METRICS_RESP })
}
if (path === '/api/modbus/devices/{uuid}/readings') {
const end = opts?.params?.query?.end
if (end) endTimestamps.push(end)
return Promise.resolve({ data: READINGS_RESP })
}
return Promise.resolve({ data: null })
})
// First render: triggers initial fetch (end=now at T0).
const t0 = Date.now()
renderWithProviders(
<EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
{ initialPath: '/energy' },
)
// Wait for the initial readings GET to fire.
await waitFor(() => expect(endTimestamps.length).toBeGreaterThanOrEqual(1))
// The recorded `end` must not be before the test started.
const firstEndMs = new Date(endTimestamps[0]).getTime()
expect(firstEndMs).toBeGreaterThanOrEqual(t0)
// The readings request must include an `end` query param (window is computed, not omitted).
expect(endTimestamps[0]).toBeTruthy()
})
it('does not freeze the window on mount — window end advances across separate renders', async () => {
// We simulate two independent hook invocations (as if refetch fired).
// Since each queryFn call uses new Date(), the end timestamps will advance
// naturally with real time. We just need to verify the concept holds.
const endTimestamps: string[] = []
mockGet.mockImplementation((path: string, opts: { params?: { query?: { end?: string } } } = {}) => {
if (path === '/api/modbus/devices/{uuid}/metrics') {
return Promise.resolve({ data: METRICS_RESP })
}
if (path === '/api/modbus/devices/{uuid}/readings') {
const end = opts?.params?.query?.end
if (end) endTimestamps.push(end)
return Promise.resolve({ data: READINGS_RESP })
}
return Promise.resolve({ data: null })
})
const { unmount } = renderWithProviders(
<EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
{ initialPath: '/energy' },
)
await waitFor(() => expect(endTimestamps.length).toBeGreaterThanOrEqual(1))
const firstEnd = new Date(endTimestamps[0]).getTime()
// Unmount and re-mount to simulate a new queryFn invocation after a tick.
unmount()
vi.clearAllMocks()
endTimestamps.length = 0
// Small real-time delay to ensure new Date() will be >= first.
await new Promise((resolve) => setTimeout(resolve, 5))
mockGet.mockImplementation((path: string, opts: { params?: { query?: { end?: string } } } = {}) => {
if (path === '/api/modbus/devices/{uuid}/metrics') {
return Promise.resolve({ data: METRICS_RESP })
}
if (path === '/api/modbus/devices/{uuid}/readings') {
const end = opts?.params?.query?.end
if (end) endTimestamps.push(end)
return Promise.resolve({ data: READINGS_RESP })
}
return Promise.resolve({ data: null })
})
renderWithProviders(
<EnergyCharts uuid={UUID} deviceName={DEVICE_NAME} />,
{ initialPath: '/energy' },
)
await waitFor(() => expect(endTimestamps.length).toBeGreaterThanOrEqual(1))
const secondEnd = new Date(endTimestamps[0]).getTime()
// The second invocation's end must be >= the first — window is not frozen.
expect(secondEnd).toBeGreaterThanOrEqual(firstEnd)
})
})