SystemInfoPage.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /**
  2. * Tests for the SystemInfoPage component.
  3. */
  4. import { describe, it, expect, vi, beforeEach } from 'vitest';
  5. import { screen, waitFor } from '@testing-library/react';
  6. import { render } from '../utils';
  7. import { SystemInfoPage } from '../../pages/SystemInfoPage';
  8. import { api } from '../../api/client';
  9. // Every permission unless a test takes one away.
  10. const permissions = vi.hoisted(() => ({ denied: [] as string[] }));
  11. vi.mock('../../contexts/AuthContext', async (importOriginal) => {
  12. const actual = await importOriginal<typeof import('../../contexts/AuthContext')>();
  13. return {
  14. ...actual,
  15. useAuth: () => ({
  16. ...actual.useAuth(),
  17. hasPermission: (permission: string) => !permissions.denied.includes(permission),
  18. }),
  19. };
  20. });
  21. // Mock the API client
  22. vi.mock('../../api/client', () => ({
  23. api: {
  24. getSystemInfo: vi.fn(),
  25. getSettings: vi.fn().mockResolvedValue({}),
  26. updateSettings: vi.fn().mockResolvedValue({}),
  27. },
  28. supportApi: {
  29. getDebugLoggingState: vi.fn().mockResolvedValue({ enabled: false, enabled_at: null, duration_seconds: null }),
  30. setDebugLogging: vi.fn().mockResolvedValue({ enabled: true, enabled_at: new Date().toISOString(), duration_seconds: 0 }),
  31. downloadSupportBundle: vi.fn().mockResolvedValue(undefined),
  32. },
  33. }));
  34. // Mock system info response
  35. const mockSystemInfo = {
  36. app: {
  37. version: '0.1.5b',
  38. base_dir: '/opt/bambuddy',
  39. archive_dir: '/opt/bambuddy/archives',
  40. },
  41. database: {
  42. engine: 'SQLite',
  43. version: 'SQLite 3.45.1',
  44. archives: 150,
  45. archives_completed: 140,
  46. archives_failed: 8,
  47. archives_printing: 2,
  48. printers: 3,
  49. filaments: 25,
  50. projects: 5,
  51. smart_plugs: 2,
  52. total_print_time_seconds: 360000,
  53. total_print_time_formatted: '100h',
  54. total_filament_grams: 5000,
  55. total_filament_kg: 5.0,
  56. },
  57. printers: {
  58. total: 3,
  59. connected: 2,
  60. connected_list: [
  61. { id: 1, name: 'X1C-01', state: 'IDLE', model: 'X1C' },
  62. { id: 2, name: 'P1S-01', state: 'RUNNING', model: 'P1S' },
  63. ],
  64. },
  65. storage: {
  66. archive_size_bytes: 1073741824,
  67. archive_size_formatted: '1.0 GB',
  68. database_size_bytes: 10485760,
  69. database_size_formatted: '10.0 MB',
  70. disk_total_bytes: 107374182400,
  71. disk_total_formatted: '100.0 GB',
  72. disk_used_bytes: 53687091200,
  73. disk_used_formatted: '50.0 GB',
  74. disk_free_bytes: 53687091200,
  75. disk_free_formatted: '50.0 GB',
  76. disk_percent_used: 50.0,
  77. },
  78. system: {
  79. platform: 'Linux',
  80. platform_release: '5.15.0',
  81. platform_version: '#1 SMP',
  82. architecture: 'x86_64',
  83. hostname: 'bambuddy-server',
  84. python_version: '3.11.0',
  85. uptime_seconds: 86400,
  86. uptime_formatted: '1d',
  87. boot_time: '2024-12-11T00:00:00',
  88. },
  89. memory: {
  90. total_bytes: 17179869184,
  91. total_formatted: '16.0 GB',
  92. available_bytes: 8589934592,
  93. available_formatted: '8.0 GB',
  94. used_bytes: 8589934592,
  95. used_formatted: '8.0 GB',
  96. percent_used: 50.0,
  97. },
  98. cpu: {
  99. count: 4,
  100. count_logical: 8,
  101. percent: 25.0,
  102. },
  103. };
  104. describe('SystemInfoPage', () => {
  105. beforeEach(() => {
  106. vi.clearAllMocks();
  107. permissions.denied = [];
  108. });
  109. it('shows the support tools only to users who may change settings', async () => {
  110. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  111. permissions.denied = ['settings:update'];
  112. render(<SystemInfoPage />);
  113. await waitFor(() => {
  114. expect(screen.getByText('X1C-01')).toBeInTheDocument();
  115. });
  116. expect(screen.queryByText("What's in the support bundle?")).not.toBeInTheDocument();
  117. });
  118. it('renders loading state initially', async () => {
  119. // Make the API call never resolve to test loading state
  120. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockImplementation(
  121. () => new Promise(() => {})
  122. );
  123. render(<SystemInfoPage />);
  124. // Should show loading spinner
  125. expect(document.querySelector('.animate-spin')).toBeInTheDocument();
  126. });
  127. it('renders system info when data loads', async () => {
  128. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  129. render(<SystemInfoPage />);
  130. await waitFor(() => {
  131. expect(screen.getByText('System Information')).toBeInTheDocument();
  132. });
  133. // Check for version
  134. expect(screen.getByText('v0.1.5b')).toBeInTheDocument();
  135. });
  136. it('displays application section', async () => {
  137. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  138. render(<SystemInfoPage />);
  139. await waitFor(() => {
  140. expect(screen.getByText('Application')).toBeInTheDocument();
  141. });
  142. expect(screen.getByText('v0.1.5b')).toBeInTheDocument();
  143. expect(screen.getByText('bambuddy-server')).toBeInTheDocument();
  144. expect(screen.getByText('1d')).toBeInTheDocument();
  145. });
  146. it('displays database statistics', async () => {
  147. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  148. render(<SystemInfoPage />);
  149. await waitFor(() => {
  150. expect(screen.getByText('Database')).toBeInTheDocument();
  151. });
  152. // Check archive counts
  153. expect(screen.getByText('150')).toBeInTheDocument(); // Total archives
  154. expect(screen.getByText('140')).toBeInTheDocument(); // Completed
  155. expect(screen.getByText('8')).toBeInTheDocument(); // Failed
  156. });
  157. it('displays connected printers', async () => {
  158. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  159. render(<SystemInfoPage />);
  160. await waitFor(() => {
  161. expect(screen.getByText('Connected Printers')).toBeInTheDocument();
  162. });
  163. // Check connected printer names
  164. expect(screen.getByText('X1C-01')).toBeInTheDocument();
  165. expect(screen.getByText('P1S-01')).toBeInTheDocument();
  166. // Check printer states
  167. expect(screen.getByText('IDLE')).toBeInTheDocument();
  168. expect(screen.getByText('RUNNING')).toBeInTheDocument();
  169. });
  170. it('displays storage information', async () => {
  171. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  172. render(<SystemInfoPage />);
  173. await waitFor(() => {
  174. expect(screen.getByText('Storage')).toBeInTheDocument();
  175. });
  176. expect(screen.getByText('1.0 GB')).toBeInTheDocument(); // Archive size
  177. expect(screen.getByText('10.0 MB')).toBeInTheDocument(); // Database size
  178. });
  179. it('displays memory usage', async () => {
  180. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  181. render(<SystemInfoPage />);
  182. await waitFor(() => {
  183. expect(screen.getByText('Memory')).toBeInTheDocument();
  184. });
  185. expect(screen.getByText('8.0 GB available')).toBeInTheDocument();
  186. });
  187. it('displays CPU information', async () => {
  188. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  189. render(<SystemInfoPage />);
  190. await waitFor(() => {
  191. expect(screen.getByText('CPU')).toBeInTheDocument();
  192. });
  193. expect(screen.getByText('4')).toBeInTheDocument(); // CPU cores
  194. expect(screen.getByText('25%')).toBeInTheDocument(); // CPU usage
  195. });
  196. it('displays system details', async () => {
  197. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  198. render(<SystemInfoPage />);
  199. await waitFor(() => {
  200. expect(screen.getByText('System Details')).toBeInTheDocument();
  201. });
  202. expect(screen.getByText('Linux')).toBeInTheDocument();
  203. expect(screen.getByText('x86_64')).toBeInTheDocument();
  204. expect(screen.getByText('3.11.0')).toBeInTheDocument(); // Python version
  205. });
  206. it('shows error state when data fails to load', async () => {
  207. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(null);
  208. render(<SystemInfoPage />);
  209. await waitFor(() => {
  210. expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
  211. });
  212. });
  213. it('shows no printers message when none connected', async () => {
  214. const noConnectedPrinters = {
  215. ...mockSystemInfo,
  216. printers: {
  217. total: 3,
  218. connected: 0,
  219. connected_list: [],
  220. },
  221. };
  222. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(noConnectedPrinters);
  223. render(<SystemInfoPage />);
  224. await waitFor(() => {
  225. expect(screen.getByText(/no printers connected/i)).toBeInTheDocument();
  226. });
  227. });
  228. it('has refresh button', async () => {
  229. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  230. render(<SystemInfoPage />);
  231. await waitFor(() => {
  232. expect(screen.getByText('Refresh')).toBeInTheDocument();
  233. });
  234. });
  235. it('applies warning color for high disk usage', async () => {
  236. const highDiskUsage = {
  237. ...mockSystemInfo,
  238. storage: {
  239. ...mockSystemInfo.storage,
  240. disk_percent_used: 80,
  241. },
  242. };
  243. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(highDiskUsage);
  244. render(<SystemInfoPage />);
  245. await waitFor(() => {
  246. expect(screen.getByText('Storage')).toBeInTheDocument();
  247. });
  248. // The progress bar should have yellow color for 75-90% usage
  249. const progressBars = document.querySelectorAll('[class*="bg-yellow"]');
  250. expect(progressBars.length).toBeGreaterThan(0);
  251. });
  252. it('displays extended privacy disclosure items', async () => {
  253. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(mockSystemInfo);
  254. render(<SystemInfoPage />);
  255. await waitFor(() => {
  256. expect(screen.getByText("What's in the support bundle?")).toBeInTheDocument();
  257. });
  258. // Original items
  259. expect(screen.getByText(/App version and debug mode/)).toBeInTheDocument();
  260. expect(screen.getByText(/Debug logs \(sanitized\)/)).toBeInTheDocument();
  261. // New diagnostic items
  262. expect(screen.getByText(/Printer connectivity and firmware versions/)).toBeInTheDocument();
  263. expect(screen.getByText(/Integration status \(Spoolman, MQTT, HA\)/)).toBeInTheDocument();
  264. expect(screen.getByText(/Network interfaces \(subnets only\)/)).toBeInTheDocument();
  265. expect(screen.getByText(/Python package versions/)).toBeInTheDocument();
  266. expect(screen.getByText(/Database health checks/)).toBeInTheDocument();
  267. expect(screen.getByText(/Docker environment details/)).toBeInTheDocument();
  268. });
  269. it('applies danger color for critical disk usage', async () => {
  270. const criticalDiskUsage = {
  271. ...mockSystemInfo,
  272. storage: {
  273. ...mockSystemInfo.storage,
  274. disk_percent_used: 95,
  275. },
  276. };
  277. (api.getSystemInfo as ReturnType<typeof vi.fn>).mockResolvedValue(criticalDiskUsage);
  278. render(<SystemInfoPage />);
  279. await waitFor(() => {
  280. expect(screen.getByText('Storage')).toBeInTheDocument();
  281. });
  282. // The progress bar should have red color for >90% usage
  283. const progressBars = document.querySelectorAll('[class*="bg-red"]');
  284. expect(progressBars.length).toBeGreaterThan(0);
  285. });
  286. });