InventoryPageCopyButton.test.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. /**
  2. * Tests for the copy-spool button in InventoryPage.
  3. *
  4. * Three callsites — table-row, card, and grouped-view inner row — each wire
  5. * onCopy from the page-level setFormModal({ spool, mode: 'copy' }) state.
  6. * These tests cover the two visually distinct components (SpoolTableRow and
  7. * SpoolCard). The grouped-view path is SpoolTableGroup which renders inner
  8. * SpoolTableRow rows with onCopy={onCopy ? () => onCopy(spool) : undefined} —
  9. * a one-line forward of the same callback the table-row test already
  10. * exercises end-to-end.
  11. */
  12. import { describe, it, expect, beforeEach } from 'vitest';
  13. import { screen, waitFor, fireEvent } from '@testing-library/react';
  14. import { render } from '../utils';
  15. import InventoryPageRouter from '../../pages/InventoryPage';
  16. import { http, HttpResponse } from 'msw';
  17. import { server } from '../mocks/server';
  18. const baseSpool = {
  19. subtype: null,
  20. brand: 'eSun',
  21. color_name: 'Blue',
  22. rgba: '0000FFFF',
  23. extra_colors: null,
  24. effect_type: null,
  25. label_weight: 1000,
  26. core_weight: 250,
  27. core_weight_catalog_id: null,
  28. slicer_filament: null,
  29. slicer_filament_name: null,
  30. nozzle_temp_min: null,
  31. nozzle_temp_max: null,
  32. note: null,
  33. added_full: null,
  34. last_used: null,
  35. encode_time: null,
  36. tag_uid: null,
  37. tray_uuid: null,
  38. data_origin: null,
  39. tag_type: null,
  40. archived_at: null,
  41. created_at: '2025-01-01T00:00:00Z',
  42. updated_at: '2025-01-01T00:00:00Z',
  43. k_profiles: [] as never[],
  44. cost_per_kg: null,
  45. last_scale_weight: null,
  46. last_weighed_at: null,
  47. storage_location: null,
  48. category: null,
  49. low_stock_threshold_pct: null,
  50. spoolman_id: null,
  51. spoolman_filament_id: null,
  52. };
  53. const MOCK_SPOOL = {
  54. ...baseSpool,
  55. id: 5,
  56. material: 'PETG',
  57. weight_used: 400,
  58. };
  59. const MOCK_SETTINGS = {
  60. auto_archive: false,
  61. save_thumbnails: false,
  62. capture_finish_photo: false,
  63. default_filament_cost: 25.0,
  64. currency: 'USD',
  65. energy_cost_per_kwh: 0.15,
  66. energy_tracking_mode: 'total',
  67. spoolman_enabled: false,
  68. spoolman_url: '',
  69. spoolman_sync_mode: 'auto',
  70. spoolman_disable_weight_sync: false,
  71. spoolman_report_partial_usage: true,
  72. check_updates: false,
  73. check_printer_firmware: false,
  74. include_beta_updates: false,
  75. language: 'en',
  76. notification_language: 'en',
  77. bed_cooled_threshold: 35,
  78. ams_humidity_good: 40,
  79. ams_humidity_fair: 60,
  80. ams_temp_good: 28,
  81. ams_temp_fair: 35,
  82. ams_history_retention_days: 30,
  83. per_printer_mapping_expanded: false,
  84. date_format: 'system',
  85. time_format: 'system',
  86. default_printer_id: null,
  87. virtual_printer_enabled: false,
  88. virtual_printer_access_code: '',
  89. virtual_printer_mode: 'immediate',
  90. dark_style: 'classic',
  91. dark_background: 'neutral',
  92. dark_accent: 'green',
  93. light_style: 'classic',
  94. light_background: 'neutral',
  95. light_accent: 'green',
  96. ftp_retry_enabled: true,
  97. ftp_retry_count: 3,
  98. ftp_retry_delay: 2,
  99. ftp_timeout: 30,
  100. mqtt_enabled: false,
  101. mqtt_broker: '',
  102. mqtt_port: 1883,
  103. mqtt_username: '',
  104. mqtt_password: '',
  105. mqtt_topic_prefix: 'bambuddy',
  106. mqtt_use_tls: false,
  107. external_url: '',
  108. ha_enabled: false,
  109. ha_url: '',
  110. ha_token: '',
  111. ha_url_from_env: false,
  112. ha_token_from_env: false,
  113. ha_env_managed: false,
  114. library_archive_mode: 'ask',
  115. library_disk_warning_gb: 5.0,
  116. camera_view_mode: 'window',
  117. preferred_slicer: 'bambu_studio',
  118. prometheus_enabled: false,
  119. prometheus_token: '',
  120. low_stock_threshold: 20.0,
  121. };
  122. function setupHandlers(spools: unknown[] = [MOCK_SPOOL]) {
  123. server.use(
  124. http.get('/api/v1/settings/', () => HttpResponse.json(MOCK_SETTINGS)),
  125. http.get('/api/v1/settings/spoolman', () =>
  126. HttpResponse.json({
  127. spoolman_enabled: 'false',
  128. spoolman_url: '',
  129. spoolman_sync_mode: 'auto',
  130. spoolman_disable_weight_sync: 'false',
  131. spoolman_report_partial_usage: 'true',
  132. })
  133. ),
  134. http.get('/api/v1/inventory/spools', () => HttpResponse.json(spools)),
  135. http.get('/api/v1/inventory/assignments', () => HttpResponse.json([])),
  136. http.get('/api/v1/inventory/catalog', () => HttpResponse.json([])),
  137. // SpoolFormModal kicks off these fetches the moment it opens. Without
  138. // handlers MSW would passthrough to the real network and ECONNREFUSED;
  139. // those promises then resolve after the test environment is torn down,
  140. // surfacing as an unhandled rejection in the modal's setState finally.
  141. http.get('/api/v1/cloud/status', () =>
  142. HttpResponse.json({ is_authenticated: false })
  143. ),
  144. http.get('/api/v1/cloud/local-presets', () =>
  145. HttpResponse.json({ filament: [], printer: [], process: [] })
  146. ),
  147. http.get('/api/v1/local-presets/', () =>
  148. HttpResponse.json({ filament: [], printer: [], process: [] })
  149. ),
  150. http.get('/api/v1/cloud/builtin-filaments', () => HttpResponse.json([])),
  151. http.get('/api/v1/inventory/color-catalog', () => HttpResponse.json([])),
  152. http.get('/api/v1/inventory/colors', () => HttpResponse.json([])),
  153. http.get('/api/v1/inventory/spool-catalog', () => HttpResponse.json([])),
  154. http.get('/api/v1/printers/', () => HttpResponse.json([])),
  155. );
  156. }
  157. describe('InventoryPage — copy button', () => {
  158. beforeEach(() => {
  159. setupHandlers();
  160. });
  161. it('opens SpoolFormModal in "Copy Spool" mode when the copy button in the table row is clicked', async () => {
  162. render(<InventoryPageRouter />);
  163. // Wait for the spool list to render
  164. await waitFor(() => {
  165. expect(screen.getAllByText('PETG').length).toBeGreaterThan(0);
  166. });
  167. // Find the "Copy Spool" button (title attribute) in the table row
  168. const copyButtons = await screen.findAllByTitle('Copy Spool');
  169. expect(copyButtons.length).toBeGreaterThan(0);
  170. // Click the first copy button (table view is default)
  171. fireEvent.click(copyButtons[0]);
  172. // The modal should open with the "Copy Spool" heading
  173. await waitFor(() => {
  174. expect(screen.getByRole('heading', { name: 'Copy Spool' })).toBeInTheDocument();
  175. });
  176. });
  177. it('opens SpoolFormModal in "Copy Spool" mode when the copy button in the cards view is clicked', async () => {
  178. render(<InventoryPageRouter />);
  179. await waitFor(() => {
  180. expect(screen.getAllByText('PETG').length).toBeGreaterThan(0);
  181. });
  182. // Switch to cards view
  183. fireEvent.click(screen.getByRole('button', { name: /^Cards$/ }));
  184. // The card-view copy button has the same title; wait for the card render
  185. // to settle, then click it.
  186. const copyButtons = await screen.findAllByTitle('Copy Spool');
  187. expect(copyButtons.length).toBeGreaterThan(0);
  188. fireEvent.click(copyButtons[0]);
  189. await waitFor(() => {
  190. expect(screen.getByRole('heading', { name: 'Copy Spool' })).toBeInTheDocument();
  191. });
  192. });
  193. });