M5: roll Energy chart window for live auto-refresh (English label); render boolean config fields as switches
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
* 12. (M5-T01B) Accordion: second section can be expanded by clicking its control.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { screen, waitFor, fireEvent } from '@testing-library/react'
|
||||
import { renderWithProviders } from '../test-utils'
|
||||
import { ConfigPage } from './ConfigPage'
|
||||
@@ -47,6 +47,26 @@ const MOCK_CONFIG = {
|
||||
],
|
||||
}
|
||||
|
||||
/** Extended fixture that includes a checkbox (bool) field. */
|
||||
const MOCK_CONFIG_WITH_CHECKBOX = {
|
||||
sections: [
|
||||
{
|
||||
name: 'MQTT',
|
||||
fields: [
|
||||
{ env_name: 'MQTT_ENABLED', label: 'MQTT Enabled', value: 'false', secret: false, input_type: 'checkbox', configured: true },
|
||||
{ env_name: 'MQTT_BROKER_HOST', label: 'MQTT Broker Host', value: 'localhost', secret: false, input_type: 'text', configured: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'SMTP',
|
||||
fields: [
|
||||
{ env_name: 'SMTP_HOST', label: 'SMTP Host', value: 'smtp.example.com', secret: false, input_type: 'text', configured: true },
|
||||
{ env_name: 'SMTP_PASSWORD', label: 'SMTP Password', value: '', secret: true, input_type: 'password', configured: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock apiClient
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -378,3 +398,97 @@ describe('ConfigPage', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Area B: checkbox (Switch) rendering and value round-trip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('ConfigPage — checkbox (bool) fields', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockGet.mockResolvedValue({ data: MOCK_CONFIG_WITH_CHECKBOX, response: { status: 200, ok: true } })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 13. Checkbox field renders as a Switch (checked=false when value='false')
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('renders a checkbox field as a Switch with checked=false when value is "false"', async () => {
|
||||
renderConfig()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('field-MQTT_ENABLED')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const switchEl = screen.getByTestId('field-MQTT_ENABLED') as HTMLInputElement
|
||||
// Mantine Switch renders as <input type="checkbox">
|
||||
expect(switchEl.type).toBe('checkbox')
|
||||
expect(switchEl.checked).toBe(false)
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 14. Toggling Switch to ON produces "true" in the save payload
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('toggling Switch to checked sends "true" for that field in buildUpdates', async () => {
|
||||
mockPut.mockResolvedValueOnce({ data: {}, response: { status: 200, ok: true } })
|
||||
mockGet.mockResolvedValue({ data: MOCK_CONFIG_WITH_CHECKBOX, response: { status: 200, ok: true } })
|
||||
|
||||
renderConfig()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('field-MQTT_ENABLED')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Toggle the switch ON
|
||||
const switchEl = screen.getByTestId('field-MQTT_ENABLED') as HTMLInputElement
|
||||
fireEvent.click(switchEl)
|
||||
|
||||
// Submit the form
|
||||
fireEvent.submit(screen.getByTestId('config-form'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPut).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
const putCall = mockPut.mock.calls[0]
|
||||
const body = putCall[1].body as { updates: Record<string, string> }
|
||||
const updates = body.updates
|
||||
|
||||
// The bool field must be sent as the string "true"
|
||||
expect(updates).toHaveProperty('MQTT_ENABLED', 'true')
|
||||
// Other non-secret fields still included
|
||||
expect(updates).toHaveProperty('MQTT_BROKER_HOST', 'localhost')
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 15. Switch starts checked=true when initial value is "true"
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
it('renders Switch as checked when initial value is "true"', async () => {
|
||||
const configWithEnabled = {
|
||||
sections: [
|
||||
{
|
||||
name: 'MQTT',
|
||||
fields: [
|
||||
{ env_name: 'MQTT_ENABLED', label: 'MQTT Enabled', value: 'true', secret: false, input_type: 'checkbox', configured: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
mockGet.mockResolvedValue({ data: configWithEnabled, response: { status: 200, ok: true } })
|
||||
|
||||
renderConfig()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('field-MQTT_ENABLED')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const switchEl = screen.getByTestId('field-MQTT_ENABLED') as HTMLInputElement
|
||||
expect(switchEl.checked).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
Loader,
|
||||
Center,
|
||||
Badge,
|
||||
Switch,
|
||||
} from '@mantine/core'
|
||||
import apiClient, { ApiError } from '../api/client'
|
||||
import type { components } from '../api/schema.d.ts'
|
||||
@@ -134,6 +135,17 @@ function ConfigFieldInput({ field, value, onChange }: ConfigFieldInputProps) {
|
||||
)
|
||||
}
|
||||
|
||||
if (field.input_type === 'checkbox') {
|
||||
return (
|
||||
<Switch
|
||||
label={field.label}
|
||||
checked={(value ?? '').toLowerCase() === 'true'}
|
||||
onChange={(e) => onChange(field.env_name, e.currentTarget.checked ? 'true' : 'false')}
|
||||
data-testid={`field-${field.env_name}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (field.input_type === 'number') {
|
||||
return (
|
||||
<TextInput
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('EnergyPage — auto-refresh switch', () => {
|
||||
|
||||
// Mantine Switch uses role="switch" and places data-testid directly on the
|
||||
// <input> element; query it by the testid or by role.
|
||||
const switchInput = screen.getByRole('switch', { name: /自动刷新/i })
|
||||
const switchInput = screen.getByRole('switch', { name: /auto-refresh/i })
|
||||
expect(switchInput).toBeChecked()
|
||||
})
|
||||
|
||||
@@ -382,7 +382,7 @@ describe('EnergyPage — auto-refresh switch', () => {
|
||||
expect(screen.getByTestId('auto-refresh-switch')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const switchInput = screen.getByRole('switch', { name: /自动刷新/i })
|
||||
const switchInput = screen.getByRole('switch', { name: /auto-refresh/i })
|
||||
expect(switchInput).toBeChecked()
|
||||
|
||||
fireEvent.click(switchInput)
|
||||
|
||||
@@ -463,7 +463,7 @@ function DeviceReadingsSection({ devices }: DeviceReadingsSectionProps) {
|
||||
<Group justify="space-between" align="center">
|
||||
<Divider label="Readings & Trends" labelPosition="left" style={{ flex: 1 }} />
|
||||
<Switch
|
||||
label="自动刷新"
|
||||
label="Auto-refresh"
|
||||
checked={autoRefreshEnabled}
|
||||
onChange={(e) => setAutoRefreshEnabled(e.currentTarget.checked)}
|
||||
size="sm"
|
||||
|
||||
Reference in New Issue
Block a user