/** * Tests for the AMSHistoryModal component. */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, waitFor, fireEvent } from '@testing-library/react'; import { render } from '../utils'; import { AMSHistoryModal } from '../../components/AMSHistoryModal'; import { api } from '../../api/client'; // Mock the API client vi.mock('../../api/client', () => ({ api: { getAMSHistory: vi.fn(), getSettings: vi.fn().mockResolvedValue({}), updateSettings: vi.fn().mockResolvedValue({}), }, })); // Mock recharts to avoid rendering issues in tests vi.mock('recharts', () => ({ LineChart: ({ children }: { children: React.ReactNode }) =>
{children}
, Line: () => null, XAxis: () => null, YAxis: () => null, CartesianGrid: () => null, Tooltip: () => null, ResponsiveContainer: ({ children }: { children: React.ReactNode }) =>
{children}
, Legend: () => null, ReferenceLine: () => null, })); const mockHistoryData = { data: [ { recorded_at: '2024-12-11T10:00:00Z', humidity: 45, temperature: 28 }, { recorded_at: '2024-12-11T10:05:00Z', humidity: 46, temperature: 27 }, { recorded_at: '2024-12-11T10:10:00Z', humidity: 44, temperature: 29 }, { recorded_at: '2024-12-11T10:15:00Z', humidity: 47, temperature: 28 }, { recorded_at: '2024-12-11T10:20:00Z', humidity: 48, temperature: 30 }, ], avg_humidity: 46, min_humidity: 44, max_humidity: 48, avg_temperature: 28.4, min_temperature: 27, max_temperature: 30, }; const defaultProps = { isOpen: true, onClose: vi.fn(), printerId: 1, printerName: 'Test Printer', amsId: 0, amsLabel: 'AMS-A', initialMode: 'humidity' as const, thresholds: { humidityGood: 40, humidityFair: 60, tempGood: 30, tempFair: 35, }, }; describe('AMSHistoryModal', () => { beforeEach(() => { vi.clearAllMocks(); (api.getAMSHistory as ReturnType).mockResolvedValue(mockHistoryData); }); it('renders nothing visible when closed', () => { render(); // The modal content should not be visible when closed expect(screen.queryByText('AMS-A History')).not.toBeInTheDocument(); }); it('renders modal when open', async () => { render(); await waitFor(() => { expect(screen.getByText('AMS-A History')).toBeInTheDocument(); }); expect(screen.getByText('Test Printer')).toBeInTheDocument(); }); it('displays humidity mode by default', async () => { render(); await waitFor(() => { expect(screen.getByText('Humidity')).toBeInTheDocument(); }); // Should show humidity stats - the Average value await waitFor(() => { expect(screen.getByText('Average')).toBeInTheDocument(); }); }); it('displays temperature mode when initialMode is temperature', async () => { render(); await waitFor(() => { expect(screen.getByText('Temperature')).toBeInTheDocument(); }); }); it('shows time range buttons', async () => { render(); await waitFor(() => { expect(screen.getByText('6h')).toBeInTheDocument(); }); expect(screen.getByText('24h')).toBeInTheDocument(); expect(screen.getByText('48h')).toBeInTheDocument(); expect(screen.getByText('7d')).toBeInTheDocument(); }); it('switches between humidity and temperature modes', async () => { render(); await waitFor(() => { expect(screen.getByText('Humidity')).toBeInTheDocument(); }); // Click temperature button const tempButton = screen.getByText('Temperature'); fireEvent.click(tempButton); // Should now show temperature mode is active (button styling changes) await waitFor(() => { // Temperature stats should be visible - checking the labels expect(screen.getByText('Min')).toBeInTheDocument(); expect(screen.getByText('Max')).toBeInTheDocument(); }); }); it('displays statistics cards', async () => { render(); await waitFor(() => { expect(screen.getByText('Current')).toBeInTheDocument(); }); expect(screen.getByText('Average')).toBeInTheDocument(); expect(screen.getByText('Min')).toBeInTheDocument(); expect(screen.getByText('Max')).toBeInTheDocument(); }); it('displays min/max humidity values', async () => { render(); await waitFor(() => { // Min humidity - may appear multiple times const minValues = screen.getAllByText('44%'); expect(minValues.length).toBeGreaterThanOrEqual(1); }); // Max humidity - may appear multiple times (in current and max cards) const maxValues = screen.getAllByText('48%'); expect(maxValues.length).toBeGreaterThanOrEqual(1); }); it('displays min/max temperature values in temperature mode', async () => { render(); await waitFor(() => { // Min temp appears in the Min card const minCards = screen.getAllByText('27°C'); expect(minCards.length).toBeGreaterThanOrEqual(1); }); // Max temp appears in the Max card (may appear multiple times in different contexts) const maxCards = screen.getAllByText('30°C'); expect(maxCards.length).toBeGreaterThanOrEqual(1); }); it('calls onClose when close button clicked', async () => { const onClose = vi.fn(); render(); await waitFor(() => { expect(screen.getByText('AMS-A History')).toBeInTheDocument(); }); // Find and click close button (X icon) const closeButton = document.querySelector('button'); if (closeButton) { fireEvent.click(closeButton); } }); it('calls onClose when clicking backdrop', async () => { const onClose = vi.fn(); render(); await waitFor(() => { expect(screen.getByText('AMS-A History')).toBeInTheDocument(); }); // Click on backdrop (the fixed overlay) const backdrop = document.querySelector('.fixed.inset-0'); if (backdrop) { fireEvent.click(backdrop); expect(onClose).toHaveBeenCalled(); } }); it('does not close when clicking modal content', async () => { const onClose = vi.fn(); render(); await waitFor(() => { expect(screen.getByText('AMS-A History')).toBeInTheDocument(); }); // Click on modal content (should not close) const modalContent = document.querySelector('.rounded-xl'); if (modalContent) { fireEvent.click(modalContent); expect(onClose).not.toHaveBeenCalled(); } }); it('shows loading state', async () => { // Make API call never resolve (api.getAMSHistory as ReturnType).mockImplementation( () => new Promise(() => {}) ); render(); await waitFor(() => { expect(screen.getByText('Loading...')).toBeInTheDocument(); }); }); it('shows error state on API failure', async () => { (api.getAMSHistory as ReturnType).mockRejectedValue( new Error('Network error') ); render(); await waitFor(() => { expect(screen.getByText('Error')).toBeInTheDocument(); }); }); it('shows no data message when empty', async () => { (api.getAMSHistory as ReturnType).mockResolvedValue({ data: [], avg_humidity: null, min_humidity: null, max_humidity: null, avg_temperature: null, min_temperature: null, max_temperature: null, }); render(); await waitFor(() => { expect(screen.getByText('No data available')).toBeInTheDocument(); }); }); it('changes time range when clicking different range buttons', async () => { render(); await waitFor(() => { expect(screen.getByText('6h')).toBeInTheDocument(); }); // Click 7d button fireEvent.click(screen.getByText('7d')); // API should be called with 168 hours (7 days) await waitFor(() => { expect(api.getAMSHistory).toHaveBeenCalledWith(1, 0, 168); }); }); it('displays recording info text', async () => { render(); await waitFor(() => { expect(screen.getByText(/data is recorded every 5 minutes/i)).toBeInTheDocument(); }); }); it('displays current value with correct color based on threshold', async () => { // Test with humidity value above fair threshold const highHumidityData = { ...mockHistoryData, data: [ ...mockHistoryData.data, { recorded_at: '2024-12-11T10:25:00Z', humidity: 75, temperature: 28 }, ], }; (api.getAMSHistory as ReturnType).mockResolvedValue(highHumidityData); render(); await waitFor(() => { // The current value (75%) should be displayed expect(screen.getByText('75%')).toBeInTheDocument(); }); }); it('renders chart component', async () => { render(); await waitFor(() => { expect(screen.getByTestId('line-chart')).toBeInTheDocument(); }); }); }); describe('AMSHistoryModal trend calculation', () => { beforeEach(() => { vi.clearAllMocks(); }); it('shows stable trend when values are similar', async () => { const stableData = { data: Array.from({ length: 20 }, (_, i) => ({ recorded_at: new Date(Date.now() - i * 5 * 60 * 1000).toISOString(), humidity: 45, // Same value temperature: 28, })), avg_humidity: 45, min_humidity: 45, max_humidity: 45, avg_temperature: 28, min_temperature: 28, max_temperature: 28, }; (api.getAMSHistory as ReturnType).mockResolvedValue(stableData); render(); await waitFor(() => { expect(screen.getByText('Current')).toBeInTheDocument(); }); // Should show stable trend icon (horizontal line) // The Minus icon indicates stable trend }); it('shows upward trend when values increase', async () => { const increasingData = { data: Array.from({ length: 20 }, (_, i) => ({ recorded_at: new Date(Date.now() - (20 - i) * 5 * 60 * 1000).toISOString(), humidity: 30 + i * 2, // Increasing values temperature: 28, })), avg_humidity: 50, min_humidity: 30, max_humidity: 68, avg_temperature: 28, min_temperature: 28, max_temperature: 28, }; (api.getAMSHistory as ReturnType).mockResolvedValue(increasingData); render(); await waitFor(() => { expect(screen.getByText('Current')).toBeInTheDocument(); }); // Should show upward trend icon (TrendingUp) }); });