QueuePage.test.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /**
  2. * Tests for the QueuePage component.
  3. */
  4. import { describe, it, expect, beforeEach, vi } from 'vitest';
  5. import { screen, waitFor } from '@testing-library/react';
  6. import userEvent from '@testing-library/user-event';
  7. import { render } from '../utils';
  8. import { QueuePage } from '../../pages/QueuePage';
  9. import { http, HttpResponse } from 'msw';
  10. import { server } from '../mocks/server';
  11. // Mock queue data
  12. const mockQueueItems = [
  13. {
  14. id: 1,
  15. printer_id: 1,
  16. archive_id: 1,
  17. position: 1,
  18. status: 'pending',
  19. scheduled_time: null,
  20. require_previous_success: false,
  21. auto_off_after: false,
  22. manual_start: false,
  23. ams_mapping: null,
  24. plate_id: null,
  25. bed_levelling: true,
  26. flow_cali: false,
  27. vibration_cali: true,
  28. layer_inspect: false,
  29. timelapse: false,
  30. use_ams: true,
  31. started_at: null,
  32. completed_at: null,
  33. error_message: null,
  34. created_at: '2024-01-01T00:00:00Z',
  35. archive_name: 'Test Print 1',
  36. archive_thumbnail: '/thumb1.png',
  37. printer_name: 'Test Printer',
  38. print_time_seconds: 3600,
  39. },
  40. {
  41. id: 2,
  42. printer_id: 1,
  43. archive_id: 2,
  44. position: 2,
  45. status: 'printing',
  46. scheduled_time: null,
  47. require_previous_success: false,
  48. auto_off_after: true,
  49. manual_start: false,
  50. ams_mapping: null,
  51. plate_id: null,
  52. bed_levelling: true,
  53. flow_cali: false,
  54. vibration_cali: true,
  55. layer_inspect: false,
  56. timelapse: false,
  57. use_ams: true,
  58. started_at: '2024-01-01T10:00:00Z',
  59. completed_at: null,
  60. error_message: null,
  61. created_at: '2024-01-01T00:00:00Z',
  62. archive_name: 'Active Print',
  63. archive_thumbnail: '/thumb2.png',
  64. printer_name: 'Test Printer',
  65. print_time_seconds: 7200,
  66. },
  67. {
  68. id: 3,
  69. printer_id: 1,
  70. archive_id: 3,
  71. position: 3,
  72. status: 'completed',
  73. scheduled_time: null,
  74. require_previous_success: false,
  75. auto_off_after: false,
  76. manual_start: false,
  77. ams_mapping: null,
  78. plate_id: null,
  79. bed_levelling: true,
  80. flow_cali: false,
  81. vibration_cali: true,
  82. layer_inspect: false,
  83. timelapse: false,
  84. use_ams: true,
  85. started_at: '2024-01-01T08:00:00Z',
  86. completed_at: '2024-01-01T09:00:00Z',
  87. error_message: null,
  88. created_at: '2024-01-01T00:00:00Z',
  89. archive_name: 'Completed Print',
  90. archive_thumbnail: '/thumb3.png',
  91. printer_name: 'Test Printer',
  92. print_time_seconds: 1800,
  93. },
  94. ];
  95. const mockPrinters = [
  96. {
  97. id: 1,
  98. name: 'Test Printer',
  99. ip_address: '192.168.1.100',
  100. serial_number: 'TESTSERIAL0001',
  101. access_code: '12345678',
  102. model: 'X1C',
  103. enabled: true,
  104. created_at: '2024-01-01T00:00:00Z',
  105. },
  106. ];
  107. describe('QueuePage', () => {
  108. beforeEach(() => {
  109. // Mock localStorage.getItem to return expected defaults for queue page
  110. vi.mocked(localStorage.getItem).mockImplementation((key: string) => {
  111. if (key === 'queue.historyCollapsed') return 'false'; // expanded
  112. if (key === 'queue.viewMode') return 'list';
  113. return null;
  114. });
  115. // Setup MSW handlers for this test
  116. server.use(
  117. http.get('/api/v1/queue/', () => {
  118. return HttpResponse.json(mockQueueItems);
  119. }),
  120. http.get('/api/v1/printers/', () => {
  121. return HttpResponse.json(mockPrinters);
  122. }),
  123. http.delete('/api/v1/queue/:id', () => {
  124. return HttpResponse.json({ success: true });
  125. }),
  126. http.post('/api/v1/queue/:id/cancel', () => {
  127. return HttpResponse.json({ success: true });
  128. }),
  129. http.post('/api/v1/queue/:id/start', () => {
  130. return HttpResponse.json({ success: true });
  131. }),
  132. http.post('/api/v1/queue/:id/stop', () => {
  133. return HttpResponse.json({ success: true });
  134. }),
  135. http.post('/api/v1/queue/reorder', () => {
  136. return HttpResponse.json({ success: true });
  137. })
  138. );
  139. });
  140. describe('rendering', () => {
  141. it('renders the page title', async () => {
  142. render(<QueuePage />);
  143. await waitFor(() => {
  144. expect(screen.getByText('Print Queue')).toBeInTheDocument();
  145. });
  146. });
  147. it('renders the page description', async () => {
  148. render(<QueuePage />);
  149. await waitFor(() => {
  150. expect(screen.getByText('Schedule and manage your print jobs')).toBeInTheDocument();
  151. });
  152. });
  153. it('shows summary cards', async () => {
  154. render(<QueuePage />);
  155. await waitFor(() => {
  156. // Check for the page title (Print Queue is the h1)
  157. expect(screen.getByText('Print Queue')).toBeInTheDocument();
  158. });
  159. });
  160. it('shows filter dropdowns', async () => {
  161. render(<QueuePage />);
  162. await waitFor(() => {
  163. expect(screen.getByText('All Printers')).toBeInTheDocument();
  164. expect(screen.getByText('All Status')).toBeInTheDocument();
  165. });
  166. });
  167. });
  168. describe('queue items display', () => {
  169. it('shows pending queue items', async () => {
  170. render(<QueuePage />);
  171. await waitFor(() => {
  172. expect(screen.getByText('Test Print 1')).toBeInTheDocument();
  173. });
  174. });
  175. it('shows active printing items', async () => {
  176. render(<QueuePage />);
  177. await waitFor(() => {
  178. expect(screen.getByText('Active Print')).toBeInTheDocument();
  179. expect(screen.getByText('Currently Printing')).toBeInTheDocument();
  180. });
  181. });
  182. it('shows completed items in history', async () => {
  183. render(<QueuePage />);
  184. await waitFor(() => {
  185. expect(screen.getByText('Completed Print')).toBeInTheDocument();
  186. });
  187. });
  188. it('shows status badges', async () => {
  189. render(<QueuePage />);
  190. await waitFor(() => {
  191. // Queue items should be visible with status indicators
  192. expect(screen.getByText('Test Print 1')).toBeInTheDocument();
  193. });
  194. });
  195. it('shows printer names', async () => {
  196. render(<QueuePage />);
  197. await waitFor(() => {
  198. const printerElements = screen.getAllByText('Test Printer');
  199. expect(printerElements.length).toBeGreaterThan(0);
  200. });
  201. });
  202. it('renders queue items with plate_id correctly', async () => {
  203. // Override with queue items that have plate_id set
  204. server.use(
  205. http.get('/api/v1/queue/', () => {
  206. return HttpResponse.json([
  207. {
  208. ...mockQueueItems[0],
  209. plate_id: 2,
  210. archive_name: 'Multi-plate Print',
  211. },
  212. ]);
  213. })
  214. );
  215. render(<QueuePage />);
  216. await waitFor(() => {
  217. expect(screen.getByText('Multi-plate Print')).toBeInTheDocument();
  218. });
  219. });
  220. });
  221. describe('empty state', () => {
  222. it('shows empty state when no queue items', async () => {
  223. server.use(
  224. http.get('/api/v1/queue/', () => {
  225. return HttpResponse.json([]);
  226. })
  227. );
  228. render(<QueuePage />);
  229. await waitFor(() => {
  230. expect(screen.getByText('No prints scheduled')).toBeInTheDocument();
  231. });
  232. });
  233. });
  234. describe('filtering', () => {
  235. it('has printer filter options', async () => {
  236. const user = userEvent.setup();
  237. render(<QueuePage />);
  238. await waitFor(() => {
  239. expect(screen.getByText('All Printers')).toBeInTheDocument();
  240. });
  241. const printerSelect = screen.getByDisplayValue('All Printers');
  242. await user.click(printerSelect);
  243. expect(screen.getByText('Unassigned')).toBeInTheDocument();
  244. });
  245. it('has status filter options', async () => {
  246. const user = userEvent.setup();
  247. render(<QueuePage />);
  248. await waitFor(() => {
  249. expect(screen.getByText('All Status')).toBeInTheDocument();
  250. });
  251. const statusSelect = screen.getByDisplayValue('All Status');
  252. await user.click(statusSelect);
  253. expect(screen.getByRole('option', { name: 'Pending' })).toBeInTheDocument();
  254. expect(screen.getByRole('option', { name: 'Printing' })).toBeInTheDocument();
  255. expect(screen.getByRole('option', { name: 'Completed' })).toBeInTheDocument();
  256. });
  257. });
  258. describe('queue actions', () => {
  259. it('shows edit button for pending items', async () => {
  260. render(<QueuePage />);
  261. await waitFor(() => {
  262. expect(screen.getByText('Test Print 1')).toBeInTheDocument();
  263. });
  264. // Find the edit button (Pencil icon)
  265. const editButtons = screen.getAllByTitle('Edit');
  266. expect(editButtons.length).toBeGreaterThan(0);
  267. });
  268. it('shows cancel button for pending items', async () => {
  269. render(<QueuePage />);
  270. await waitFor(() => {
  271. expect(screen.getByText('Test Print 1')).toBeInTheDocument();
  272. });
  273. const cancelButtons = screen.getAllByTitle('Cancel');
  274. expect(cancelButtons.length).toBeGreaterThan(0);
  275. });
  276. it('shows stop button for printing items', async () => {
  277. render(<QueuePage />);
  278. await waitFor(() => {
  279. expect(screen.getByText('Active Print')).toBeInTheDocument();
  280. });
  281. const stopButtons = screen.getAllByTitle('Stop Print');
  282. expect(stopButtons.length).toBeGreaterThan(0);
  283. });
  284. it('shows re-queue button for history items', async () => {
  285. render(<QueuePage />);
  286. await waitFor(() => {
  287. expect(screen.getByText('Completed Print')).toBeInTheDocument();
  288. });
  289. const requeueButtons = screen.getAllByTitle('Re-queue');
  290. expect(requeueButtons.length).toBeGreaterThan(0);
  291. });
  292. });
  293. describe('clear history', () => {
  294. it('shows clear history button when history exists', async () => {
  295. render(<QueuePage />);
  296. await waitFor(() => {
  297. expect(screen.getByText('Clear History')).toBeInTheDocument();
  298. });
  299. });
  300. it('opens confirm modal when clicking clear history', async () => {
  301. const user = userEvent.setup();
  302. render(<QueuePage />);
  303. await waitFor(() => {
  304. expect(screen.getByText('Clear History')).toBeInTheDocument();
  305. });
  306. const clearButton = screen.getByRole('button', { name: /clear history/i });
  307. await user.click(clearButton);
  308. await waitFor(() => {
  309. expect(screen.getByText(/Are you sure you want to remove all/i)).toBeInTheDocument();
  310. });
  311. });
  312. });
  313. describe('staged items', () => {
  314. it('shows staged badge for manual_start items', async () => {
  315. server.use(
  316. http.get('/api/v1/queue/', () => {
  317. return HttpResponse.json([
  318. {
  319. ...mockQueueItems[0],
  320. manual_start: true,
  321. },
  322. ]);
  323. })
  324. );
  325. render(<QueuePage />);
  326. await waitFor(() => {
  327. expect(screen.getByText('Staged')).toBeInTheDocument();
  328. });
  329. });
  330. it('shows start button for staged items', async () => {
  331. server.use(
  332. http.get('/api/v1/queue/', () => {
  333. return HttpResponse.json([
  334. {
  335. ...mockQueueItems[0],
  336. manual_start: true,
  337. },
  338. ]);
  339. })
  340. );
  341. render(<QueuePage />);
  342. await waitFor(() => {
  343. expect(screen.getByTitle('Start Print')).toBeInTheDocument();
  344. });
  345. });
  346. });
  347. describe('auto power off badge', () => {
  348. it('shows power off badge when auto_off_after is true', async () => {
  349. render(<QueuePage />);
  350. await waitFor(() => {
  351. expect(screen.getByText('Auto power off')).toBeInTheDocument();
  352. });
  353. });
  354. });
  355. describe('gcode injection badge', () => {
  356. it('shows G-code badge when gcode_injection is true', async () => {
  357. const itemsWithGcode = mockQueueItems.map((item, i) =>
  358. i === 0 ? { ...item, gcode_injection: true } : item
  359. );
  360. server.use(
  361. http.get('/api/v1/queue/', () => HttpResponse.json(itemsWithGcode)),
  362. );
  363. render(<QueuePage />);
  364. await waitFor(() => {
  365. expect(screen.getByText('G-code')).toBeInTheDocument();
  366. });
  367. });
  368. it('does not show G-code badge when gcode_injection is false', async () => {
  369. render(<QueuePage />);
  370. await waitFor(() => {
  371. expect(screen.getByText('Test Print 1')).toBeInTheDocument();
  372. });
  373. expect(screen.queryByText('G-code')).not.toBeInTheDocument();
  374. });
  375. });
  376. });