AMSHistoryModal.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /**
  2. * Tests for the AMSHistoryModal component.
  3. */
  4. import { describe, it, expect, vi, beforeEach } from 'vitest';
  5. import { screen, waitFor, fireEvent } from '@testing-library/react';
  6. import { render } from '../utils';
  7. import { AMSHistoryModal } from '../../components/AMSHistoryModal';
  8. import { api } from '../../api/client';
  9. // Mock the API client
  10. vi.mock('../../api/client', () => ({
  11. api: {
  12. getAMSHistory: vi.fn(),
  13. getSettings: vi.fn().mockResolvedValue({}),
  14. updateSettings: vi.fn().mockResolvedValue({}),
  15. },
  16. }));
  17. // Mock recharts to avoid rendering issues in tests
  18. vi.mock('recharts', () => ({
  19. LineChart: ({ children }: { children: React.ReactNode }) => <div data-testid="line-chart">{children}</div>,
  20. Line: () => null,
  21. XAxis: () => null,
  22. YAxis: () => null,
  23. CartesianGrid: () => null,
  24. Tooltip: () => null,
  25. ResponsiveContainer: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  26. Legend: () => null,
  27. ReferenceLine: () => null,
  28. }));
  29. const mockHistoryData = {
  30. data: [
  31. { recorded_at: '2024-12-11T10:00:00Z', humidity: 45, temperature: 28 },
  32. { recorded_at: '2024-12-11T10:05:00Z', humidity: 46, temperature: 27 },
  33. { recorded_at: '2024-12-11T10:10:00Z', humidity: 44, temperature: 29 },
  34. { recorded_at: '2024-12-11T10:15:00Z', humidity: 47, temperature: 28 },
  35. { recorded_at: '2024-12-11T10:20:00Z', humidity: 48, temperature: 30 },
  36. ],
  37. avg_humidity: 46,
  38. min_humidity: 44,
  39. max_humidity: 48,
  40. avg_temperature: 28.4,
  41. min_temperature: 27,
  42. max_temperature: 30,
  43. };
  44. const defaultProps = {
  45. isOpen: true,
  46. onClose: vi.fn(),
  47. printerId: 1,
  48. printerName: 'Test Printer',
  49. amsId: 0,
  50. amsLabel: 'AMS-A',
  51. initialMode: 'humidity' as const,
  52. thresholds: {
  53. humidityGood: 40,
  54. humidityFair: 60,
  55. tempGood: 30,
  56. tempFair: 35,
  57. },
  58. };
  59. describe('AMSHistoryModal', () => {
  60. beforeEach(() => {
  61. vi.clearAllMocks();
  62. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockResolvedValue(mockHistoryData);
  63. });
  64. it('renders nothing visible when closed', () => {
  65. render(<AMSHistoryModal {...defaultProps} isOpen={false} />);
  66. // The modal content should not be visible when closed
  67. expect(screen.queryByText('AMS-A History')).not.toBeInTheDocument();
  68. });
  69. it('renders modal when open', async () => {
  70. render(<AMSHistoryModal {...defaultProps} />);
  71. await waitFor(() => {
  72. expect(screen.getByText('AMS-A History')).toBeInTheDocument();
  73. });
  74. expect(screen.getByText('Test Printer')).toBeInTheDocument();
  75. });
  76. it('displays humidity mode by default', async () => {
  77. render(<AMSHistoryModal {...defaultProps} />);
  78. await waitFor(() => {
  79. expect(screen.getByText('Humidity')).toBeInTheDocument();
  80. });
  81. // Should show humidity stats - the Average value
  82. await waitFor(() => {
  83. expect(screen.getByText('Average')).toBeInTheDocument();
  84. });
  85. });
  86. it('displays temperature mode when initialMode is temperature', async () => {
  87. render(<AMSHistoryModal {...defaultProps} initialMode="temperature" />);
  88. await waitFor(() => {
  89. expect(screen.getByText('Temperature')).toBeInTheDocument();
  90. });
  91. });
  92. it('shows time range buttons', async () => {
  93. render(<AMSHistoryModal {...defaultProps} />);
  94. await waitFor(() => {
  95. expect(screen.getByText('6h')).toBeInTheDocument();
  96. });
  97. expect(screen.getByText('24h')).toBeInTheDocument();
  98. expect(screen.getByText('48h')).toBeInTheDocument();
  99. expect(screen.getByText('7d')).toBeInTheDocument();
  100. });
  101. it('switches between humidity and temperature modes', async () => {
  102. render(<AMSHistoryModal {...defaultProps} />);
  103. await waitFor(() => {
  104. expect(screen.getByText('Humidity')).toBeInTheDocument();
  105. });
  106. // Click temperature button
  107. const tempButton = screen.getByText('Temperature');
  108. fireEvent.click(tempButton);
  109. // Should now show temperature mode is active (button styling changes)
  110. await waitFor(() => {
  111. // Temperature stats should be visible - checking the labels
  112. expect(screen.getByText('Min')).toBeInTheDocument();
  113. expect(screen.getByText('Max')).toBeInTheDocument();
  114. });
  115. });
  116. it('displays statistics cards', async () => {
  117. render(<AMSHistoryModal {...defaultProps} />);
  118. await waitFor(() => {
  119. expect(screen.getByText('Current')).toBeInTheDocument();
  120. });
  121. expect(screen.getByText('Average')).toBeInTheDocument();
  122. expect(screen.getByText('Min')).toBeInTheDocument();
  123. expect(screen.getByText('Max')).toBeInTheDocument();
  124. });
  125. it('displays min/max humidity values', async () => {
  126. render(<AMSHistoryModal {...defaultProps} />);
  127. await waitFor(() => {
  128. // Min humidity - may appear multiple times
  129. const minValues = screen.getAllByText('44%');
  130. expect(minValues.length).toBeGreaterThanOrEqual(1);
  131. });
  132. // Max humidity - may appear multiple times (in current and max cards)
  133. const maxValues = screen.getAllByText('48%');
  134. expect(maxValues.length).toBeGreaterThanOrEqual(1);
  135. });
  136. it('displays min/max temperature values in temperature mode', async () => {
  137. render(<AMSHistoryModal {...defaultProps} initialMode="temperature" />);
  138. await waitFor(() => {
  139. // Min temp appears in the Min card
  140. const minCards = screen.getAllByText('27°C');
  141. expect(minCards.length).toBeGreaterThanOrEqual(1);
  142. });
  143. // Max temp appears in the Max card (may appear multiple times in different contexts)
  144. const maxCards = screen.getAllByText('30°C');
  145. expect(maxCards.length).toBeGreaterThanOrEqual(1);
  146. });
  147. it('calls onClose when close button clicked', async () => {
  148. const onClose = vi.fn();
  149. render(<AMSHistoryModal {...defaultProps} onClose={onClose} />);
  150. await waitFor(() => {
  151. expect(screen.getByText('AMS-A History')).toBeInTheDocument();
  152. });
  153. // Find and click close button (X icon)
  154. const closeButton = document.querySelector('button');
  155. if (closeButton) {
  156. fireEvent.click(closeButton);
  157. }
  158. });
  159. it('calls onClose when clicking backdrop', async () => {
  160. const onClose = vi.fn();
  161. render(<AMSHistoryModal {...defaultProps} onClose={onClose} />);
  162. await waitFor(() => {
  163. expect(screen.getByText('AMS-A History')).toBeInTheDocument();
  164. });
  165. // Click on backdrop (the fixed overlay)
  166. const backdrop = document.querySelector('.fixed.inset-0');
  167. if (backdrop) {
  168. fireEvent.click(backdrop);
  169. expect(onClose).toHaveBeenCalled();
  170. }
  171. });
  172. it('does not close when clicking modal content', async () => {
  173. const onClose = vi.fn();
  174. render(<AMSHistoryModal {...defaultProps} onClose={onClose} />);
  175. await waitFor(() => {
  176. expect(screen.getByText('AMS-A History')).toBeInTheDocument();
  177. });
  178. // Click on modal content (should not close)
  179. const modalContent = document.querySelector('.rounded-xl');
  180. if (modalContent) {
  181. fireEvent.click(modalContent);
  182. expect(onClose).not.toHaveBeenCalled();
  183. }
  184. });
  185. it('shows loading state', async () => {
  186. // Make API call never resolve
  187. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockImplementation(
  188. () => new Promise(() => {})
  189. );
  190. render(<AMSHistoryModal {...defaultProps} />);
  191. await waitFor(() => {
  192. expect(screen.getByText('Loading...')).toBeInTheDocument();
  193. });
  194. });
  195. it('shows error state on API failure', async () => {
  196. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockRejectedValue(
  197. new Error('Network error')
  198. );
  199. render(<AMSHistoryModal {...defaultProps} />);
  200. await waitFor(() => {
  201. expect(screen.getByText('Error')).toBeInTheDocument();
  202. });
  203. });
  204. it('shows no data message when empty', async () => {
  205. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockResolvedValue({
  206. data: [],
  207. avg_humidity: null,
  208. min_humidity: null,
  209. max_humidity: null,
  210. avg_temperature: null,
  211. min_temperature: null,
  212. max_temperature: null,
  213. });
  214. render(<AMSHistoryModal {...defaultProps} />);
  215. await waitFor(() => {
  216. expect(screen.getByText('No data available')).toBeInTheDocument();
  217. });
  218. });
  219. it('changes time range when clicking different range buttons', async () => {
  220. render(<AMSHistoryModal {...defaultProps} />);
  221. await waitFor(() => {
  222. expect(screen.getByText('6h')).toBeInTheDocument();
  223. });
  224. // Click 7d button
  225. fireEvent.click(screen.getByText('7d'));
  226. // API should be called with 168 hours (7 days)
  227. await waitFor(() => {
  228. expect(api.getAMSHistory).toHaveBeenCalledWith(1, 0, 168);
  229. });
  230. });
  231. it('displays recording info text', async () => {
  232. render(<AMSHistoryModal {...defaultProps} />);
  233. await waitFor(() => {
  234. expect(screen.getByText(/data is recorded every 5 minutes/i)).toBeInTheDocument();
  235. });
  236. });
  237. it('displays current value with correct color based on threshold', async () => {
  238. // Test with humidity value above fair threshold
  239. const highHumidityData = {
  240. ...mockHistoryData,
  241. data: [
  242. ...mockHistoryData.data,
  243. { recorded_at: '2024-12-11T10:25:00Z', humidity: 75, temperature: 28 },
  244. ],
  245. };
  246. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockResolvedValue(highHumidityData);
  247. render(<AMSHistoryModal {...defaultProps} />);
  248. await waitFor(() => {
  249. // The current value (75%) should be displayed
  250. expect(screen.getByText('75%')).toBeInTheDocument();
  251. });
  252. });
  253. it('renders chart component', async () => {
  254. render(<AMSHistoryModal {...defaultProps} />);
  255. await waitFor(() => {
  256. expect(screen.getByTestId('line-chart')).toBeInTheDocument();
  257. });
  258. });
  259. });
  260. describe('AMSHistoryModal trend calculation', () => {
  261. beforeEach(() => {
  262. vi.clearAllMocks();
  263. });
  264. it('shows stable trend when values are similar', async () => {
  265. const stableData = {
  266. data: Array.from({ length: 20 }, (_, i) => ({
  267. recorded_at: new Date(Date.now() - i * 5 * 60 * 1000).toISOString(),
  268. humidity: 45, // Same value
  269. temperature: 28,
  270. })),
  271. avg_humidity: 45,
  272. min_humidity: 45,
  273. max_humidity: 45,
  274. avg_temperature: 28,
  275. min_temperature: 28,
  276. max_temperature: 28,
  277. };
  278. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockResolvedValue(stableData);
  279. render(<AMSHistoryModal {...defaultProps} />);
  280. await waitFor(() => {
  281. expect(screen.getByText('Current')).toBeInTheDocument();
  282. });
  283. // Should show stable trend icon (horizontal line)
  284. // The Minus icon indicates stable trend
  285. });
  286. it('shows upward trend when values increase', async () => {
  287. const increasingData = {
  288. data: Array.from({ length: 20 }, (_, i) => ({
  289. recorded_at: new Date(Date.now() - (20 - i) * 5 * 60 * 1000).toISOString(),
  290. humidity: 30 + i * 2, // Increasing values
  291. temperature: 28,
  292. })),
  293. avg_humidity: 50,
  294. min_humidity: 30,
  295. max_humidity: 68,
  296. avg_temperature: 28,
  297. min_temperature: 28,
  298. max_temperature: 28,
  299. };
  300. (api.getAMSHistory as ReturnType<typeof vi.fn>).mockResolvedValue(increasingData);
  301. render(<AMSHistoryModal {...defaultProps} />);
  302. await waitFor(() => {
  303. expect(screen.getByText('Current')).toBeInTheDocument();
  304. });
  305. // Should show upward trend icon (TrendingUp)
  306. });
  307. });