AmsBackupModal.test.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /**
  2. * Render tests for the AMS Filament Backup modal (#1762).
  3. *
  4. * Modal now renders one SVG ring per backup pair (BambuStudio Auto Refill
  5. * style); lone slots are intentionally suppressed.
  6. */
  7. import { describe, it, expect, vi } from 'vitest';
  8. import { screen, fireEvent } from '@testing-library/react';
  9. import { render } from '../utils';
  10. import { AmsBackupModal } from '../../components/AmsBackupModal';
  11. function makeAmsUnits() {
  12. return [
  13. {
  14. id: 0,
  15. tray: [
  16. { id: 0, tray_type: 'PLA', tray_sub_brands: 'PLA Basic', tray_color: '#000000', tray_info_idx: 'GFA00' },
  17. { id: 1, tray_type: 'PETG', tray_sub_brands: 'PETG HF', tray_color: '#0000FF', tray_info_idx: 'GFG99' },
  18. ],
  19. },
  20. {
  21. id: 1,
  22. tray: [
  23. { id: 0, tray_type: 'PLA', tray_sub_brands: 'PLA Basic', tray_color: '#000000', tray_info_idx: 'GFA00' },
  24. ],
  25. },
  26. ];
  27. }
  28. describe('AmsBackupModal', () => {
  29. it('returns null when isOpen=false', () => {
  30. const { container } = render(
  31. <AmsBackupModal
  32. isOpen={false}
  33. state={true}
  34. amsUnits={makeAmsUnits()}
  35. amsExtruderMap={undefined}
  36. isDualNozzle={false}
  37. canToggle
  38. pending={false}
  39. onToggle={vi.fn()}
  40. onClose={vi.fn()}
  41. />,
  42. );
  43. expect(container.querySelector('[data-testid="ams-backup-modal"]')).toBeNull();
  44. });
  45. it('renders a backup ring for each pair and OMITS lone slots', async () => {
  46. render(
  47. <AmsBackupModal
  48. isOpen
  49. state={true}
  50. amsUnits={makeAmsUnits()}
  51. amsExtruderMap={undefined}
  52. isDualNozzle={false}
  53. canToggle
  54. pending={false}
  55. onToggle={vi.fn()}
  56. onClose={vi.fn()}
  57. />,
  58. );
  59. // PLA Basic — pair: rendered in centre of its ring
  60. expect(await screen.findByText('PLA Basic')).toBeInTheDocument();
  61. // PETG HF — lone, must NOT appear (no longer listed)
  62. expect(screen.queryByText('PETG HF')).not.toBeInTheDocument();
  63. });
  64. it('closes on Escape keypress while open', async () => {
  65. const onClose = vi.fn();
  66. render(
  67. <AmsBackupModal
  68. isOpen
  69. state={true}
  70. amsUnits={makeAmsUnits()}
  71. amsExtruderMap={undefined}
  72. isDualNozzle={false}
  73. canToggle
  74. pending={false}
  75. onToggle={vi.fn()}
  76. onClose={onClose}
  77. />,
  78. );
  79. fireEvent.keyDown(window, { key: 'Escape' });
  80. expect(onClose).toHaveBeenCalledTimes(1);
  81. });
  82. it('does NOT fire onClose on Escape when closed (listener unmounts)', () => {
  83. const onClose = vi.fn();
  84. const { rerender } = render(
  85. <AmsBackupModal
  86. isOpen
  87. state={true}
  88. amsUnits={makeAmsUnits()}
  89. amsExtruderMap={undefined}
  90. isDualNozzle={false}
  91. canToggle
  92. pending={false}
  93. onToggle={vi.fn()}
  94. onClose={onClose}
  95. />,
  96. );
  97. rerender(
  98. <AmsBackupModal
  99. isOpen={false}
  100. state={true}
  101. amsUnits={makeAmsUnits()}
  102. amsExtruderMap={undefined}
  103. isDualNozzle={false}
  104. canToggle
  105. pending={false}
  106. onToggle={vi.fn()}
  107. onClose={onClose}
  108. />,
  109. );
  110. fireEvent.keyDown(window, { key: 'Escape' });
  111. expect(onClose).not.toHaveBeenCalled();
  112. });
  113. it('toggle reflects the ON state and fires onToggle(false) when clicked', async () => {
  114. const onToggle = vi.fn();
  115. render(
  116. <AmsBackupModal
  117. isOpen
  118. state={true}
  119. amsUnits={makeAmsUnits()}
  120. amsExtruderMap={undefined}
  121. isDualNozzle={false}
  122. canToggle
  123. pending={false}
  124. onToggle={onToggle}
  125. onClose={vi.fn()}
  126. />,
  127. );
  128. const toggle = await screen.findByRole('switch');
  129. expect(toggle).toHaveAttribute('aria-checked', 'true');
  130. fireEvent.click(toggle);
  131. expect(onToggle).toHaveBeenCalledWith(false);
  132. });
  133. it('toggle is disabled when state is unknown (A1 family)', async () => {
  134. render(
  135. <AmsBackupModal
  136. isOpen
  137. state={null}
  138. amsUnits={[]}
  139. amsExtruderMap={undefined}
  140. isDualNozzle={false}
  141. canToggle
  142. pending={false}
  143. onToggle={vi.fn()}
  144. onClose={vi.fn()}
  145. />,
  146. );
  147. const toggle = await screen.findByRole('switch');
  148. expect(toggle).toBeDisabled();
  149. expect(screen.getByText(/Unsupported/i)).toBeInTheDocument();
  150. });
  151. it('toggle is disabled when the user lacks printers:control', async () => {
  152. render(
  153. <AmsBackupModal
  154. isOpen
  155. state={true}
  156. amsUnits={makeAmsUnits()}
  157. amsExtruderMap={undefined}
  158. isDualNozzle={false}
  159. canToggle={false}
  160. pending={false}
  161. onToggle={vi.fn()}
  162. onClose={vi.fn()}
  163. />,
  164. );
  165. const toggle = await screen.findByRole('switch');
  166. expect(toggle).toBeDisabled();
  167. });
  168. it('shows a no-pairs empty state when AMS has no backup pair', async () => {
  169. render(
  170. <AmsBackupModal
  171. isOpen
  172. state={true}
  173. // Just one slot, can't form any pair.
  174. amsUnits={[
  175. { id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_sub_brands: 'PLA Basic', tray_color: '#000', tray_info_idx: 'GFA00' }] },
  176. ]}
  177. amsExtruderMap={undefined}
  178. isDualNozzle={false}
  179. canToggle
  180. pending={false}
  181. onToggle={vi.fn()}
  182. onClose={vi.fn()}
  183. />,
  184. );
  185. expect(await screen.findByText(/No backup pairs/i)).toBeInTheDocument();
  186. });
  187. it('on dual-extruder with distinct map values, renders R / L badges on each ring', async () => {
  188. render(
  189. <AmsBackupModal
  190. isOpen
  191. state={true}
  192. amsUnits={[
  193. { id: 0, tray: [{ id: 0, tray_type: 'PLA', tray_sub_brands: 'PLA Basic', tray_color: '#000', tray_info_idx: 'GFA00' }] },
  194. { id: 1, tray: [{ id: 0, tray_type: 'PLA', tray_sub_brands: 'PLA Basic', tray_color: '#000', tray_info_idx: 'GFA00' }] },
  195. { id: 2, tray: [{ id: 0, tray_type: 'PETG', tray_sub_brands: 'PETG HF', tray_color: '#0FF', tray_info_idx: 'GFG99' }] },
  196. { id: 3, tray: [{ id: 0, tray_type: 'PETG', tray_sub_brands: 'PETG HF', tray_color: '#0FF', tray_info_idx: 'GFG99' }] },
  197. ]}
  198. // AMS 0+1 on right (ex 0), AMS 2+3 on left (ex 1) → two pairs, one per side.
  199. amsExtruderMap={{ '0': 0, '1': 0, '2': 1, '3': 1 }}
  200. isDualNozzle
  201. canToggle
  202. pending={false}
  203. onToggle={vi.fn()}
  204. onClose={vi.fn()}
  205. />,
  206. );
  207. // Both rings should be present (PLA Basic and PETG HF in the centres).
  208. expect(await screen.findByText('PLA Basic')).toBeInTheDocument();
  209. expect(screen.getByText('PETG HF')).toBeInTheDocument();
  210. // Both extruder badges visible.
  211. expect(screen.getByText('R')).toBeInTheDocument();
  212. expect(screen.getByText('L')).toBeInTheDocument();
  213. });
  214. it('collapses to single section (no R/L badges) when isDualNozzle=true but map has one distinct value', async () => {
  215. render(
  216. <AmsBackupModal
  217. isOpen
  218. state={true}
  219. amsUnits={makeAmsUnits()}
  220. amsExtruderMap={{ '0': 0, '1': 0 }}
  221. isDualNozzle
  222. canToggle
  223. pending={false}
  224. onToggle={vi.fn()}
  225. onClose={vi.fn()}
  226. />,
  227. );
  228. expect(screen.queryByText('R')).not.toBeInTheDocument();
  229. expect(screen.queryByText('L')).not.toBeInTheDocument();
  230. });
  231. });