PrintersPageAmsLoadUnload.test.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * Tests for the AMS slot load / unload buttons on PrintersPage (#891).
  3. *
  4. * Verifies that the menu in each AMS slot popover exposes Load and Unload,
  5. * that clicking them POSTs to the right endpoint with the right tray_id, and
  6. * that the buttons are hidden while the printer is RUNNING.
  7. */
  8. import { describe, it, expect, beforeEach } from 'vitest';
  9. import { screen, waitFor } from '@testing-library/react';
  10. import userEvent from '@testing-library/user-event';
  11. import { render } from '../utils';
  12. import { PrintersPage } from '../../pages/PrintersPage';
  13. import { http, HttpResponse } from 'msw';
  14. import { server } from '../mocks/server';
  15. const mockPrinter = {
  16. id: 1,
  17. name: 'X1 Carbon',
  18. ip_address: '192.168.1.100',
  19. serial_number: '00M09A350100001',
  20. access_code: '12345678',
  21. model: 'X1C',
  22. enabled: true,
  23. nozzle_diameter: 0.4,
  24. nozzle_type: 'hardened_steel',
  25. location: 'Workshop',
  26. auto_archive: true,
  27. created_at: '2024-01-01T00:00:00Z',
  28. updated_at: '2024-01-01T00:00:00Z',
  29. };
  30. const baseTray = {
  31. tray_color: 'FF0000FF',
  32. tray_type: 'PLA',
  33. tray_sub_brands: 'PLA Basic',
  34. tray_id_name: 'A00-R0',
  35. tray_info_idx: 'GFA00',
  36. remain: 80,
  37. k: 0.02,
  38. cali_idx: null,
  39. tag_uid: null,
  40. tray_uuid: null,
  41. nozzle_temp_min: 190,
  42. nozzle_temp_max: 230,
  43. drying_temp: null,
  44. drying_time: null,
  45. state: 11,
  46. };
  47. const mockIdleStatusWithAms = {
  48. connected: true,
  49. state: 'IDLE',
  50. progress: 0,
  51. layer_num: 0,
  52. total_layers: 0,
  53. temperatures: { nozzle: 25, bed: 25, chamber: 25 },
  54. remaining_time: 0,
  55. filename: null,
  56. wifi_signal: -50,
  57. speed_level: 2,
  58. vt_tray: [],
  59. ams: [
  60. {
  61. id: 0,
  62. humidity: 30,
  63. temp: 25,
  64. is_ams_ht: false,
  65. serial_number: 'AMS00',
  66. sw_ver: '1.0.0',
  67. dry_time: 0,
  68. dry_status: 0,
  69. dry_sub_status: 0,
  70. dry_sf_reason: [],
  71. module_type: 'ams',
  72. tray: [
  73. { id: 0, ...baseTray },
  74. { id: 1, ...baseTray, tray_color: '00FF00FF', tray_type: 'PETG' },
  75. { id: 2, ...baseTray, tray_color: '0000FFFF', tray_type: 'ABS' },
  76. { id: 3, ...baseTray, tray_color: 'FFFF00FF', tray_type: 'TPU' },
  77. ],
  78. },
  79. ],
  80. };
  81. const mockRunningStatus = {
  82. ...mockIdleStatusWithAms,
  83. state: 'RUNNING',
  84. };
  85. describe('PrintersPage - AMS load/unload (#891)', () => {
  86. beforeEach(() => {
  87. server.use(
  88. http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])),
  89. http.get('/api/v1/queue/', () => HttpResponse.json([])),
  90. );
  91. });
  92. it('Load posts to /ams/load with tray_id derived from amsId*4 + slot', async () => {
  93. const user = userEvent.setup();
  94. let captured: { tray_id: string | null } | null = null;
  95. server.use(
  96. http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockIdleStatusWithAms)),
  97. http.post('/api/v1/printers/:id/ams/load', ({ request }) => {
  98. const url = new URL(request.url);
  99. captured = { tray_id: url.searchParams.get('tray_id') };
  100. return HttpResponse.json({ success: true, message: 'Loading filament from AMS 0 slot 3' });
  101. }),
  102. );
  103. render(<PrintersPage />);
  104. await waitFor(() => {
  105. // The slot menu button is hidden until we hover. Pull it directly out of the DOM.
  106. expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0);
  107. });
  108. // Slot 2 (third one, slotIdx=2) → expected tray_id = 0*4 + 2 = 2
  109. const menuButtons = document.querySelectorAll<HTMLButtonElement>('[title="Slot options"]');
  110. await user.click(menuButtons[2]);
  111. await waitFor(() => {
  112. expect(screen.getByText('Load')).toBeInTheDocument();
  113. });
  114. await user.click(screen.getByText('Load'));
  115. await waitFor(() => {
  116. expect(captured).not.toBeNull();
  117. expect(captured!.tray_id).toBe('2');
  118. });
  119. });
  120. it('Unload posts to /ams/unload (no body, no params)', async () => {
  121. const user = userEvent.setup();
  122. let unloadCalled = false;
  123. server.use(
  124. http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockIdleStatusWithAms)),
  125. http.post('/api/v1/printers/:id/ams/unload', () => {
  126. unloadCalled = true;
  127. return HttpResponse.json({ success: true, message: 'Unloading filament' });
  128. }),
  129. );
  130. render(<PrintersPage />);
  131. await waitFor(() => {
  132. expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0);
  133. });
  134. const menuButtons = document.querySelectorAll<HTMLButtonElement>('[title="Slot options"]');
  135. await user.click(menuButtons[0]);
  136. await waitFor(() => {
  137. expect(screen.getByText('Unload')).toBeInTheDocument();
  138. });
  139. await user.click(screen.getByText('Unload'));
  140. await waitFor(() => {
  141. expect(unloadCalled).toBe(true);
  142. });
  143. });
  144. it('hides the slot menu while the printer is RUNNING', async () => {
  145. server.use(
  146. http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockRunningStatus)),
  147. );
  148. render(<PrintersPage />);
  149. // Wait for the page to render the printer card.
  150. await waitFor(() => {
  151. expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
  152. });
  153. // No "Slot options" menu trigger should be present at all while running.
  154. expect(document.querySelectorAll('[title="Slot options"]').length).toBe(0);
  155. });
  156. it('external spool slot exposes Load and posts tray_id=254', async () => {
  157. const user = userEvent.setup();
  158. let captured: string | null = null;
  159. server.use(
  160. http.get('/api/v1/printers/:id/status', () =>
  161. HttpResponse.json({
  162. ...mockIdleStatusWithAms,
  163. ams: [], // external-only
  164. vt_tray: [{ id: 254, ...baseTray, tray_type: 'PLA', tray_color: 'FFFFFFFF' }],
  165. }),
  166. ),
  167. http.post('/api/v1/printers/:id/ams/load', ({ request }) => {
  168. captured = new URL(request.url).searchParams.get('tray_id');
  169. return HttpResponse.json({ success: true, message: 'Loading filament from external spool' });
  170. }),
  171. );
  172. render(<PrintersPage />);
  173. await waitFor(() => {
  174. expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0);
  175. });
  176. const menuButtons = document.querySelectorAll<HTMLButtonElement>('[title="Slot options"]');
  177. await user.click(menuButtons[0]);
  178. await waitFor(() => {
  179. expect(screen.getByText('Load')).toBeInTheDocument();
  180. });
  181. await user.click(screen.getByText('Load'));
  182. await waitFor(() => {
  183. expect(captured).toBe('254');
  184. });
  185. });
  186. });