FilamentOverride.test.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /**
  2. * Tests for the FilamentOverride component.
  3. *
  4. * FilamentOverride allows users to override the 3MF's original filament
  5. * choices with filaments available across printers of the selected model.
  6. */
  7. import { describe, it, expect, vi, afterEach } from 'vitest';
  8. import { screen, fireEvent, cleanup } from '@testing-library/react';
  9. import { render } from '../utils';
  10. import { FilamentOverride } from '../../components/PrintModal/FilamentOverride';
  11. import type { FilamentReqsData } from '../../components/PrintModal/types';
  12. const defaultFilamentReqs: FilamentReqsData = {
  13. filaments: [
  14. { slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5 },
  15. ],
  16. };
  17. const defaultAvailable = [
  18. { type: 'PLA', color: '#FF0000', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: null },
  19. { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Basic', extruder_id: null },
  20. { type: 'PETG', color: '#0000FF', tray_info_idx: 'GFG00', tray_sub_brands: 'PETG Basic', extruder_id: null },
  21. ];
  22. const mockOnChange = vi.fn();
  23. afterEach(() => {
  24. cleanup();
  25. vi.clearAllMocks();
  26. });
  27. describe('FilamentOverride', () => {
  28. describe('rendering', () => {
  29. it('returns null when filamentReqs is undefined', () => {
  30. render(
  31. <FilamentOverride
  32. filamentReqs={undefined}
  33. availableFilaments={defaultAvailable}
  34. overrides={{}}
  35. onChange={mockOnChange}
  36. />
  37. );
  38. expect(screen.queryByText('Filament Override')).not.toBeInTheDocument();
  39. });
  40. it('returns null when filaments array is empty', () => {
  41. render(
  42. <FilamentOverride
  43. filamentReqs={{ filaments: [] }}
  44. availableFilaments={defaultAvailable}
  45. overrides={{}}
  46. onChange={mockOnChange}
  47. />
  48. );
  49. expect(screen.queryByText('Filament Override')).not.toBeInTheDocument();
  50. });
  51. it('returns null when availableFilaments is empty', () => {
  52. render(
  53. <FilamentOverride
  54. filamentReqs={defaultFilamentReqs}
  55. availableFilaments={[]}
  56. overrides={{}}
  57. onChange={mockOnChange}
  58. />
  59. );
  60. expect(screen.queryByText('Filament Override')).not.toBeInTheDocument();
  61. });
  62. it('renders filament slot with type and grams', () => {
  63. render(
  64. <FilamentOverride
  65. filamentReqs={defaultFilamentReqs}
  66. availableFilaments={defaultAvailable}
  67. overrides={{}}
  68. onChange={mockOnChange}
  69. />
  70. );
  71. // The grams text "(25g)" is in a nested span within the type label
  72. expect(screen.getByText('(25g)')).toBeInTheDocument();
  73. // "Filament Override" heading confirms the section renders
  74. expect(screen.getByText('Filament Override')).toBeInTheDocument();
  75. });
  76. it('renders override dropdown for each slot', () => {
  77. const twoSlotReqs: FilamentReqsData = {
  78. filaments: [
  79. { slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5 },
  80. { slot_id: 2, type: 'PLA', color: '#00FF00', used_grams: 10, used_meters: 3.2 },
  81. ],
  82. };
  83. render(
  84. <FilamentOverride
  85. filamentReqs={twoSlotReqs}
  86. availableFilaments={defaultAvailable}
  87. overrides={{}}
  88. onChange={mockOnChange}
  89. />
  90. );
  91. const selects = screen.getAllByRole('combobox');
  92. expect(selects).toHaveLength(2);
  93. });
  94. });
  95. describe('type filtering', () => {
  96. it('only shows same-type filaments in dropdown', () => {
  97. render(
  98. <FilamentOverride
  99. filamentReqs={defaultFilamentReqs}
  100. availableFilaments={defaultAvailable}
  101. overrides={{}}
  102. onChange={mockOnChange}
  103. />
  104. );
  105. const select = screen.getByRole('combobox');
  106. const options = select.querySelectorAll('option');
  107. // 1 default "Original" option + 2 PLA options (not PETG)
  108. expect(options).toHaveLength(3);
  109. // Verify no PETG option values exist
  110. const optionValues = Array.from(options).map((o) => o.getAttribute('value'));
  111. expect(optionValues).not.toContain('PETG|#0000FF');
  112. });
  113. it('shows all same-type options regardless of color', () => {
  114. const threeColorAvailable = [
  115. { type: 'PLA', color: '#FF0000', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: null },
  116. { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Basic', extruder_id: null },
  117. { type: 'PLA', color: '#FFFFFF', tray_info_idx: 'GFA02', tray_sub_brands: 'PLA Basic', extruder_id: null },
  118. ];
  119. render(
  120. <FilamentOverride
  121. filamentReqs={defaultFilamentReqs}
  122. availableFilaments={threeColorAvailable}
  123. overrides={{}}
  124. onChange={mockOnChange}
  125. />
  126. );
  127. const select = screen.getByRole('combobox');
  128. const options = select.querySelectorAll('option');
  129. // 1 default "Original" option + 3 PLA color options
  130. expect(options).toHaveLength(4);
  131. });
  132. });
  133. describe('subtype display', () => {
  134. it('shows tray_sub_brands in dropdown options when available', () => {
  135. const subtypeAvailable = [
  136. { type: 'PLA', color: '#000000', tray_info_idx: 'GFL99', tray_sub_brands: 'PLA Basic', extruder_id: null },
  137. { type: 'PLA', color: '#000000', tray_info_idx: 'GFL05', tray_sub_brands: 'PLA Matte', extruder_id: null },
  138. ];
  139. render(
  140. <FilamentOverride
  141. filamentReqs={defaultFilamentReqs}
  142. availableFilaments={subtypeAvailable}
  143. overrides={{}}
  144. onChange={mockOnChange}
  145. />
  146. );
  147. const select = screen.getByRole('combobox');
  148. const options = Array.from(select.querySelectorAll('option'));
  149. const optionTexts = options.map((o) => o.textContent);
  150. // Should show "PLA Basic" and "PLA Matte", not just "PLA"
  151. expect(optionTexts.some((t) => t?.includes('PLA Basic'))).toBe(true);
  152. expect(optionTexts.some((t) => t?.includes('PLA Matte'))).toBe(true);
  153. });
  154. it('falls back to type when tray_sub_brands is empty', () => {
  155. const noSubtypeAvailable = [
  156. { type: 'PLA', color: '#FF0000', tray_info_idx: 'GFA00', tray_sub_brands: '', extruder_id: null },
  157. ];
  158. render(
  159. <FilamentOverride
  160. filamentReqs={defaultFilamentReqs}
  161. availableFilaments={noSubtypeAvailable}
  162. overrides={{}}
  163. onChange={mockOnChange}
  164. />
  165. );
  166. const select = screen.getByRole('combobox');
  167. const options = Array.from(select.querySelectorAll('option'));
  168. // Non-default option should show "PLA" as the type fallback
  169. const nonDefaultOptions = options.filter((o) => o.getAttribute('value') !== '');
  170. expect(nonDefaultOptions[0].textContent).toContain('PLA');
  171. });
  172. });
  173. describe('nozzle filtering', () => {
  174. it('filters by extruder_id when nozzle_id is set', () => {
  175. const nozzleReqs: FilamentReqsData = {
  176. filaments: [
  177. { slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5, nozzle_id: 0 },
  178. ],
  179. };
  180. const dualExtruderAvailable = [
  181. { type: 'PLA', color: '#FF0000', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: 0 },
  182. { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Basic', extruder_id: 1 },
  183. ];
  184. render(
  185. <FilamentOverride
  186. filamentReqs={nozzleReqs}
  187. availableFilaments={dualExtruderAvailable}
  188. overrides={{}}
  189. onChange={mockOnChange}
  190. />
  191. );
  192. const select = screen.getByRole('combobox');
  193. const options = select.querySelectorAll('option');
  194. // 1 default + 1 PLA with extruder_id=0 (extruder_id=1 is filtered out)
  195. expect(options).toHaveLength(2);
  196. const optionValues = Array.from(options).map((o) => o.getAttribute('value'));
  197. expect(optionValues).toContain('PLA|#FF0000');
  198. expect(optionValues).not.toContain('PLA|#00FF00');
  199. });
  200. it('shows all filaments when nozzle_id is undefined', () => {
  201. const noNozzleReqs: FilamentReqsData = {
  202. filaments: [
  203. { slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5 },
  204. ],
  205. };
  206. const mixedExtruderAvailable = [
  207. { type: 'PLA', color: '#FF0000', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: 0 },
  208. { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Basic', extruder_id: 1 },
  209. ];
  210. render(
  211. <FilamentOverride
  212. filamentReqs={noNozzleReqs}
  213. availableFilaments={mixedExtruderAvailable}
  214. overrides={{}}
  215. onChange={mockOnChange}
  216. />
  217. );
  218. const select = screen.getByRole('combobox');
  219. const options = select.querySelectorAll('option');
  220. // 1 default + 2 PLA options (no nozzle filtering)
  221. expect(options).toHaveLength(3);
  222. });
  223. it('includes filaments with null extruder_id', () => {
  224. const nozzleReqs: FilamentReqsData = {
  225. filaments: [
  226. { slot_id: 1, type: 'PLA', color: '#FF0000', used_grams: 25, used_meters: 8.5, nozzle_id: 0 },
  227. ],
  228. };
  229. const mixedAvailable = [
  230. { type: 'PLA', color: '#FF0000', tray_info_idx: 'GFA00', tray_sub_brands: 'PLA Basic', extruder_id: 0 },
  231. { type: 'PLA', color: '#00FF00', tray_info_idx: 'GFA01', tray_sub_brands: 'PLA Basic', extruder_id: null },
  232. { type: 'PLA', color: '#FFFFFF', tray_info_idx: 'GFA02', tray_sub_brands: 'PLA Basic', extruder_id: 1 },
  233. ];
  234. render(
  235. <FilamentOverride
  236. filamentReqs={nozzleReqs}
  237. availableFilaments={mixedAvailable}
  238. overrides={{}}
  239. onChange={mockOnChange}
  240. />
  241. );
  242. const select = screen.getByRole('combobox');
  243. const options = select.querySelectorAll('option');
  244. // 1 default + extruder_id=0 + extruder_id=null (extruder_id=1 filtered out)
  245. expect(options).toHaveLength(3);
  246. const optionValues = Array.from(options).map((o) => o.getAttribute('value'));
  247. expect(optionValues).toContain('PLA|#FF0000');
  248. expect(optionValues).toContain('PLA|#00FF00');
  249. expect(optionValues).not.toContain('PLA|#FFFFFF');
  250. });
  251. });
  252. describe('interactions', () => {
  253. it('calls onChange when selecting an override', () => {
  254. render(
  255. <FilamentOverride
  256. filamentReqs={defaultFilamentReqs}
  257. availableFilaments={defaultAvailable}
  258. overrides={{}}
  259. onChange={mockOnChange}
  260. />
  261. );
  262. const select = screen.getByRole('combobox');
  263. fireEvent.change(select, { target: { value: 'PLA|#00FF00' } });
  264. expect(mockOnChange).toHaveBeenCalledWith({
  265. 1: { type: 'PLA', color: '#00FF00' },
  266. });
  267. });
  268. it('calls onChange to remove override when selecting original', () => {
  269. const activeOverrides = {
  270. 1: { type: 'PLA', color: '#00FF00' },
  271. };
  272. render(
  273. <FilamentOverride
  274. filamentReqs={defaultFilamentReqs}
  275. availableFilaments={defaultAvailable}
  276. overrides={activeOverrides}
  277. onChange={mockOnChange}
  278. />
  279. );
  280. const select = screen.getByRole('combobox');
  281. fireEvent.change(select, { target: { value: '' } });
  282. expect(mockOnChange).toHaveBeenCalledWith({});
  283. });
  284. });
  285. });