PrintersPageAmsSwitchInlet.test.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. /**
  2. * The AMS card side badge on a Filament Track Switch machine.
  3. *
  4. * Without a switch, each AMS is wired to one nozzle and the card badges it L or
  5. * R. With a switch fitted, the AMS is bound to one of the switch's two *inlets*
  6. * instead and reaches BOTH nozzles through it — so every unit reports extruder
  7. * 0xE and `ams_extruder_map` comes back empty.
  8. *
  9. * The card used to fall through to the AMS unit id in that case, which quietly
  10. * labelled AMS 0 "R" and AMS 1 "L" from nothing but their unit numbers, gave a
  11. * third unit no badge at all, and was wrong for every one of them. It now shows
  12. * the inlet the printer's own "Manual AMS Setup" screen assigned — lettered L
  13. * for In-A and R for In-B, with the tooltip naming the inlet outright so the
  14. * letter is not mistaken for a claim about which nozzle the AMS feeds.
  15. */
  16. import { describe, it, expect, beforeEach } from 'vitest';
  17. import { screen, waitFor } from '@testing-library/react';
  18. import { render } from '../utils';
  19. import { PrintersPage } from '../../pages/PrintersPage';
  20. import { http, HttpResponse } from 'msw';
  21. import { server } from '../mocks/server';
  22. const mockPrinter = {
  23. id: 1,
  24. name: 'H2C',
  25. ip_address: '192.168.1.100',
  26. serial_number: '31B8BP610600650',
  27. access_code: '12345678',
  28. model: 'H2C',
  29. enabled: true,
  30. nozzle_count: 2,
  31. nozzle_diameter: 0.4,
  32. nozzle_type: 'hardened_steel',
  33. location: 'Workshop',
  34. auto_archive: true,
  35. created_at: '2024-01-01T00:00:00Z',
  36. updated_at: '2024-01-01T00:00:00Z',
  37. };
  38. const baseTray = {
  39. tray_color: 'FF0000FF',
  40. tray_type: 'PLA',
  41. tray_sub_brands: 'PLA Basic',
  42. tray_id_name: 'A00-R0',
  43. tray_info_idx: 'GFA00',
  44. remain: 80,
  45. k: 0.02,
  46. cali_idx: null,
  47. tag_uid: null,
  48. tray_uuid: null,
  49. nozzle_temp_min: 190,
  50. nozzle_temp_max: 230,
  51. drying_temp: 55,
  52. drying_time: 8,
  53. state: 3,
  54. };
  55. function amsUnit(id: number) {
  56. return {
  57. id,
  58. humidity: 30,
  59. temp: 33,
  60. is_ams_ht: false,
  61. serial_number: `AMS0${id}`,
  62. sw_ver: '03.00.21.29',
  63. module_type: 'n3f',
  64. tray: [0, 1, 2, 3].map((t) => ({ id: t, ...baseTray })),
  65. };
  66. }
  67. /** Three AMS units on a dual-nozzle printer, with the FTS fields under test. */
  68. function makeStatus(over: Record<string, unknown>) {
  69. return {
  70. connected: true,
  71. state: 'IDLE',
  72. progress: 0,
  73. layer_num: 0,
  74. total_layers: 0,
  75. // Two nozzle readings are what marks the card as dual-nozzle, which is the
  76. // precondition for any L/R badge appearing at all.
  77. temperatures: { nozzle: 25, nozzle_2: 25, bed: 25, chamber: 25 },
  78. remaining_time: 0,
  79. filename: null,
  80. wifi_signal: -29,
  81. speed_level: 2,
  82. vt_tray: [],
  83. ams: [amsUnit(0), amsUnit(1), amsUnit(2)],
  84. ams_extruder_map: {},
  85. fila_switch: null,
  86. ams_switch_inlet: {},
  87. ...over,
  88. };
  89. }
  90. function renderWith(over: Record<string, unknown>) {
  91. server.use(
  92. http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])),
  93. http.get('/api/v1/printers/:id/status', () => HttpResponse.json(makeStatus(over))),
  94. http.get('/api/v1/queue/', () => HttpResponse.json([])),
  95. );
  96. render(<PrintersPage />);
  97. }
  98. const FTS_INSTALLED = { installed: true, in_slots: [-1, -1], out_extruders: [1, 0], stat: 0, info: 0 };
  99. describe('PrintersPage — AMS card side badge with a Filament Track Switch', () => {
  100. beforeEach(() => {
  101. server.use(http.get('/api/v1/queue/', () => HttpResponse.json([])));
  102. });
  103. it('badges each AMS with the switch inlet it is plumbed into', async () => {
  104. renderWith({
  105. fila_switch: FTS_INSTALLED,
  106. ams_switch_inlet: { '0': 'A', '1': 'B', '2': 'B' },
  107. });
  108. await waitFor(() => {
  109. expect(screen.getAllByTitle(/Filament Track Switch IN-A/).length).toBe(1);
  110. });
  111. expect(screen.getAllByTitle(/Filament Track Switch IN-B/).length).toBe(2);
  112. });
  113. it('letters In-A as L and In-B as R, and names the inlet in the tooltip', async () => {
  114. renderWith({
  115. fila_switch: FTS_INSTALLED,
  116. ams_switch_inlet: { '0': 'A', '1': 'B', '2': 'B' },
  117. });
  118. // The letter is familiar; the tooltip carries what it actually means, since
  119. // an AMS behind the switch reaches both nozzles and "L" is the inlet's
  120. // position rather than the nozzle it feeds.
  121. const inA = await screen.findByTitle(/IN-A/);
  122. expect(inA.textContent).toBe('L');
  123. expect(inA.title).toMatch(/\(L\)/);
  124. expect(inA.title).toMatch(/both nozzles/i);
  125. for (const inB of screen.getAllByTitle(/IN-B/)) {
  126. expect(inB.textContent).toBe('R');
  127. expect(inB.title).toMatch(/\(R\)/);
  128. }
  129. });
  130. it('does not reuse the plain nozzle tooltip for an inlet badge', async () => {
  131. // The two badges share a letter but never a tooltip: a bare "Left" would be
  132. // the very claim the inlet badge exists to avoid making.
  133. renderWith({
  134. fila_switch: FTS_INSTALLED,
  135. ams_switch_inlet: { '0': 'A', '1': 'B', '2': 'B' },
  136. });
  137. await waitFor(() => {
  138. expect(screen.getAllByTitle(/IN-A/).length).toBeGreaterThan(0);
  139. });
  140. expect(screen.queryByTitle('Left')).toBeNull();
  141. expect(screen.queryByTitle('Right')).toBeNull();
  142. });
  143. it('shows nothing rather than guessing when the switch is not set up yet', async () => {
  144. // A switch fitted but not yet assigned on the printer screen reports no
  145. // binding. This is the case the old unit-id fallback got wrong.
  146. renderWith({ fila_switch: FTS_INSTALLED, ams_switch_inlet: {} });
  147. // Wait for the AMS cards themselves, so the absence assertions below cannot
  148. // pass trivially during the loading window.
  149. await waitFor(() => {
  150. expect(screen.getAllByText('AMS-C').length).toBeGreaterThan(0);
  151. });
  152. expect(screen.queryByTitle('Left')).toBeNull();
  153. expect(screen.queryByTitle('Right')).toBeNull();
  154. expect(screen.queryByTitle(/IN-[AB]/)).toBeNull();
  155. });
  156. it('still shows L/R on a dual-nozzle printer without a switch', async () => {
  157. // Regression guard: the inlet work must not take the ordinary H2D badge
  158. // down with it.
  159. renderWith({ fila_switch: null, ams_extruder_map: { '0': 1, '1': 0, '2': 0 } });
  160. await waitFor(() => {
  161. expect(screen.getAllByTitle('Left').length).toBe(1);
  162. });
  163. expect(screen.getAllByTitle('Right').length).toBe(2);
  164. expect(screen.queryByTitle(/IN-[AB]/)).toBeNull();
  165. });
  166. it('prefers a real extruder id over the unit-id guess even with a switch fitted', async () => {
  167. // An AMS reporting a genuine extruder id is bound to that nozzle directly,
  168. // switch or no switch — BambuStudio treats a non-0xE id as authoritative.
  169. renderWith({
  170. fila_switch: FTS_INSTALLED,
  171. ams_extruder_map: { '2': 1 },
  172. ams_switch_inlet: { '0': 'A', '1': 'B' },
  173. });
  174. await waitFor(() => {
  175. expect(screen.getAllByTitle(/IN-A/).length).toBe(1);
  176. });
  177. expect(screen.getAllByTitle('Left').length).toBe(1);
  178. expect(screen.queryByTitle('Right')).toBeNull();
  179. });
  180. });