/** * Tests for the copy-spool button in InventoryPage. * * Three callsites — table-row, card, and grouped-view inner row — each wire * onCopy from the page-level setFormModal({ spool, mode: 'copy' }) state. * These tests cover the two visually distinct components (SpoolTableRow and * SpoolCard). The grouped-view path is SpoolTableGroup which renders inner * SpoolTableRow rows with onCopy={onCopy ? () => onCopy(spool) : undefined} — * a one-line forward of the same callback the table-row test already * exercises end-to-end. */ import { describe, it, expect, beforeEach } from 'vitest'; import { screen, waitFor, fireEvent } from '@testing-library/react'; import { render } from '../utils'; import InventoryPageRouter from '../../pages/InventoryPage'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; const baseSpool = { subtype: null, brand: 'eSun', color_name: 'Blue', rgba: '0000FFFF', extra_colors: null, effect_type: null, label_weight: 1000, core_weight: 250, core_weight_catalog_id: null, 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: [] as never[], cost_per_kg: null, last_scale_weight: null, last_weighed_at: null, storage_location: null, category: null, low_stock_threshold_pct: null, spoolman_id: null, spoolman_filament_id: null, }; const MOCK_SPOOL = { ...baseSpool, id: 5, material: 'PETG', weight_used: 400, }; const MOCK_SETTINGS = { auto_archive: false, save_thumbnails: false, capture_finish_photo: false, 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: false, check_printer_firmware: false, 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: 'immediate', 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, }; function setupHandlers(spools: unknown[] = [MOCK_SPOOL]) { server.use( http.get('/api/v1/settings/', () => HttpResponse.json(MOCK_SETTINGS)), http.get('/api/v1/settings/spoolman', () => HttpResponse.json({ spoolman_enabled: 'false', spoolman_url: '', spoolman_sync_mode: 'auto', spoolman_disable_weight_sync: 'false', spoolman_report_partial_usage: 'true', }) ), http.get('/api/v1/inventory/spools', () => HttpResponse.json(spools)), http.get('/api/v1/inventory/assignments', () => HttpResponse.json([])), http.get('/api/v1/inventory/catalog', () => HttpResponse.json([])), // SpoolFormModal kicks off these fetches the moment it opens. Without // handlers MSW would passthrough to the real network and ECONNREFUSED; // those promises then resolve after the test environment is torn down, // surfacing as an unhandled rejection in the modal's setState finally. http.get('/api/v1/cloud/status', () => HttpResponse.json({ is_authenticated: false }) ), http.get('/api/v1/cloud/local-presets', () => HttpResponse.json({ filament: [], printer: [], process: [] }) ), http.get('/api/v1/local-presets/', () => HttpResponse.json({ filament: [], printer: [], process: [] }) ), http.get('/api/v1/cloud/builtin-filaments', () => HttpResponse.json([])), http.get('/api/v1/inventory/color-catalog', () => HttpResponse.json([])), http.get('/api/v1/inventory/colors', () => HttpResponse.json([])), http.get('/api/v1/inventory/spool-catalog', () => HttpResponse.json([])), http.get('/api/v1/printers/', () => HttpResponse.json([])), ); } describe('InventoryPage — copy button', () => { beforeEach(() => { setupHandlers(); }); it('opens SpoolFormModal in "Copy Spool" mode when the copy button in the table row is clicked', async () => { render(); // Wait for the spool list to render await waitFor(() => { expect(screen.getAllByText('PETG').length).toBeGreaterThan(0); }); // Find the "Copy Spool" button (title attribute) in the table row const copyButtons = await screen.findAllByTitle('Copy Spool'); expect(copyButtons.length).toBeGreaterThan(0); // Click the first copy button (table view is default) fireEvent.click(copyButtons[0]); // The modal should open with the "Copy Spool" heading await waitFor(() => { expect(screen.getByRole('heading', { name: 'Copy Spool' })).toBeInTheDocument(); }); }); it('opens SpoolFormModal in "Copy Spool" mode when the copy button in the cards view is clicked', async () => { render(); await waitFor(() => { expect(screen.getAllByText('PETG').length).toBeGreaterThan(0); }); // Switch to cards view fireEvent.click(screen.getByRole('button', { name: /^Cards$/ })); // The card-view copy button has the same title; wait for the card render // to settle, then click it. const copyButtons = await screen.findAllByTitle('Copy Spool'); expect(copyButtons.length).toBeGreaterThan(0); fireEvent.click(copyButtons[0]); await waitFor(() => { expect(screen.getByRole('heading', { name: 'Copy Spool' })).toBeInTheDocument(); }); }); });