PrintModal.test.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. /**
  2. * Tests for the unified PrintModal component.
  3. *
  4. * The PrintModal supports three modes:
  5. * - 'reprint': Immediate print from archive (multi-printer support)
  6. * - 'add-to-queue': Schedule print to queue (multi-printer support)
  7. * - 'edit-queue-item': Edit existing queue item (single printer)
  8. */
  9. import { describe, it, expect, vi, beforeEach } from 'vitest';
  10. import { screen, waitFor } from '@testing-library/react';
  11. import userEvent from '@testing-library/user-event';
  12. import { render } from '../utils';
  13. import { PrintModal } from '../../components/PrintModal';
  14. import { http, HttpResponse } from 'msw';
  15. import { server } from '../mocks/server';
  16. import type { PrintQueueItem } from '../../api/client';
  17. const mockPrinters = [
  18. { id: 1, name: 'X1 Carbon', model: 'X1C', ip_address: '192.168.1.100', enabled: true, is_active: true },
  19. { id: 2, name: 'P1S', model: 'P1S', ip_address: '192.168.1.101', enabled: true, is_active: true },
  20. ];
  21. const createMockQueueItem = (overrides: Partial<PrintQueueItem> = {}): PrintQueueItem => ({
  22. id: 1,
  23. printer_id: 1,
  24. archive_id: 1,
  25. position: 1,
  26. scheduled_time: null,
  27. require_previous_success: false,
  28. auto_off_after: false,
  29. manual_start: false,
  30. ams_mapping: null,
  31. plate_id: null,
  32. bed_levelling: true,
  33. flow_cali: false,
  34. vibration_cali: true,
  35. layer_inspect: false,
  36. timelapse: false,
  37. use_ams: true,
  38. status: 'pending',
  39. started_at: null,
  40. completed_at: null,
  41. error_message: null,
  42. created_at: '2024-01-01T00:00:00Z',
  43. archive_name: 'Test Print',
  44. archive_thumbnail: null,
  45. printer_name: 'Test Printer',
  46. print_time_seconds: 3600,
  47. ...overrides,
  48. });
  49. describe('PrintModal', () => {
  50. const mockOnClose = vi.fn();
  51. const mockOnSuccess = vi.fn();
  52. beforeEach(() => {
  53. vi.clearAllMocks();
  54. server.use(
  55. http.get('/api/v1/printers/', () => {
  56. return HttpResponse.json(mockPrinters);
  57. }),
  58. http.get('/api/v1/archives/:id/plates', () => {
  59. return HttpResponse.json({ is_multi_plate: false, plates: [] });
  60. }),
  61. http.get('/api/v1/archives/:id/filament-requirements', () => {
  62. return HttpResponse.json({ filaments: [] });
  63. }),
  64. http.get('/api/v1/printers/:id/status', () => {
  65. return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] });
  66. }),
  67. http.post('/api/v1/archives/:id/reprint', () => {
  68. return HttpResponse.json({ success: true });
  69. }),
  70. http.post('/api/v1/queue/', () => {
  71. return HttpResponse.json({ id: 1, status: 'pending' });
  72. }),
  73. http.patch('/api/v1/queue/:id', () => {
  74. return HttpResponse.json({ id: 1, status: 'pending' });
  75. })
  76. );
  77. });
  78. describe('reprint mode', () => {
  79. it('renders the modal title', () => {
  80. render(
  81. <PrintModal
  82. mode="reprint"
  83. archiveId={1}
  84. archiveName="Benchy"
  85. onClose={mockOnClose}
  86. onSuccess={mockOnSuccess}
  87. />
  88. );
  89. expect(screen.getByText('Re-print')).toBeInTheDocument();
  90. });
  91. it('shows archive name', () => {
  92. render(
  93. <PrintModal
  94. mode="reprint"
  95. archiveId={1}
  96. archiveName="Benchy"
  97. onClose={mockOnClose}
  98. onSuccess={mockOnSuccess}
  99. />
  100. );
  101. expect(screen.getByText('Benchy')).toBeInTheDocument();
  102. });
  103. it('shows printer selection with checkboxes for multi-select', async () => {
  104. render(
  105. <PrintModal
  106. mode="reprint"
  107. archiveId={1}
  108. archiveName="Benchy"
  109. onClose={mockOnClose}
  110. onSuccess={mockOnSuccess}
  111. />
  112. );
  113. await waitFor(() => {
  114. expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
  115. expect(screen.getByText('P1S')).toBeInTheDocument();
  116. });
  117. });
  118. it('has print button', () => {
  119. render(
  120. <PrintModal
  121. mode="reprint"
  122. archiveId={1}
  123. archiveName="Benchy"
  124. onClose={mockOnClose}
  125. onSuccess={mockOnSuccess}
  126. />
  127. );
  128. // Get the submit button specifically (not printer selection buttons)
  129. const submitButton = screen.getByRole('button', { name: /^print$/i });
  130. expect(submitButton).toBeInTheDocument();
  131. });
  132. it('has cancel button', () => {
  133. render(
  134. <PrintModal
  135. mode="reprint"
  136. archiveId={1}
  137. archiveName="Benchy"
  138. onClose={mockOnClose}
  139. onSuccess={mockOnSuccess}
  140. />
  141. );
  142. expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
  143. });
  144. it('calls onClose when cancel is clicked', async () => {
  145. const user = userEvent.setup();
  146. render(
  147. <PrintModal
  148. mode="reprint"
  149. archiveId={1}
  150. archiveName="Benchy"
  151. onClose={mockOnClose}
  152. onSuccess={mockOnSuccess}
  153. />
  154. );
  155. await user.click(screen.getByRole('button', { name: /cancel/i }));
  156. expect(mockOnClose).toHaveBeenCalled();
  157. });
  158. it('print button is disabled until printer is selected', () => {
  159. render(
  160. <PrintModal
  161. mode="reprint"
  162. archiveId={1}
  163. archiveName="Benchy"
  164. onClose={mockOnClose}
  165. onSuccess={mockOnSuccess}
  166. />
  167. );
  168. // Get the submit button specifically (not printer selection buttons)
  169. const printButton = screen.getByRole('button', { name: /^print$/i });
  170. expect(printButton).toBeDisabled();
  171. });
  172. it('shows no printers message when none active', async () => {
  173. server.use(
  174. http.get('/api/v1/printers/', () => {
  175. return HttpResponse.json([]);
  176. })
  177. );
  178. render(
  179. <PrintModal
  180. mode="reprint"
  181. archiveId={1}
  182. archiveName="Benchy"
  183. onClose={mockOnClose}
  184. onSuccess={mockOnSuccess}
  185. />
  186. );
  187. await waitFor(() => {
  188. expect(screen.getByText('No active printers available')).toBeInTheDocument();
  189. });
  190. });
  191. it('shows print options toggle', () => {
  192. render(
  193. <PrintModal
  194. mode="reprint"
  195. archiveId={1}
  196. archiveName="Benchy"
  197. onClose={mockOnClose}
  198. onSuccess={mockOnSuccess}
  199. />
  200. );
  201. expect(screen.getByText('Print Options')).toBeInTheDocument();
  202. });
  203. });
  204. describe('add-to-queue mode', () => {
  205. it('renders the modal title', () => {
  206. render(
  207. <PrintModal
  208. mode="add-to-queue"
  209. archiveId={1}
  210. archiveName="Test Print"
  211. onClose={mockOnClose}
  212. />
  213. );
  214. expect(screen.getByText('Schedule Print')).toBeInTheDocument();
  215. });
  216. it('shows archive name', () => {
  217. render(
  218. <PrintModal
  219. mode="add-to-queue"
  220. archiveId={1}
  221. archiveName="Test Print"
  222. onClose={mockOnClose}
  223. />
  224. );
  225. expect(screen.getByText('Test Print')).toBeInTheDocument();
  226. });
  227. it('shows add button', () => {
  228. render(
  229. <PrintModal
  230. mode="add-to-queue"
  231. archiveId={1}
  232. archiveName="Test Print"
  233. onClose={mockOnClose}
  234. />
  235. );
  236. expect(screen.getByRole('button', { name: /add to queue/i })).toBeInTheDocument();
  237. });
  238. it('shows cancel button', () => {
  239. render(
  240. <PrintModal
  241. mode="add-to-queue"
  242. archiveId={1}
  243. archiveName="Test Print"
  244. onClose={mockOnClose}
  245. />
  246. );
  247. expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
  248. });
  249. it('shows Queue Only option', () => {
  250. render(
  251. <PrintModal
  252. mode="add-to-queue"
  253. archiveId={1}
  254. archiveName="Test Print"
  255. onClose={mockOnClose}
  256. />
  257. );
  258. expect(screen.getByText('Queue Only')).toBeInTheDocument();
  259. });
  260. it('shows power off option', () => {
  261. render(
  262. <PrintModal
  263. mode="add-to-queue"
  264. archiveId={1}
  265. archiveName="Test Print"
  266. onClose={mockOnClose}
  267. />
  268. );
  269. expect(screen.getByText(/power off/i)).toBeInTheDocument();
  270. });
  271. it('shows schedule options', () => {
  272. render(
  273. <PrintModal
  274. mode="add-to-queue"
  275. archiveId={1}
  276. archiveName="Test Print"
  277. onClose={mockOnClose}
  278. />
  279. );
  280. expect(screen.getByText('ASAP')).toBeInTheDocument();
  281. expect(screen.getByText('Scheduled')).toBeInTheDocument();
  282. });
  283. it('calls onClose when cancel is clicked', async () => {
  284. const user = userEvent.setup();
  285. render(
  286. <PrintModal
  287. mode="add-to-queue"
  288. archiveId={1}
  289. archiveName="Test Print"
  290. onClose={mockOnClose}
  291. />
  292. );
  293. await user.click(screen.getByRole('button', { name: /cancel/i }));
  294. expect(mockOnClose).toHaveBeenCalled();
  295. });
  296. });
  297. describe('edit-queue-item mode', () => {
  298. it('renders the modal title', () => {
  299. const item = createMockQueueItem();
  300. render(
  301. <PrintModal
  302. mode="edit-queue-item"
  303. archiveId={1}
  304. archiveName="Test Print"
  305. queueItem={item}
  306. onClose={mockOnClose}
  307. />
  308. );
  309. expect(screen.getByText('Edit Queue Item')).toBeInTheDocument();
  310. });
  311. it('shows save button', () => {
  312. const item = createMockQueueItem();
  313. render(
  314. <PrintModal
  315. mode="edit-queue-item"
  316. archiveId={1}
  317. archiveName="Test Print"
  318. queueItem={item}
  319. onClose={mockOnClose}
  320. />
  321. );
  322. expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
  323. });
  324. it('shows cancel button', () => {
  325. const item = createMockQueueItem();
  326. render(
  327. <PrintModal
  328. mode="edit-queue-item"
  329. archiveId={1}
  330. archiveName="Test Print"
  331. queueItem={item}
  332. onClose={mockOnClose}
  333. />
  334. );
  335. expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
  336. });
  337. it('shows print options toggle', () => {
  338. const item = createMockQueueItem();
  339. render(
  340. <PrintModal
  341. mode="edit-queue-item"
  342. archiveId={1}
  343. archiveName="Test Print"
  344. queueItem={item}
  345. onClose={mockOnClose}
  346. />
  347. );
  348. expect(screen.getByText('Print Options')).toBeInTheDocument();
  349. });
  350. it('shows Queue Only option', () => {
  351. const item = createMockQueueItem();
  352. render(
  353. <PrintModal
  354. mode="edit-queue-item"
  355. archiveId={1}
  356. archiveName="Test Print"
  357. queueItem={item}
  358. onClose={mockOnClose}
  359. />
  360. );
  361. expect(screen.getByText('Queue Only')).toBeInTheDocument();
  362. });
  363. it('shows power off option', () => {
  364. const item = createMockQueueItem();
  365. render(
  366. <PrintModal
  367. mode="edit-queue-item"
  368. archiveId={1}
  369. archiveName="Test Print"
  370. queueItem={item}
  371. onClose={mockOnClose}
  372. />
  373. );
  374. expect(screen.getByText(/power off/i)).toBeInTheDocument();
  375. });
  376. it('calls onClose when cancel button is clicked', async () => {
  377. const user = userEvent.setup();
  378. const item = createMockQueueItem();
  379. render(
  380. <PrintModal
  381. mode="edit-queue-item"
  382. archiveId={1}
  383. archiveName="Test Print"
  384. queueItem={item}
  385. onClose={mockOnClose}
  386. />
  387. );
  388. const cancelButton = screen.getByRole('button', { name: /cancel/i });
  389. await user.click(cancelButton);
  390. expect(mockOnClose).toHaveBeenCalled();
  391. });
  392. it('shows printer selector for single selection', async () => {
  393. const item = createMockQueueItem();
  394. render(
  395. <PrintModal
  396. mode="edit-queue-item"
  397. archiveId={1}
  398. archiveName="Test Print"
  399. queueItem={item}
  400. onClose={mockOnClose}
  401. />
  402. );
  403. // PrinterSelector shows printer names directly
  404. await waitFor(() => {
  405. expect(screen.getByText('P1S')).toBeInTheDocument();
  406. });
  407. });
  408. });
  409. describe('multi-printer selection', () => {
  410. it('shows select all button when multiple printers available', async () => {
  411. render(
  412. <PrintModal
  413. mode="reprint"
  414. archiveId={1}
  415. archiveName="Benchy"
  416. onClose={mockOnClose}
  417. />
  418. );
  419. await waitFor(() => {
  420. expect(screen.getByText('Select all')).toBeInTheDocument();
  421. });
  422. });
  423. it('shows selected count when multiple printers selected', async () => {
  424. const user = userEvent.setup();
  425. render(
  426. <PrintModal
  427. mode="reprint"
  428. archiveId={1}
  429. archiveName="Benchy"
  430. onClose={mockOnClose}
  431. />
  432. );
  433. await waitFor(() => {
  434. expect(screen.getByText('Select all')).toBeInTheDocument();
  435. });
  436. await user.click(screen.getByText('Select all'));
  437. await waitFor(() => {
  438. expect(screen.getByText(/2 printers selected/)).toBeInTheDocument();
  439. });
  440. });
  441. it('updates button text when multiple printers selected', async () => {
  442. const user = userEvent.setup();
  443. render(
  444. <PrintModal
  445. mode="reprint"
  446. archiveId={1}
  447. archiveName="Benchy"
  448. onClose={mockOnClose}
  449. />
  450. );
  451. await waitFor(() => {
  452. expect(screen.getByText('Select all')).toBeInTheDocument();
  453. });
  454. await user.click(screen.getByText('Select all'));
  455. await waitFor(() => {
  456. expect(screen.getByRole('button', { name: /print to 2 printers/i })).toBeInTheDocument();
  457. });
  458. });
  459. });
  460. });