mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 12:46:49 +02:00
Auto-off now has a "Keep Enabled" toggle that keeps it active between prints instead of disabling after each use (one-shot). Useful for HA accessories like BentoBox filters that should always power off after prints. Default behavior (one-shot) is unchanged.
409 lines
13 KiB
TypeScript
409 lines
13 KiB
TypeScript
/**
|
|
* Tests for the SmartPlugCard component.
|
|
*
|
|
* These tests focus on critical regression scenarios:
|
|
* - Toggle persistence for auto_on/auto_off settings
|
|
* - Power control functionality
|
|
* - Status display
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { screen, waitFor } from '@testing-library/react';
|
|
import userEvent from '@testing-library/user-event';
|
|
import { render } from '../utils';
|
|
import { SmartPlugCard } from '../../components/SmartPlugCard';
|
|
import type { SmartPlug } from '../../api/client';
|
|
|
|
// Mock data
|
|
const createMockPlug = (overrides: Partial<SmartPlug> = {}): SmartPlug => ({
|
|
id: 1,
|
|
name: 'Test Plug',
|
|
plug_type: 'tasmota',
|
|
ip_address: '192.168.1.100',
|
|
ha_entity_id: null,
|
|
ha_power_entity: null,
|
|
ha_energy_today_entity: null,
|
|
ha_energy_total_entity: null,
|
|
// MQTT fields (legacy)
|
|
mqtt_topic: null,
|
|
mqtt_multiplier: 1.0,
|
|
// MQTT power fields
|
|
mqtt_power_topic: null,
|
|
mqtt_power_path: null,
|
|
mqtt_power_multiplier: 1.0,
|
|
// MQTT energy fields
|
|
mqtt_energy_topic: null,
|
|
mqtt_energy_path: null,
|
|
mqtt_energy_multiplier: 1.0,
|
|
// MQTT state fields
|
|
mqtt_state_topic: null,
|
|
mqtt_state_path: null,
|
|
mqtt_state_on_value: null,
|
|
printer_id: 1,
|
|
enabled: true,
|
|
auto_on: true,
|
|
auto_off: true,
|
|
auto_off_persistent: false,
|
|
off_delay_mode: 'time',
|
|
off_delay_minutes: 5,
|
|
off_temp_threshold: 70,
|
|
username: null,
|
|
password: null,
|
|
power_alert_enabled: false,
|
|
power_alert_high: null,
|
|
power_alert_low: null,
|
|
power_alert_last_triggered: null,
|
|
schedule_enabled: false,
|
|
schedule_on_time: null,
|
|
schedule_off_time: null,
|
|
last_state: 'ON',
|
|
last_checked: null,
|
|
auto_off_executed: false,
|
|
show_in_switchbar: false,
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
...overrides,
|
|
});
|
|
|
|
describe('SmartPlugCard', () => {
|
|
const mockOnEdit = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('rendering', () => {
|
|
it('renders plug name', () => {
|
|
const plug = createMockPlug({ name: 'My Test Plug' });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
expect(screen.getByText('My Test Plug')).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders plug IP address', () => {
|
|
const plug = createMockPlug({ ip_address: '192.168.1.200' });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
expect(screen.getByText('192.168.1.200')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows power ON/OFF buttons', () => {
|
|
const plug = createMockPlug();
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Look for power control buttons
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('automation settings', () => {
|
|
it('shows automation settings section when expanded', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug();
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Find and click the settings toggle
|
|
const settingsToggle = screen.getByText('Automation Settings');
|
|
await user.click(settingsToggle);
|
|
|
|
// Should show Auto On and Auto Off labels
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Auto On')).toBeInTheDocument();
|
|
expect(screen.getByText('Auto Off')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays auto_off toggle in correct state when enabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ auto_off: true });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Expand settings
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
// The toggle should reflect auto_off = true
|
|
const autoOffText = screen.getByText('Auto Off');
|
|
expect(autoOffText).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays auto_off toggle in correct state when disabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ auto_off: false });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Expand settings
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
const autoOffText = screen.getByText('Auto Off');
|
|
expect(autoOffText).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows delay mode options when auto_off is enabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ auto_off: true, off_delay_mode: 'time' });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Expand settings
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
// Should show delay mode buttons
|
|
expect(screen.getByText('Time')).toBeInTheDocument();
|
|
expect(screen.getByText('Temp')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('does not show delay mode options when auto_off is disabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ auto_off: false });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Expand settings
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
// Delay mode options should not be visible
|
|
expect(screen.queryByText('Turn Off Delay Mode')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('schedule display', () => {
|
|
it('shows schedule badge when scheduling is enabled', () => {
|
|
const plug = createMockPlug({
|
|
schedule_enabled: true,
|
|
schedule_on_time: '08:00',
|
|
schedule_off_time: '22:00',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
expect(screen.getByText(/08:00.*22:00/)).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show schedule badge when scheduling is disabled', () => {
|
|
const plug = createMockPlug({
|
|
schedule_enabled: false,
|
|
schedule_on_time: '08:00',
|
|
schedule_off_time: '22:00',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Schedule times should not be visible
|
|
expect(screen.queryByText(/08:00.*22:00/)).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('power control', () => {
|
|
it('shows confirmation modal before power off', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ last_state: 'ON' });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Find and click the Off button
|
|
const offButton = screen.getByRole('button', { name: /off/i });
|
|
await user.click(offButton);
|
|
|
|
// Confirmation modal should appear with the dialog title
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Turn Off Smart Plug')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('edit functionality', () => {
|
|
it('calls onEdit when edit button is clicked', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug();
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Expand settings first
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
// Find and click edit button
|
|
await waitFor(async () => {
|
|
const editButtons = screen.getAllByRole('button');
|
|
const editButton = editButtons.find(
|
|
(btn) =>
|
|
btn.textContent?.includes('Edit') ||
|
|
btn.querySelector('[class*="pencil"]')
|
|
);
|
|
if (editButton) {
|
|
await user.click(editButton);
|
|
}
|
|
});
|
|
|
|
// onEdit should have been called (may not be called if edit button not found)
|
|
// This test verifies the interaction pattern
|
|
});
|
|
});
|
|
|
|
describe('persistent auto-off', () => {
|
|
it('shows Keep Enabled toggle when auto_off is enabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ auto_off: true, auto_off_persistent: false });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Keep Enabled')).toBeInTheDocument();
|
|
expect(screen.getByText('Stay enabled between prints instead of one-shot')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('does not show Keep Enabled toggle when auto_off is disabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({ auto_off: false });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Keep Enabled')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows Keep Enabled toggle for HA plugs with auto_off enabled', async () => {
|
|
const user = userEvent.setup();
|
|
const plug = createMockPlug({
|
|
plug_type: 'homeassistant',
|
|
ip_address: null,
|
|
ha_entity_id: 'switch.bentobox_filter',
|
|
auto_off: true,
|
|
auto_off_persistent: true,
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
await user.click(screen.getByText('Automation Settings'));
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Keep Enabled')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('disabled state', () => {
|
|
it('renders plug even when disabled', () => {
|
|
const plug = createMockPlug({ enabled: false });
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Plug should still render with its name
|
|
expect(screen.getByText('Test Plug')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Home Assistant plugs', () => {
|
|
it('renders HA plug with entity_id instead of IP', () => {
|
|
const plug = createMockPlug({
|
|
plug_type: 'homeassistant',
|
|
ip_address: null,
|
|
ha_entity_id: 'switch.printer_plug',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Should show entity_id, not IP
|
|
expect(screen.getByText('switch.printer_plug')).toBeInTheDocument();
|
|
expect(screen.queryByText('192.168.1.100')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders HA plug name correctly', () => {
|
|
const plug = createMockPlug({
|
|
name: 'HA Printer Plug',
|
|
plug_type: 'homeassistant',
|
|
ip_address: null,
|
|
ha_entity_id: 'switch.printer_plug',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
expect(screen.getByText('HA Printer Plug')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows power controls for HA plug', () => {
|
|
const plug = createMockPlug({
|
|
plug_type: 'homeassistant',
|
|
ip_address: null,
|
|
ha_entity_id: 'switch.printer_plug',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Power control buttons should still be present
|
|
const buttons = screen.getAllByRole('button');
|
|
expect(buttons.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('MQTT plugs', () => {
|
|
it('renders MQTT plug with topic instead of IP', () => {
|
|
const plug = createMockPlug({
|
|
plug_type: 'mqtt',
|
|
ip_address: null,
|
|
mqtt_topic: 'zigbee2mqtt/shelly-power',
|
|
mqtt_power_path: 'power_l1',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Should show topic, not IP
|
|
expect(screen.getByText('zigbee2mqtt/shelly-power')).toBeInTheDocument();
|
|
expect(screen.queryByText('192.168.1.100')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('renders MQTT plug name correctly', () => {
|
|
const plug = createMockPlug({
|
|
name: 'MQTT Energy Monitor',
|
|
plug_type: 'mqtt',
|
|
ip_address: null,
|
|
mqtt_topic: 'sensors/power',
|
|
mqtt_power_path: 'power',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
expect(screen.getByText('MQTT Energy Monitor')).toBeInTheDocument();
|
|
});
|
|
|
|
it('shows Monitor Only badge for MQTT plug', () => {
|
|
const plug = createMockPlug({
|
|
plug_type: 'mqtt',
|
|
ip_address: null,
|
|
mqtt_topic: 'test/topic',
|
|
mqtt_power_path: 'power',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
expect(screen.getByText('Monitor Only')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not show power control buttons for MQTT plug', () => {
|
|
const plug = createMockPlug({
|
|
plug_type: 'mqtt',
|
|
ip_address: null,
|
|
mqtt_topic: 'test/topic',
|
|
mqtt_power_path: 'power',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// On/Off buttons should not be present for monitor-only plugs
|
|
expect(screen.queryByRole('button', { name: /^on$/i })).not.toBeInTheDocument();
|
|
expect(screen.queryByRole('button', { name: /^off$/i })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('shows Settings instead of Automation Settings for MQTT plug', async () => {
|
|
const plug = createMockPlug({
|
|
plug_type: 'mqtt',
|
|
ip_address: null,
|
|
mqtt_topic: 'test/topic',
|
|
mqtt_power_path: 'power',
|
|
});
|
|
render(<SmartPlugCard plug={plug} onEdit={mockOnEdit} />);
|
|
|
|
// Should show "Settings" not "Automation Settings"
|
|
expect(screen.getByText('Settings')).toBeInTheDocument();
|
|
expect(screen.queryByText('Automation Settings')).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|