PrintersPageAmsLoadUnload.test.tsx 6.5 KB

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