/** * Tests for the VirtualPrinterSettings component. * * Tests the virtual printer configuration UI including: * - Enable/disable toggle * - Access code management * - Archive mode selection * - Status display */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { render } from '../utils'; import { VirtualPrinterSettings } from '../../components/VirtualPrinterSettings'; // Mock the API client vi.mock('../../api/client', () => ({ api: { getSettings: vi.fn().mockResolvedValue({}), updateSettings: vi.fn().mockResolvedValue({}), }, virtualPrinterApi: { getSettings: vi.fn(), updateSettings: vi.fn(), getModels: vi.fn().mockResolvedValue({ models: { '3DPrinter-X1-Carbon': 'X1C', 'C12': 'P1S', 'N7': 'P2S', }, }), }, })); // Import mocked module import { virtualPrinterApi } from '../../api/client'; // Mock data factory const createMockSettings = (overrides = {}) => ({ enabled: false, access_code_set: false, mode: 'immediate' as const, model: '3DPrinter-X1-Carbon', target_printer_id: null as number | null, remote_interface_ip: null as string | null, status: { enabled: false, running: false, mode: 'immediate', name: 'Bambuddy', serial: '00M00A391800001', model: '3DPrinter-X1-Carbon', model_name: 'X1C', pending_files: 0, }, ...overrides, }); describe('VirtualPrinterSettings', () => { beforeEach(() => { vi.clearAllMocks(); // Default mock implementation vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue(createMockSettings()); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue(createMockSettings()); }); describe('rendering', () => { it('renders loading state initially', () => { // Delay the API response to catch loading state vi.mocked(virtualPrinterApi.getSettings).mockImplementation( () => new Promise(() => {}) // Never resolves ); render(); // Should show loading spinner expect(document.querySelector('.animate-spin')).toBeInTheDocument(); }); it('renders component title', async () => { render(); await waitFor(() => { expect(screen.getByText('Virtual Printer')).toBeInTheDocument(); }); }); it('renders enable toggle', async () => { render(); await waitFor(() => { expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument(); }); }); it('renders access code section', async () => { render(); await waitFor(() => { expect(screen.getByText('Access Code')).toBeInTheDocument(); }); }); it('renders mode section', async () => { render(); await waitFor(() => { expect(screen.getByText('Mode')).toBeInTheDocument(); }); }); it('renders how it works info', async () => { render(); await waitFor(() => { expect(screen.getByText('How it works:')).toBeInTheDocument(); }); }); }); describe('status indicator', () => { it('shows Stopped when not running', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ status: { ...createMockSettings().status, running: false } }) ); render(); await waitFor(() => { expect(screen.getByText('Stopped')).toBeInTheDocument(); }); }); it('shows Running when active', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ enabled: true, status: { ...createMockSettings().status, enabled: true, running: true }, }) ); render(); await waitFor(() => { expect(screen.getByText('Running')).toBeInTheDocument(); }); }); it('shows status details when running', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ enabled: true, status: { enabled: true, running: true, mode: 'immediate', name: 'Bambuddy', serial: '00M00A391800001', model: '3DPrinter-X1-Carbon', model_name: 'X1C', pending_files: 0, }, }) ); render(); await waitFor(() => { expect(screen.getByText('Status Details')).toBeInTheDocument(); expect(screen.getByText('Bambuddy')).toBeInTheDocument(); expect(screen.getByText('00M00A391800001')).toBeInTheDocument(); }); }); }); describe('access code', () => { it('shows warning when access code not set', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ access_code_set: false }) ); render(); await waitFor(() => { expect(screen.getByText('No access code set - required to enable')).toBeInTheDocument(); }); }); it('shows success when access code is set', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ access_code_set: true }) ); render(); await waitFor(() => { expect(screen.getByText('Access code is set')).toBeInTheDocument(); }); }); it('shows character count while typing', async () => { const user = userEvent.setup(); render(); await waitFor(() => { expect(screen.getByText('Access Code')).toBeInTheDocument(); }); const input = screen.getByPlaceholderText('Enter 8-char code'); await user.type(input, '1234'); expect(screen.getByText('(4/8)')).toBeInTheDocument(); }); it('saves access code on button click', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue( createMockSettings({ access_code_set: true }) ); render(); await waitFor(() => { expect(screen.getByText('Access Code')).toBeInTheDocument(); }); const input = screen.getByPlaceholderText('Enter 8-char code'); await user.type(input, '12345678'); const saveButton = screen.getByRole('button', { name: 'Save' }); await user.click(saveButton); await waitFor(() => { expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ access_code: '12345678', }); }); }); it('toggles password visibility', async () => { const user = userEvent.setup(); render(); await waitFor(() => { expect(screen.getByText('Access Code')).toBeInTheDocument(); }); const input = screen.getByPlaceholderText('Enter 8-char code'); expect(input).toHaveAttribute('type', 'password'); // Find and click the visibility toggle (eye icon button) const toggleButtons = screen.getAllByRole('button'); const visibilityToggle = toggleButtons.find( (btn) => btn.querySelector('svg') && btn.className.includes('absolute') ); if (visibilityToggle) { await user.click(visibilityToggle); expect(input).toHaveAttribute('type', 'text'); } }); }); describe('mode selection', () => { it('renders Archive mode option', async () => { render(); await waitFor(() => { expect(screen.getByText('Archive')).toBeInTheDocument(); expect(screen.getByText('Archive files immediately')).toBeInTheDocument(); }); }); it('renders Review mode option', async () => { render(); await waitFor(() => { expect(screen.getByText('Review')).toBeInTheDocument(); expect(screen.getByText('Review before archiving')).toBeInTheDocument(); }); }); it('renders Queue mode option', async () => { render(); await waitFor(() => { expect(screen.getByText('Queue')).toBeInTheDocument(); expect(screen.getByText('Archive and add to queue')).toBeInTheDocument(); }); }); it('highlights current mode (review)', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ mode: 'review' }) ); render(); await waitFor(() => { const reviewButton = screen.getByText('Review').closest('button'); expect(reviewButton?.className).toContain('border-bambu-green'); }); }); it('highlights current mode (legacy queue maps to review)', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ mode: 'queue' }) ); render(); await waitFor(() => { const reviewButton = screen.getByText('Review').closest('button'); expect(reviewButton?.className).toContain('border-bambu-green'); }); }); it('changes mode to review on click', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue( createMockSettings({ mode: 'review' }) ); render(); await waitFor(() => { expect(screen.getByText('Review')).toBeInTheDocument(); }); const reviewButton = screen.getByText('Review').closest('button'); if (reviewButton) { await user.click(reviewButton); } await waitFor(() => { expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'review' }); }); }); it('changes mode to print_queue on click', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue( createMockSettings({ mode: 'print_queue' }) ); render(); await waitFor(() => { expect(screen.getByText('Queue')).toBeInTheDocument(); }); const queueButton = screen.getByText('Queue').closest('button'); if (queueButton) { await user.click(queueButton); } await waitFor(() => { expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'print_queue' }); }); }); }); describe('enable/disable toggle', () => { it('cannot enable without access code', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ enabled: false, access_code_set: false }) ); render(); await waitFor(() => { expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument(); }); // Find the toggle switch (it's a button with relative class containing the slider) const allButtons = screen.getAllByRole('button'); const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12')); if (toggle) { await user.click(toggle); } // Should not call update API (no access code set) expect(virtualPrinterApi.updateSettings).not.toHaveBeenCalled(); }); it('can enable when access code is set', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ enabled: false, access_code_set: true }) ); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue( createMockSettings({ enabled: true, access_code_set: true }) ); render(); await waitFor(() => { expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument(); }); // Find the toggle switch (it's a button with rounded-full and w-12 classes) const allButtons = screen.getAllByRole('button'); const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12')); expect(toggle).toBeDefined(); if (toggle) { await user.click(toggle); } await waitFor(() => { expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith( expect.objectContaining({ enabled: true }) ); }); }); it('can disable when enabled', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ enabled: true, access_code_set: true }) ); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue( createMockSettings({ enabled: false, access_code_set: true }) ); render(); await waitFor(() => { expect(screen.getByText('Enable Virtual Printer')).toBeInTheDocument(); }); // Find the toggle switch const allButtons = screen.getAllByRole('button'); const toggle = allButtons.find((btn) => btn.className.includes('rounded-full') && btn.className.includes('w-12')); expect(toggle).toBeDefined(); if (toggle) { await user.click(toggle); } await waitFor(() => { expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith( expect.objectContaining({ enabled: false }) ); }); }); }); describe('info section', () => { it('shows setup required warning', async () => { render(); await waitFor(() => { expect(screen.getByText('Setup Required')).toBeInTheDocument(); }); }); it('shows link to setup guide', async () => { render(); await waitFor(() => { expect(screen.getByText('Read the setup guide before enabling')).toBeInTheDocument(); }); }); it('shows how it works section', async () => { render(); await waitFor(() => { expect(screen.getByText('How it works:')).toBeInTheDocument(); expect(screen.getByText(/Complete the setup guide for your platform/)).toBeInTheDocument(); }); }); }); describe('proxy mode', () => { it('renders Proxy mode option', async () => { render(); await waitFor(() => { expect(screen.getByText('Proxy')).toBeInTheDocument(); expect(screen.getByText('Relay to real printer')).toBeInTheDocument(); }); }); it('highlights proxy mode when selected', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ mode: 'proxy', target_printer_id: 1 }) ); render(); await waitFor(() => { const proxyButton = screen.getByText('Proxy').closest('button'); expect(proxyButton?.className).toContain('border-blue-500'); }); }); it('shows proxy status details when running in proxy mode', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ enabled: true, mode: 'proxy', target_printer_id: 1, status: { enabled: true, running: true, mode: 'proxy', name: 'Bambuddy (Proxy)', serial: '00M00A391800001', model: '3DPrinter-X1-Carbon', model_name: 'X1C', pending_files: 0, proxy: { running: true, target_host: '192.168.1.100', ftp_port: 990, // Privileged port for Bambu Studio compatibility mqtt_port: 8883, ftp_connections: 1, mqtt_connections: 2, }, }, }) ); render(); await waitFor(() => { expect(screen.getByText('Running')).toBeInTheDocument(); expect(screen.getByText('Status Details')).toBeInTheDocument(); // IP address appears in multiple places (target printer and status details) expect(screen.getAllByText('192.168.1.100').length).toBeGreaterThan(0); }); }); it('shows target printer dropdown in proxy mode', async () => { vi.mocked(virtualPrinterApi.getSettings).mockResolvedValue( createMockSettings({ mode: 'proxy' }) ); render(); await waitFor(() => { expect(screen.getByText('Target Printer')).toBeInTheDocument(); expect(screen.getByText('Select a printer...')).toBeInTheDocument(); }); }); it('changes mode to proxy on click', async () => { const user = userEvent.setup(); vi.mocked(virtualPrinterApi.updateSettings).mockResolvedValue( createMockSettings({ mode: 'proxy' }) ); render(); await waitFor(() => { expect(screen.getByText('Proxy')).toBeInTheDocument(); }); const proxyButton = screen.getByText('Proxy').closest('button'); if (proxyButton) { await user.click(proxyButton); } await waitFor(() => { expect(virtualPrinterApi.updateSettings).toHaveBeenCalledWith({ mode: 'proxy' }); }); }); }); });