SpoolBuddySettingsPage.test.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /**
  2. * Tests for SpoolBuddySettingsPage:
  3. * - Renders 5 tabs (Device, Display, Scale, Updates, System)
  4. * - Device tab shows hostname, IP, NFC status
  5. * - Updates tab shows "Check for Updates" button
  6. * - System tab shows OS stats
  7. * - Tab switching works
  8. */
  9. import { describe, it, expect, vi, beforeEach } from 'vitest';
  10. import { screen, waitFor, fireEvent } from '@testing-library/react';
  11. import React from 'react';
  12. import { render } from '@testing-library/react';
  13. import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  14. import { MemoryRouter, Route, Routes, Outlet } from 'react-router-dom';
  15. import { SpoolBuddySettingsPage } from '../../pages/spoolbuddy/SpoolBuddySettingsPage';
  16. vi.mock('../../api/client', () => ({
  17. spoolbuddyApi: {
  18. getDevices: vi.fn().mockResolvedValue([{
  19. id: 1,
  20. device_id: 'sb-test-001',
  21. hostname: 'spoolbuddy-pi',
  22. ip_address: '192.168.1.100',
  23. firmware_version: '1.2.3',
  24. has_nfc: true,
  25. has_scale: true,
  26. tare_offset: 0,
  27. calibration_factor: 1.0,
  28. nfc_reader_type: 'PN532',
  29. nfc_connection: 'I2C',
  30. display_brightness: 80,
  31. display_blank_timeout: 300,
  32. has_backlight: true,
  33. last_calibrated_at: null,
  34. last_seen: '2026-03-22T12:00:00Z',
  35. pending_command: null,
  36. nfc_ok: true,
  37. scale_ok: true,
  38. uptime_s: 3600,
  39. update_status: null,
  40. update_message: null,
  41. system_stats: {
  42. os: { os: 'Raspbian GNU/Linux 12', kernel: '6.1.0-rpi7', arch: 'aarch64', python: '3.11.2' },
  43. cpu_temp_c: 52.1,
  44. cpu_count: 4,
  45. load_avg: [0.15, 0.22, 0.18],
  46. memory: { total_mb: 1024, available_mb: 512, used_mb: 512, percent: 50.0 },
  47. disk: { total_gb: 29.7, used_gb: 8.2, free_gb: 21.5, percent: 27.6 },
  48. system_uptime_s: 86400,
  49. },
  50. online: true,
  51. }]),
  52. updateDisplay: vi.fn().mockResolvedValue({ status: 'ok' }),
  53. tare: vi.fn().mockResolvedValue({ status: 'ok' }),
  54. setCalibrationFactor: vi.fn().mockResolvedValue({ status: 'ok' }),
  55. checkDaemonUpdate: vi.fn().mockResolvedValue({
  56. current_version: '1.2.3',
  57. latest_version: '1.2.3',
  58. update_available: false,
  59. }),
  60. triggerUpdate: vi.fn().mockResolvedValue({ status: 'ok', message: '' }),
  61. getSSHPublicKey: vi.fn().mockResolvedValue({ public_key: 'ssh-ed25519 AAAA test-key' }),
  62. },
  63. }));
  64. vi.mock('react-i18next', () => ({
  65. useTranslation: () => ({
  66. t: (_key: string, fallback: string) => fallback,
  67. i18n: { language: 'en', changeLanguage: vi.fn() },
  68. }),
  69. }));
  70. const mockOutletContext = {
  71. selectedPrinterId: null,
  72. setSelectedPrinterId: vi.fn(),
  73. sbState: {
  74. weight: 250.0,
  75. weightStable: true,
  76. rawAdc: 12345,
  77. matchedSpool: null,
  78. unknownTagUid: null,
  79. deviceOnline: true,
  80. deviceId: 'sb-test-001',
  81. remainingWeight: null,
  82. netWeight: null,
  83. },
  84. setAlert: vi.fn(),
  85. displayBrightness: 80,
  86. setDisplayBrightness: vi.fn(),
  87. };
  88. function OutletWrapper() {
  89. return <Outlet context={mockOutletContext} />;
  90. }
  91. function renderPage() {
  92. const qc = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } });
  93. return render(
  94. <QueryClientProvider client={qc}>
  95. <MemoryRouter initialEntries={['/spoolbuddy/settings']}>
  96. <Routes>
  97. <Route element={<OutletWrapper />}>
  98. <Route path="spoolbuddy/settings" element={<SpoolBuddySettingsPage />} />
  99. </Route>
  100. </Routes>
  101. </MemoryRouter>
  102. </QueryClientProvider>
  103. );
  104. }
  105. describe('SpoolBuddySettingsPage', () => {
  106. beforeEach(() => {
  107. vi.clearAllMocks();
  108. });
  109. it('renders 5 tabs', async () => {
  110. renderPage();
  111. await waitFor(() => {
  112. expect(screen.getByText('Device')).toBeDefined();
  113. expect(screen.getByText('Display')).toBeDefined();
  114. expect(screen.getByText('Scale')).toBeDefined();
  115. expect(screen.getByText('Updates')).toBeDefined();
  116. expect(screen.getByText('System')).toBeDefined();
  117. });
  118. });
  119. it('device tab shows hostname and IP', async () => {
  120. renderPage();
  121. await waitFor(() => {
  122. expect(screen.getByText('spoolbuddy-pi')).toBeDefined();
  123. expect(screen.getByText('192.168.1.100')).toBeDefined();
  124. });
  125. });
  126. it('device tab shows NFC reader type', async () => {
  127. renderPage();
  128. await waitFor(() => {
  129. expect(screen.getByText('PN532')).toBeDefined();
  130. });
  131. });
  132. it('device tab shows NFC status as Ready', async () => {
  133. renderPage();
  134. await waitFor(() => {
  135. expect(screen.getByText('Ready')).toBeDefined();
  136. });
  137. });
  138. it('switching to Updates tab shows Check for Updates and Force Update buttons', async () => {
  139. renderPage();
  140. await waitFor(() => {
  141. expect(screen.getByText('Updates')).toBeDefined();
  142. });
  143. fireEvent.click(screen.getByText('Updates'));
  144. await waitFor(() => {
  145. expect(screen.getByText('Check for Updates')).toBeDefined();
  146. expect(screen.getByText('Force Update')).toBeDefined();
  147. });
  148. });
  149. it('Updates tab shows current version', async () => {
  150. renderPage();
  151. await waitFor(() => {
  152. expect(screen.getByText('Updates')).toBeDefined();
  153. });
  154. fireEvent.click(screen.getByText('Updates'));
  155. await waitFor(() => {
  156. expect(screen.getByText('1.2.3')).toBeDefined();
  157. });
  158. });
  159. it('Updates tab shows SSH Setup section', async () => {
  160. renderPage();
  161. await waitFor(() => {
  162. expect(screen.getByText('Updates')).toBeDefined();
  163. });
  164. fireEvent.click(screen.getByText('Updates'));
  165. await waitFor(() => {
  166. expect(screen.getByText('SSH Setup')).toBeDefined();
  167. });
  168. });
  169. it('Updates tab shows Apply Update when update is available', async () => {
  170. const { spoolbuddyApi } = await import('../../api/client');
  171. vi.mocked(spoolbuddyApi.checkDaemonUpdate).mockResolvedValue({
  172. current_version: '1.2.3',
  173. latest_version: '1.3.0',
  174. update_available: true,
  175. });
  176. renderPage();
  177. await waitFor(() => {
  178. expect(screen.getByText('Updates')).toBeDefined();
  179. });
  180. fireEvent.click(screen.getByText('Updates'));
  181. await waitFor(() => {
  182. expect(screen.getByText('Apply Update')).toBeDefined();
  183. });
  184. });
  185. it('switching to Display tab shows Brightness', async () => {
  186. renderPage();
  187. await waitFor(() => {
  188. expect(screen.getByText('Display')).toBeDefined();
  189. });
  190. fireEvent.click(screen.getByText('Display'));
  191. await waitFor(() => {
  192. expect(screen.getByText('Brightness')).toBeDefined();
  193. });
  194. });
  195. it('switching to Scale tab shows Tare and Calibrate buttons', async () => {
  196. renderPage();
  197. await waitFor(() => {
  198. expect(screen.getByText('Scale')).toBeDefined();
  199. });
  200. fireEvent.click(screen.getByText('Scale'));
  201. await waitFor(() => {
  202. expect(screen.getByText('Tare')).toBeDefined();
  203. expect(screen.getByText('Calibrate')).toBeDefined();
  204. });
  205. });
  206. it('System tab shows CPU info', async () => {
  207. renderPage();
  208. await waitFor(() => {
  209. expect(screen.getByText('System')).toBeDefined();
  210. });
  211. fireEvent.click(screen.getByText('System'));
  212. await waitFor(() => {
  213. expect(screen.getByText('CPU')).toBeDefined();
  214. expect(screen.getByText('4')).toBeDefined(); // cpu_count
  215. expect(screen.getByText('0.15 / 0.22 / 0.18')).toBeDefined(); // load_avg
  216. });
  217. });
  218. it('System tab shows memory stats', async () => {
  219. renderPage();
  220. await waitFor(() => {
  221. expect(screen.getByText('System')).toBeDefined();
  222. });
  223. fireEvent.click(screen.getByText('System'));
  224. await waitFor(() => {
  225. expect(screen.getByText('Memory')).toBeDefined();
  226. expect(screen.getByText('512 / 1024 MB')).toBeDefined();
  227. });
  228. });
  229. it('System tab shows disk stats', async () => {
  230. renderPage();
  231. await waitFor(() => {
  232. expect(screen.getByText('System')).toBeDefined();
  233. });
  234. fireEvent.click(screen.getByText('System'));
  235. await waitFor(() => {
  236. expect(screen.getByText('Disk')).toBeDefined();
  237. expect(screen.getByText('8.2 / 29.7 GB')).toBeDefined();
  238. });
  239. });
  240. it('System tab shows OS info', async () => {
  241. renderPage();
  242. await waitFor(() => {
  243. expect(screen.getByText('System')).toBeDefined();
  244. });
  245. fireEvent.click(screen.getByText('System'));
  246. await waitFor(() => {
  247. expect(screen.getByText('Raspbian GNU/Linux 12')).toBeDefined();
  248. expect(screen.getByText('aarch64')).toBeDefined();
  249. expect(screen.getByText('3.11.2')).toBeDefined();
  250. });
  251. });
  252. it('System tab shows waiting message when no stats', async () => {
  253. const { spoolbuddyApi } = await import('../../api/client');
  254. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([{
  255. id: 1,
  256. device_id: 'sb-test-001',
  257. hostname: 'spoolbuddy-pi',
  258. ip_address: '192.168.1.100',
  259. firmware_version: '1.2.3',
  260. has_nfc: true,
  261. has_scale: true,
  262. tare_offset: 0,
  263. calibration_factor: 1.0,
  264. nfc_reader_type: 'PN532',
  265. nfc_connection: 'I2C',
  266. display_brightness: 80,
  267. display_blank_timeout: 300,
  268. has_backlight: true,
  269. last_calibrated_at: null,
  270. last_seen: '2026-03-22T12:00:00Z',
  271. pending_command: null,
  272. nfc_ok: true,
  273. scale_ok: true,
  274. uptime_s: 3600,
  275. update_status: null,
  276. update_message: null,
  277. system_stats: null,
  278. online: true,
  279. }]);
  280. renderPage();
  281. await waitFor(() => {
  282. expect(screen.getByText('System')).toBeDefined();
  283. });
  284. fireEvent.click(screen.getByText('System'));
  285. await waitFor(() => {
  286. expect(screen.getByText('Waiting for system stats...')).toBeDefined();
  287. });
  288. });
  289. });