/** * Tests for the AMS slot load / unload buttons on PrintersPage (#891). * * Verifies that the menu in each AMS slot popover exposes Load and Unload, * that clicking them POSTs to the right endpoint with the right tray_id, and * that the buttons are hidden while the printer is RUNNING. */ import { describe, it, expect, beforeEach } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../utils'; import { PrintersPage } from '../../pages/PrintersPage'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; const mockPrinter = { id: 1, name: 'X1 Carbon', ip_address: '192.168.1.100', serial_number: '00M09A350100001', access_code: '12345678', model: 'X1C', enabled: true, nozzle_diameter: 0.4, nozzle_type: 'hardened_steel', location: 'Workshop', auto_archive: true, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }; const baseTray = { tray_color: 'FF0000FF', tray_type: 'PLA', tray_sub_brands: 'PLA Basic', tray_id_name: 'A00-R0', tray_info_idx: 'GFA00', remain: 80, k: 0.02, cali_idx: null, tag_uid: null, tray_uuid: null, nozzle_temp_min: 190, nozzle_temp_max: 230, drying_temp: null, drying_time: null, state: 11, }; const mockIdleStatusWithAms = { connected: true, state: 'IDLE', progress: 0, layer_num: 0, total_layers: 0, temperatures: { nozzle: 25, bed: 25, chamber: 25 }, remaining_time: 0, filename: null, wifi_signal: -50, speed_level: 2, vt_tray: [], ams: [ { id: 0, humidity: 30, temp: 25, is_ams_ht: false, serial_number: 'AMS00', sw_ver: '1.0.0', dry_time: 0, dry_status: 0, dry_sub_status: 0, dry_sf_reason: [], module_type: 'ams', tray: [ { id: 0, ...baseTray }, { id: 1, ...baseTray, tray_color: '00FF00FF', tray_type: 'PETG' }, { id: 2, ...baseTray, tray_color: '0000FFFF', tray_type: 'ABS' }, { id: 3, ...baseTray, tray_color: 'FFFF00FF', tray_type: 'TPU' }, ], }, ], }; const mockRunningStatus = { ...mockIdleStatusWithAms, state: 'RUNNING', }; describe('PrintersPage - AMS load/unload (#891)', () => { beforeEach(() => { server.use( http.get('/api/v1/printers/', () => HttpResponse.json([mockPrinter])), http.get('/api/v1/queue/', () => HttpResponse.json([])), ); }); it('Load posts to /ams/load with tray_id derived from amsId*4 + slot', async () => { const user = userEvent.setup(); let captured: { tray_id: string | null } | null = null; server.use( http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockIdleStatusWithAms)), http.post('/api/v1/printers/:id/ams/load', ({ request }) => { const url = new URL(request.url); captured = { tray_id: url.searchParams.get('tray_id') }; return HttpResponse.json({ success: true, message: 'Loading filament from AMS 0 slot 3' }); }), ); render(); await waitFor(() => { // The slot menu button is hidden until we hover. Pull it directly out of the DOM. expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0); }); // Slot 2 (third one, slotIdx=2) → expected tray_id = 0*4 + 2 = 2 const menuButtons = document.querySelectorAll('[title="Slot options"]'); await user.click(menuButtons[2]); await waitFor(() => { expect(screen.getByText('Load')).toBeInTheDocument(); }); await user.click(screen.getByText('Load')); await waitFor(() => { expect(captured).not.toBeNull(); expect(captured!.tray_id).toBe('2'); }); }); it('Unload posts to /ams/unload (no body, no params)', async () => { const user = userEvent.setup(); let unloadCalled = false; server.use( http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockIdleStatusWithAms)), http.post('/api/v1/printers/:id/ams/unload', () => { unloadCalled = true; return HttpResponse.json({ success: true, message: 'Unloading filament' }); }), ); render(); await waitFor(() => { expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0); }); const menuButtons = document.querySelectorAll('[title="Slot options"]'); await user.click(menuButtons[0]); await waitFor(() => { expect(screen.getByText('Unload')).toBeInTheDocument(); }); await user.click(screen.getByText('Unload')); await waitFor(() => { expect(unloadCalled).toBe(true); }); }); it('hides the slot menu while the printer is RUNNING', async () => { server.use( http.get('/api/v1/printers/:id/status', () => HttpResponse.json(mockRunningStatus)), ); render(); // Wait for the page to render the printer card. await waitFor(() => { expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); }); // No "Slot options" menu trigger should be present at all while running. expect(document.querySelectorAll('[title="Slot options"]').length).toBe(0); }); it('external spool slot exposes Load and posts tray_id=254', async () => { const user = userEvent.setup(); let captured: string | null = null; server.use( http.get('/api/v1/printers/:id/status', () => HttpResponse.json({ ...mockIdleStatusWithAms, ams: [], // external-only vt_tray: [{ id: 254, ...baseTray, tray_type: 'PLA', tray_color: 'FFFFFFFF' }], }), ), http.post('/api/v1/printers/:id/ams/load', ({ request }) => { captured = new URL(request.url).searchParams.get('tray_id'); return HttpResponse.json({ success: true, message: 'Loading filament from external spool' }); }), ); render(); await waitFor(() => { expect(document.querySelectorAll('[title="Slot options"]').length).toBeGreaterThan(0); }); const menuButtons = document.querySelectorAll('[title="Slot options"]'); await user.click(menuButtons[0]); await waitFor(() => { expect(screen.getByText('Load')).toBeInTheDocument(); }); await user.click(screen.getByText('Load')); await waitFor(() => { expect(captured).toBe('254'); }); }); });