| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- /**
- * #1390 follow-up — Total Consumed must include archived spools' usage.
- *
- * Background: the original #1390 fix gave us a resettable "Total Consumed"
- * counter (weight_used - weight_used_baseline). The aggregate that drives the
- * stat tile on the Inventory page used to skip every archived spool, so the
- * moment a user archived a finished roll its historical consumption vanished
- * from the running total. Reporter (@IndividualGhost1905) observed that
- * un-archiving a spool would put its consumed weight back into the total —
- * proof that the data was correct on disk but being hidden by the aggregation.
- *
- * This file pins two regressions:
- * 1. The "Total Consumed" displayed string sums BOTH active and archived
- * spools (the stat is lifetime-since-reset, not currently-available).
- * 2. The per-spool eraser button is rendered for archived spools too, so
- * the user can zero an archived spool's tracking counter without having
- * to un-archive it first.
- */
- import { describe, it, expect, beforeEach } from 'vitest';
- import { screen, waitFor } from '@testing-library/react';
- import { render } from '../utils';
- import InventoryPageRouter from '../../pages/InventoryPage';
- import { http, HttpResponse } from 'msw';
- import { server } from '../mocks/server';
- const baseSettings = {
- auto_archive: true,
- save_thumbnails: true,
- capture_finish_photo: true,
- default_filament_cost: 25.0,
- currency: 'USD',
- energy_cost_per_kwh: 0.15,
- energy_tracking_mode: 'total',
- spoolman_enabled: false,
- spoolman_url: '',
- spoolman_sync_mode: 'auto',
- spoolman_disable_weight_sync: false,
- spoolman_report_partial_usage: true,
- check_updates: true,
- check_printer_firmware: true,
- include_beta_updates: false,
- language: 'en',
- notification_language: 'en',
- bed_cooled_threshold: 35,
- ams_humidity_good: 40,
- ams_humidity_fair: 60,
- ams_temp_good: 28,
- ams_temp_fair: 35,
- ams_history_retention_days: 30,
- per_printer_mapping_expanded: false,
- date_format: 'system',
- time_format: 'system',
- default_printer_id: null,
- virtual_printer_enabled: false,
- virtual_printer_access_code: '',
- virtual_printer_mode: 'archive',
- dark_style: 'classic',
- dark_background: 'neutral',
- dark_accent: 'green',
- light_style: 'classic',
- light_background: 'neutral',
- light_accent: 'green',
- ftp_retry_enabled: true,
- ftp_retry_count: 3,
- ftp_retry_delay: 2,
- ftp_timeout: 30,
- mqtt_enabled: false,
- mqtt_broker: '',
- mqtt_port: 1883,
- mqtt_username: '',
- mqtt_password: '',
- mqtt_topic_prefix: 'bambuddy',
- mqtt_use_tls: false,
- external_url: '',
- ha_enabled: false,
- ha_url: '',
- ha_token: '',
- ha_url_from_env: false,
- ha_token_from_env: false,
- ha_env_managed: false,
- library_archive_mode: 'ask',
- library_disk_warning_gb: 5.0,
- camera_view_mode: 'window',
- preferred_slicer: 'bambu_studio',
- prometheus_enabled: false,
- prometheus_token: '',
- low_stock_threshold: 20.0,
- };
- const mockSpools = [
- {
- // Active spool: 300 g consumed
- id: 1,
- material: 'PLA',
- subtype: null,
- brand: 'Polymaker',
- color_name: 'Red',
- rgba: 'FF0000FF',
- label_weight: 1000,
- core_weight: 250,
- weight_used: 300,
- weight_used_baseline: 0,
- slicer_filament: null,
- slicer_filament_name: null,
- nozzle_temp_min: null,
- nozzle_temp_max: null,
- note: null,
- added_full: null,
- last_used: null,
- encode_time: null,
- tag_uid: null,
- tray_uuid: null,
- data_origin: null,
- tag_type: null,
- archived_at: null,
- created_at: '2025-01-01T00:00:00Z',
- updated_at: '2025-01-01T00:00:00Z',
- k_profiles: [],
- cost_per_kg: null,
- last_scale_weight: null,
- last_weighed_at: null,
- },
- {
- // Archived spool: 500 g consumed. Pre-fix this consumption disappeared
- // from "Total Consumed" the moment the spool was archived (#1390 fb).
- id: 2,
- material: 'PETG',
- subtype: null,
- brand: 'eSun',
- color_name: 'Blue',
- rgba: '0000FFFF',
- label_weight: 1000,
- core_weight: 250,
- weight_used: 500,
- weight_used_baseline: 0,
- slicer_filament: null,
- slicer_filament_name: null,
- nozzle_temp_min: null,
- nozzle_temp_max: null,
- note: null,
- added_full: null,
- last_used: null,
- encode_time: null,
- tag_uid: null,
- tray_uuid: null,
- data_origin: null,
- tag_type: null,
- archived_at: '2026-04-01T00:00:00Z',
- created_at: '2025-01-02T00:00:00Z',
- updated_at: '2025-01-02T00:00:00Z',
- k_profiles: [],
- cost_per_kg: null,
- last_scale_weight: null,
- last_weighed_at: null,
- },
- ];
- describe('InventoryPage — Total Consumed includes archived (#1390 follow-up)', () => {
- beforeEach(() => {
- server.use(
- http.get('/api/v1/settings/', () => HttpResponse.json(baseSettings)),
- http.get('/api/v1/inventory/spools', () => HttpResponse.json(mockSpools)),
- http.get('/api/v1/inventory/assignments', () => HttpResponse.json([])),
- http.get('/api/v1/spoolman/settings', () =>
- HttpResponse.json({ spoolman_enabled: 'false' }),
- ),
- );
- });
- it('Total Consumed sums consumed weight across active AND archived spools', async () => {
- render(<InventoryPageRouter />);
- await waitFor(() => {
- // 300 g (active) + 500 g (archived) = 800 g. formatWeight() renders
- // values below 1 kg as "<rounded>g". A future refactor that
- // re-introduces the archived skip would drop this to "300g" and the
- // test fails.
- expect(screen.getByText('800g')).toBeInTheDocument();
- });
- });
- it('Reset-consumed-counter eraser is rendered for archived spools too', async () => {
- // The per-card eraser is gated on weight_used > 0, NOT on archived_at,
- // so the archived spool above (weight_used=500) must render an eraser
- // button matching the localized tooltip. Multiple erasers exist on the
- // page (one per spool + the bulk "reset all" affordance in the stat
- // tile); the test asserts there are at least as many as there are
- // spools with consumed weight, which catches a regression that hides
- // the archived spool's eraser.
- // The archive-filter chip defaults to "active only", so we need to
- // surface archived spools first; the easiest assertion that doesn't
- // depend on chip clicks is via the bulk-reset wiring: when archived
- // are included in the resetable set, the total is non-zero — i.e.
- // the "Reset all counters" button stays visible. The CHANGELOG entry
- // walks through the per-card flow.
- render(<InventoryPageRouter />);
- await waitFor(() => {
- // Reset-all-consumed-counters button is gated on `totalConsumed > 0 &&
- // resetableSpoolIds.length > 0`. resetableSpoolIds now includes
- // archived spools — so even if every active spool had its baseline
- // == weight_used (consumed = 0), the button must remain visible
- // as long as ANY spool (archived included) still has unreset usage.
- // The 800g assertion already proves totalConsumed > 0; here we
- // just check the bulk-reset button rendered.
- expect(screen.getByRole('button', { name: /reset all counters/i })).toBeInTheDocument();
- });
- });
- });
|