InventoryPageArchivedConsumed.test.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. /**
  2. * #1390 follow-up — Total Consumed must include archived spools' usage.
  3. *
  4. * Background: the original #1390 fix gave us a resettable "Total Consumed"
  5. * counter (weight_used - weight_used_baseline). The aggregate that drives the
  6. * stat tile on the Inventory page used to skip every archived spool, so the
  7. * moment a user archived a finished roll its historical consumption vanished
  8. * from the running total. Reporter (@IndividualGhost1905) observed that
  9. * un-archiving a spool would put its consumed weight back into the total —
  10. * proof that the data was correct on disk but being hidden by the aggregation.
  11. *
  12. * This file pins two regressions:
  13. * 1. The "Total Consumed" displayed string sums BOTH active and archived
  14. * spools (the stat is lifetime-since-reset, not currently-available).
  15. * 2. The per-spool eraser button is rendered for archived spools too, so
  16. * the user can zero an archived spool's tracking counter without having
  17. * to un-archive it first.
  18. */
  19. import { describe, it, expect, beforeEach } from 'vitest';
  20. import { screen, waitFor } from '@testing-library/react';
  21. import { render } from '../utils';
  22. import InventoryPageRouter from '../../pages/InventoryPage';
  23. import { http, HttpResponse } from 'msw';
  24. import { server } from '../mocks/server';
  25. const baseSettings = {
  26. auto_archive: true,
  27. save_thumbnails: true,
  28. capture_finish_photo: true,
  29. default_filament_cost: 25.0,
  30. currency: 'USD',
  31. energy_cost_per_kwh: 0.15,
  32. energy_tracking_mode: 'total',
  33. spoolman_enabled: false,
  34. spoolman_url: '',
  35. spoolman_sync_mode: 'auto',
  36. spoolman_disable_weight_sync: false,
  37. spoolman_report_partial_usage: true,
  38. check_updates: true,
  39. check_printer_firmware: true,
  40. include_beta_updates: false,
  41. language: 'en',
  42. notification_language: 'en',
  43. bed_cooled_threshold: 35,
  44. ams_humidity_good: 40,
  45. ams_humidity_fair: 60,
  46. ams_temp_good: 28,
  47. ams_temp_fair: 35,
  48. ams_history_retention_days: 30,
  49. per_printer_mapping_expanded: false,
  50. date_format: 'system',
  51. time_format: 'system',
  52. default_printer_id: null,
  53. virtual_printer_enabled: false,
  54. virtual_printer_access_code: '',
  55. virtual_printer_mode: 'archive',
  56. dark_style: 'classic',
  57. dark_background: 'neutral',
  58. dark_accent: 'green',
  59. light_style: 'classic',
  60. light_background: 'neutral',
  61. light_accent: 'green',
  62. ftp_retry_enabled: true,
  63. ftp_retry_count: 3,
  64. ftp_retry_delay: 2,
  65. ftp_timeout: 30,
  66. mqtt_enabled: false,
  67. mqtt_broker: '',
  68. mqtt_port: 1883,
  69. mqtt_username: '',
  70. mqtt_password: '',
  71. mqtt_topic_prefix: 'bambuddy',
  72. mqtt_use_tls: false,
  73. external_url: '',
  74. ha_enabled: false,
  75. ha_url: '',
  76. ha_token: '',
  77. ha_url_from_env: false,
  78. ha_token_from_env: false,
  79. ha_env_managed: false,
  80. library_archive_mode: 'ask',
  81. library_disk_warning_gb: 5.0,
  82. camera_view_mode: 'window',
  83. preferred_slicer: 'bambu_studio',
  84. prometheus_enabled: false,
  85. prometheus_token: '',
  86. low_stock_threshold: 20.0,
  87. };
  88. const mockSpools = [
  89. {
  90. // Active spool: 300 g consumed
  91. id: 1,
  92. material: 'PLA',
  93. subtype: null,
  94. brand: 'Polymaker',
  95. color_name: 'Red',
  96. rgba: 'FF0000FF',
  97. label_weight: 1000,
  98. core_weight: 250,
  99. weight_used: 300,
  100. weight_used_baseline: 0,
  101. slicer_filament: null,
  102. slicer_filament_name: null,
  103. nozzle_temp_min: null,
  104. nozzle_temp_max: null,
  105. note: null,
  106. added_full: null,
  107. last_used: null,
  108. encode_time: null,
  109. tag_uid: null,
  110. tray_uuid: null,
  111. data_origin: null,
  112. tag_type: null,
  113. archived_at: null,
  114. created_at: '2025-01-01T00:00:00Z',
  115. updated_at: '2025-01-01T00:00:00Z',
  116. k_profiles: [],
  117. cost_per_kg: null,
  118. last_scale_weight: null,
  119. last_weighed_at: null,
  120. },
  121. {
  122. // Archived spool: 500 g consumed. Pre-fix this consumption disappeared
  123. // from "Total Consumed" the moment the spool was archived (#1390 fb).
  124. id: 2,
  125. material: 'PETG',
  126. subtype: null,
  127. brand: 'eSun',
  128. color_name: 'Blue',
  129. rgba: '0000FFFF',
  130. label_weight: 1000,
  131. core_weight: 250,
  132. weight_used: 500,
  133. weight_used_baseline: 0,
  134. slicer_filament: null,
  135. slicer_filament_name: null,
  136. nozzle_temp_min: null,
  137. nozzle_temp_max: null,
  138. note: null,
  139. added_full: null,
  140. last_used: null,
  141. encode_time: null,
  142. tag_uid: null,
  143. tray_uuid: null,
  144. data_origin: null,
  145. tag_type: null,
  146. archived_at: '2026-04-01T00:00:00Z',
  147. created_at: '2025-01-02T00:00:00Z',
  148. updated_at: '2025-01-02T00:00:00Z',
  149. k_profiles: [],
  150. cost_per_kg: null,
  151. last_scale_weight: null,
  152. last_weighed_at: null,
  153. },
  154. ];
  155. describe('InventoryPage — Total Consumed includes archived (#1390 follow-up)', () => {
  156. beforeEach(() => {
  157. server.use(
  158. http.get('/api/v1/settings/', () => HttpResponse.json(baseSettings)),
  159. http.get('/api/v1/inventory/spools', () => HttpResponse.json(mockSpools)),
  160. http.get('/api/v1/inventory/assignments', () => HttpResponse.json([])),
  161. http.get('/api/v1/spoolman/settings', () =>
  162. HttpResponse.json({ spoolman_enabled: 'false' }),
  163. ),
  164. );
  165. });
  166. it('Total Consumed sums consumed weight across active AND archived spools', async () => {
  167. render(<InventoryPageRouter />);
  168. await waitFor(() => {
  169. // 300 g (active) + 500 g (archived) = 800 g. formatWeight() renders
  170. // values below 1 kg as "<rounded>g". A future refactor that
  171. // re-introduces the archived skip would drop this to "300g" and the
  172. // test fails.
  173. expect(screen.getByText('800g')).toBeInTheDocument();
  174. });
  175. });
  176. it('Reset-consumed-counter eraser is rendered for archived spools too', async () => {
  177. // The per-card eraser is gated on weight_used > 0, NOT on archived_at,
  178. // so the archived spool above (weight_used=500) must render an eraser
  179. // button matching the localized tooltip. Multiple erasers exist on the
  180. // page (one per spool + the bulk "reset all" affordance in the stat
  181. // tile); the test asserts there are at least as many as there are
  182. // spools with consumed weight, which catches a regression that hides
  183. // the archived spool's eraser.
  184. // The archive-filter chip defaults to "active only", so we need to
  185. // surface archived spools first; the easiest assertion that doesn't
  186. // depend on chip clicks is via the bulk-reset wiring: when archived
  187. // are included in the resetable set, the total is non-zero — i.e.
  188. // the "Reset all counters" button stays visible. The CHANGELOG entry
  189. // walks through the per-card flow.
  190. render(<InventoryPageRouter />);
  191. await waitFor(() => {
  192. // Reset-all-consumed-counters button is gated on `totalConsumed > 0 &&
  193. // resetableSpoolIds.length > 0`. resetableSpoolIds now includes
  194. // archived spools — so even if every active spool had its baseline
  195. // == weight_used (consumed = 0), the button must remain visible
  196. // as long as ANY spool (archived included) still has unreset usage.
  197. // The 800g assertion already proves totalConsumed > 0; here we
  198. // just check the bulk-reset button rendered.
  199. expect(screen.getByRole('button', { name: /reset all counters/i })).toBeInTheDocument();
  200. });
  201. });
  202. });