| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- /**
- * Tests for LOCATION column in InventoryPage when in Spoolman mode.
- *
- * Regression test for Phase 8: LOCATION column showed "-" for Spoolman
- * spools assigned to AMS slots, because only the local
- * /inventory/assignments endpoint was queried — the Spoolman
- * /spoolman/inventory/slot-assignments/all endpoint was ignored.
- */
- import { describe, it, expect, beforeEach } from 'vitest';
- import { screen, waitFor, within } from '@testing-library/react';
- import { render } from '../utils';
- import InventoryPageRouter from '../../pages/InventoryPage';
- import { http, HttpResponse } from 'msw';
- import { server } from '../mocks/server';
- import { useQuery } from '@tanstack/react-query';
- import { api } from '../../api/client';
- // Full settings shape — pattern matches InventoryPageLowStock.test.tsx.
- const mockSettings = {
- 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,
- 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_disk_warning_gb: 5.0,
- camera_view_mode: 'window',
- preferred_slicer: 'bambu_studio',
- prometheus_enabled: false,
- prometheus_token: '',
- low_stock_threshold: 20.0,
- };
- const mockSpoolmanSpool = {
- id: 216,
- material: 'PLA',
- subtype: null,
- brand: 'Bambu Lab',
- color_name: 'Orange',
- rgba: 'FF8800FF',
- label_weight: 1000,
- core_weight: 250,
- weight_used: 200,
- 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: 'spoolman',
- tag_type: 'spoolman',
- 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,
- storage_location: 'IKEA Regal',
- };
- describe('InventoryPage - LOCATION column (Spoolman mode)', () => {
- beforeEach(() => {
- localStorage.clear();
- server.use(
- http.get('/api/v1/settings/', () => HttpResponse.json(mockSettings)),
- http.get('/api/v1/inventory/assignments', () => HttpResponse.json([])),
- http.get('/api/v1/inventory/catalog', () => HttpResponse.json([])),
- );
- });
- it('shows AMS slot in LOCATION column for spoolman spool with assignment', async () => {
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({
- spoolman_enabled: 'true',
- spoolman_url: 'http://localhost:7912',
- })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () =>
- HttpResponse.json([mockSpoolmanSpool])
- ),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () =>
- HttpResponse.json([
- {
- printer_id: 1,
- printer_name: 'Sully',
- ams_id: 0,
- tray_id: 2,
- spoolman_spool_id: 216,
- ams_label: null,
- },
- ])
- ),
- );
- const { container } = render(<InventoryPageRouter />);
- // LOCATION cell renders "{printerLabel} {slotLabel}" via JSX template,
- // which splits into separate text nodes. Use innerHTML / textContent inspection.
- // formatSlotLabel(0, 2, false, false) => "A3"
- await waitFor(() => {
- expect(container.textContent).toContain('Sully');
- expect(container.textContent).toContain('A3');
- });
- });
- it('labels an H3C slot 3H-n and a per-hotend external input Ext-n', async () => {
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({ spoolman_enabled: 'true', spoolman_url: 'http://localhost:7912' })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () =>
- HttpResponse.json([mockSpoolmanSpool, { ...mockSpoolmanSpool, id: 217, color_name: 'Blue', rgba: '0000FFFF' }])
- ),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () =>
- HttpResponse.json([
- // AMS 3H slot 7 is stored as (32, 6); input 250 (hotend 5) as (255, 5).
- { printer_id: 2, printer_name: 'Workshop', ams_id: 32, tray_id: 6, spoolman_spool_id: 216, ams_label: null, printer_model: 'H3C' },
- { printer_id: 2, printer_name: 'Workshop', ams_id: 255, tray_id: 5, spoolman_spool_id: 217, ams_label: null, printer_model: 'H3C' },
- ])
- ),
- );
- const { container } = render(<InventoryPageRouter />);
- await waitFor(() => {
- expect(container.textContent).toContain('Workshop 3H-7');
- expect(container.textContent).toContain('Workshop Ext-6');
- });
- });
- it('shows "-" in LOCATION column when spoolman spool has no slot assignment', async () => {
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({
- spoolman_enabled: 'true',
- spoolman_url: 'http://localhost:7912',
- })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () =>
- HttpResponse.json([mockSpoolmanSpool])
- ),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () =>
- HttpResponse.json([])
- ),
- );
- const { container } = render(<InventoryPageRouter />);
- await waitFor(() => {
- // Spool row is rendered — brand "Bambu Lab" appears in the table somewhere.
- expect(container.textContent).toContain('Bambu Lab');
- });
- // LOCATION cell shows "-" (there may be other "-" cells too — at least one expected).
- const dashCells = screen.getAllByText('-');
- expect(dashCells.length).toBeGreaterThan(0);
- });
- it('does not call /spoolman/inventory/slot-assignments/all in local mode', async () => {
- let slotEndpointCalled = false;
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({ spoolman_enabled: 'false', spoolman_url: '' })
- ),
- http.get('/api/v1/inventory/spools', () => HttpResponse.json([])),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => {
- slotEndpointCalled = true;
- return HttpResponse.json([]);
- }),
- );
- render(<InventoryPageRouter />);
- // Wait for the page to settle by checking a stable element from the stat cards.
- await waitFor(() => {
- expect(screen.getByText(/total inventory/i)).toBeInTheDocument();
- });
- expect(slotEndpointCalled).toBe(false);
- });
- it('built-in rows another page put in the shared cache stay off a Spoolman spool (#3430)', async () => {
- // The printer page fetches ['spool-assignments'] in either mode; a
- // disabled query still hands out what is cached under its key.
- function OtherPage() {
- useQuery({ queryKey: ['spool-assignments'], queryFn: () => api.getAssignments() });
- return null;
- }
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({ spoolman_enabled: 'true', spoolman_url: 'http://localhost:7912' })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () => HttpResponse.json([mockSpoolmanSpool])),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
- http.get('/api/v1/inventory/assignments', () =>
- HttpResponse.json([
- { id: 99, spool_id: 216, printer_id: 7, printer_name: 'LocalPrinter', ams_id: 0, tray_id: 0, ams_label: null, created_at: '2025-01-01T00:00:00Z' },
- ])
- ),
- );
- const { container } = render(
- <>
- <OtherPage />
- <InventoryPageRouter />
- </>
- );
- await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
- await new Promise((r) => setTimeout(r, 50));
- expect(container.textContent).not.toContain('LocalPrinter');
- });
- it('does not fetch built-in assignments in Spoolman mode', async () => {
- let builtInFetched = false;
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({ spoolman_enabled: 'true', spoolman_url: 'http://localhost:7912' })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () => HttpResponse.json([mockSpoolmanSpool])),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
- http.get('/api/v1/inventory/assignments', () => {
- builtInFetched = true;
- return HttpResponse.json([]);
- }),
- );
- const { container } = render(<InventoryPageRouter />);
- await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
- expect(builtInFetched).toBe(false);
- });
- it('counts spoolman slot assignments in the IN PRINTER stat card', async () => {
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({
- spoolman_enabled: 'true',
- spoolman_url: 'http://localhost:7912',
- })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () =>
- HttpResponse.json([mockSpoolmanSpool])
- ),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () =>
- HttpResponse.json([
- {
- printer_id: 1,
- printer_name: 'Sully',
- ams_id: 0,
- tray_id: 2,
- spoolman_spool_id: 216,
- ams_label: null,
- },
- ])
- ),
- );
- render(<InventoryPageRouter />);
- // Find the "IN PRINTER" stat card by its label, then assert the count "1"
- // appears within the same stat-card div. This verifies the inPrinterCount
- // sums Spoolman slot assignments (was 0 before Phase 8).
- await waitFor(() => {
- const label = screen.getByText(/^in printer$/i);
- const card = label.closest('div.bg-bambu-dark-secondary');
- expect(card).not.toBeNull();
- expect(within(card as HTMLElement).getByText('1')).toBeInTheDocument();
- });
- });
- it('a built-in assignment kept from built-in mode is not shown on a Spoolman spool (#3430)', async () => {
- // Since #2812 built-in rows survive a switch to Spoolman. Their spool_id is
- // a built-in id, so built-in spool 216 in "LocalPrinter" B1 must not label
- // Spoolman spool 216, which Spoolman has in "SpoolmanPrinter" C4.
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({
- spoolman_enabled: 'true',
- spoolman_url: 'http://localhost:7912',
- })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () =>
- HttpResponse.json([mockSpoolmanSpool])
- ),
- http.get('/api/v1/inventory/assignments', () =>
- HttpResponse.json([
- {
- id: 99,
- spool_id: 216,
- printer_id: 7,
- printer_name: 'LocalPrinter',
- ams_id: 1,
- tray_id: 0,
- ams_label: null,
- created_at: '2025-01-01T00:00:00Z',
- },
- ])
- ),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () =>
- HttpResponse.json([
- {
- printer_id: 8,
- printer_name: 'SpoolmanPrinter',
- ams_id: 2,
- tray_id: 3,
- spoolman_spool_id: 216,
- ams_label: null,
- },
- ])
- ),
- );
- const { container } = render(<InventoryPageRouter />);
- await waitFor(() => {
- expect(container.textContent).toContain('SpoolmanPrinter');
- expect(container.textContent).toContain('C4');
- });
- expect(container.textContent).not.toContain('LocalPrinter');
- });
- it('a spool Spoolman has in no slot shows no slot, whatever the built-in table holds (#3430)', async () => {
- // The reporter's case: the spool was taken out and Spoolman's row removed,
- // but a kept built-in row with the same number still named a slot.
- server.use(
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({
- spoolman_enabled: 'true',
- spoolman_url: 'http://localhost:7912',
- })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () =>
- HttpResponse.json([mockSpoolmanSpool])
- ),
- http.get('/api/v1/inventory/assignments', () =>
- HttpResponse.json([
- {
- id: 99,
- spool_id: 216,
- printer_id: 7,
- printer_name: 'LocalPrinter',
- ams_id: 0,
- tray_id: 0,
- ams_label: null,
- created_at: '2025-01-01T00:00:00Z',
- },
- ])
- ),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
- );
- const { container } = render(<InventoryPageRouter />);
- await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
- expect(container.textContent).not.toContain('LocalPrinter');
- // Nor counted as in a printer.
- const label = screen.getByText(/^in printer$/i);
- const card = label.closest('div.bg-bambu-dark-secondary');
- expect(within(card as HTMLElement).getByText('0')).toBeInTheDocument();
- });
- });
- describe('InventoryPage - Forecast in Spoolman mode (#3406)', () => {
- it("the forecast does not read the built-in inventory's usage history", async () => {
- localStorage.clear();
- // The Spoolman spools' own rows are read (#3407), never the built-in ones.
- let builtInUsageRead = false;
- let spoolmanUsageRead = false;
- server.use(
- http.get('/api/v1/settings/', () => HttpResponse.json(mockSettings)),
- http.get('/api/v1/inventory/assignments', () => HttpResponse.json([])),
- http.get('/api/v1/inventory/catalog', () => HttpResponse.json([])),
- http.get('/api/v1/settings/spoolman', () =>
- HttpResponse.json({ spoolman_enabled: 'true', spoolman_url: 'http://localhost:7912' })
- ),
- http.get('/api/v1/spoolman/inventory/spools', () => HttpResponse.json([mockSpoolmanSpool])),
- http.get('/api/v1/spoolman/inventory/slot-assignments/all', () => HttpResponse.json([])),
- http.get('/api/v1/inventory/usage', ({ request }) => {
- if (new URL(request.url).searchParams.get('spoolman') === 'true') spoolmanUsageRead = true;
- else builtInUsageRead = true;
- return HttpResponse.json([]);
- }),
- );
- const { container } = render(<InventoryPageRouter />);
- await waitFor(() => expect(container.textContent).toContain('Bambu Lab'));
- screen.getByRole('button', { name: /forecast/i }).click();
- // The forecast is up, with the rate from the spool's own consumption.
- expect(await screen.findByText('Est.')).toBeInTheDocument();
- await waitFor(() => expect(spoolmanUsageRead).toBe(true));
- expect(builtInUsageRead).toBe(false);
- });
- });
|