Files
bambuddy/frontend/src/__tests__/components/SmartPlugCard.test.tsx
T
maziggy c6f62f9cd9 Add persistent auto-off option for smart plugs (#826)
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.
2026-03-27 07:50:47 +01:00

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();
});
});
});