/** * Tests for the unified PrintModal component. * * The PrintModal supports two modes: * - 'create': Create a print queue item * - 'edit-queue-item': Edit existing queue item (single printer) */ 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 { PrintModal } from '../../components/PrintModal'; import { http, HttpResponse } from 'msw'; import { server } from '../mocks/server'; import type { PrintQueueItem } from '../../api/client'; const mockPrinters = [ { id: 1, name: 'X1 Carbon', model: 'X1C', ip_address: '192.168.1.100', enabled: true, is_active: true }, { id: 2, name: 'P1S', model: 'P1S', ip_address: '192.168.1.101', enabled: true, is_active: true }, { id: 3, name: 'A1 Mini', model: 'A1M', ip_address: '192.168.1.102', enabled: true, is_active: true }, ]; const createMockQueueItem = (overrides: Partial = {}): PrintQueueItem => ({ id: 1, printer_id: 1, archive_id: 1, position: 1, scheduled_time: null, require_previous_success: false, auto_off_after: false, gcode_injection: false, manual_start: false, ams_mapping: null, plate_id: null, bed_levelling: true, flow_cali: false, vibration_cali: true, layer_inspect: false, timelapse: false, use_ams: true, status: 'pending', started_at: null, completed_at: null, error_message: null, created_at: '2024-01-01T00:00:00Z', archive_name: 'Test Print', archive_thumbnail: null, printer_name: 'Test Printer', print_time_seconds: 3600, batch_id: null, batch_name: null, ...overrides, }); describe('PrintModal', () => { const mockOnClose = vi.fn(); const mockOnSuccess = vi.fn(); beforeEach(() => { vi.clearAllMocks(); server.use( http.get('/api/v1/printers/', () => { return HttpResponse.json(mockPrinters); }), http.get('/api/v1/archives/:id/plates', () => { return HttpResponse.json({ is_multi_plate: false, plates: [] }); }), http.get('/api/v1/archives/:id/filament-requirements', () => { return HttpResponse.json({ filaments: [] }); }), http.get('/api/v1/printers/:id/status', () => { return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] }); }), http.post('/api/v1/queue/', () => { return HttpResponse.json({ id: 1, status: 'pending' }); }), http.patch('/api/v1/queue/:id', () => { return HttpResponse.json({ id: 1, status: 'pending' }); }) ); }); describe('create mode', () => { it('renders the modal title', () => { render( ); expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument(); }); it('shows archive name', () => { render( ); expect(screen.getByText('Benchy')).toBeInTheDocument(); }); it('shows printer selection with checkboxes for multi-select', async () => { render( ); await waitFor(() => { expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); expect(screen.getByText('P1S')).toBeInTheDocument(); }); }); it('has print button', () => { render( ); // Get the submit button specifically (not printer selection buttons) const submitButton = screen.getByRole('button', { name: /^print$/i }); expect(submitButton).toBeInTheDocument(); }); it('has cancel button', () => { render( ); expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); }); it('calls onClose when cancel is clicked', async () => { const user = userEvent.setup(); render( ); await user.click(screen.getByRole('button', { name: /cancel/i })); expect(mockOnClose).toHaveBeenCalled(); }); it('print button is disabled until printer is selected', () => { render( ); // Get the submit button specifically (not printer selection buttons) const printButton = screen.getByRole('button', { name: /^print$/i }); expect(printButton).toBeDisabled(); }); it('shows no printers message when none active', async () => { server.use( http.get('/api/v1/printers/', () => { return HttpResponse.json([]); }) ); render( ); await waitFor(() => { expect(screen.getByText('No active printers available')).toBeInTheDocument(); }); }); it('shows print options toggle', async () => { render( ); await waitFor(() => { expect(screen.getByText('Print Options')).toBeInTheDocument(); }); }); }); describe('create mode', () => { it('renders the modal title', () => { render( ); expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument(); }); it('shows archive name', () => { render( ); expect(screen.getByText('Test Print')).toBeInTheDocument(); }); it('shows add button', () => { render( ); expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); it('shows cancel button', () => { render( ); expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); }); it('shows Queue option', () => { render( ); expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument(); }); it('shows power off option', () => { render( ); expect(screen.getByText(/power off/i)).toBeInTheDocument(); }); it('shows schedule options', () => { render( ); expect(screen.getByRole('button', { name: /asap/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /schedule/i })).toBeInTheDocument(); }); it('orders schedule options by time', () => { render( ); const options = screen.getAllByRole('button', { name: /^(asap|queue|schedule)$/i }); expect(options.map(button => button.textContent?.trim())).toEqual(['ASAP', 'Queue', 'Schedule']); }); it('calls onClose when cancel is clicked', async () => { const user = userEvent.setup(); render( ); await user.click(screen.getByRole('button', { name: /cancel/i })); expect(mockOnClose).toHaveBeenCalled(); }); }); describe('edit-queue-item mode', () => { it('renders the modal title', () => { const item = createMockQueueItem(); render( ); expect(screen.getByText('Edit Queue Item')).toBeInTheDocument(); }); it('shows save button', () => { const item = createMockQueueItem(); render( ); expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument(); }); it('shows cancel button', () => { const item = createMockQueueItem(); render( ); expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument(); }); it('shows print options toggle', () => { const item = createMockQueueItem(); render( ); expect(screen.getByText('Print Options')).toBeInTheDocument(); }); it('shows Queue option', () => { const item = createMockQueueItem(); render( ); expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument(); }); it('shows power off option', () => { const item = createMockQueueItem(); render( ); expect(screen.getByText(/power off/i)).toBeInTheDocument(); }); it('calls onClose when cancel button is clicked', async () => { const user = userEvent.setup(); const item = createMockQueueItem(); render( ); const cancelButton = screen.getByRole('button', { name: /cancel/i }); await user.click(cancelButton); expect(mockOnClose).toHaveBeenCalled(); }); it('shows printer selector for single selection', async () => { const item = createMockQueueItem(); render( ); // PrinterSelector shows printer names directly await waitFor(() => { expect(screen.getByText('P1S')).toBeInTheDocument(); }); }); }); describe('multi-printer selection', () => { it('shows select all button when multiple printers available', async () => { render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); }); it('shows selected count when multiple printers selected', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText(/3 printers selected/)).toBeInTheDocument(); }); }); it('updates button text when multiple printers selected', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); }); }); describe('busy printer handling (#622)', () => { beforeEach(() => { // Set up per-printer statuses: printer 1 RUNNING, printer 2 IDLE, printer 3 FINISH server.use( http.get('/api/v1/printers/:id/status', ({ params }) => { const id = Number(params.id); if (id === 1) { return HttpResponse.json({ connected: true, state: 'RUNNING', stg_cur_name: null, ams: [], vt_tray: [], nozzles: [], }); } if (id === 2) { return HttpResponse.json({ connected: true, state: 'IDLE', stg_cur_name: null, ams: [], vt_tray: [], nozzles: [], }); } // printer 3 return HttpResponse.json({ connected: true, state: 'FINISH', stg_cur_name: null, ams: [], vt_tray: [], nozzles: [], }); }) ); }); it('shows state badges on printers in create mode', async () => { render( ); await waitFor(() => { expect(screen.getByText('Printing')).toBeInTheDocument(); expect(screen.getByText('Idle')).toBeInTheDocument(); expect(screen.getByText('Finished')).toBeInTheDocument(); }); }); it('allows selecting a busy printer in create mode', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Printing')).toBeInTheDocument(); }); const busyButton = screen.getByText('X1 Carbon').closest('button'); expect(busyButton).not.toBeDisabled(); await user.click(busyButton!); await waitFor(() => { expect(screen.getByText('1 printer selected')).toBeInTheDocument(); }); }); it('select all includes busy printers in create mode', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); expect(screen.getByText('Printing')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText(/3 printers selected/)).toBeInTheDocument(); }); }); it('allows selecting busy printers in create mode', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Printing')).toBeInTheDocument(); }); // The busy printer button should NOT be disabled in queue mode const busyButton = screen.getByText('X1 Carbon').closest('button'); expect(busyButton).not.toBeDisabled(); await user.click(busyButton!); await waitFor(() => { expect(screen.getByText('1 printer selected')).toBeInTheDocument(); }); }); it('shows Offline badge for disconnected printers', async () => { server.use( http.get('/api/v1/printers/:id/status', () => { return HttpResponse.json({ connected: false, state: null, stg_cur_name: null, ams: [], vt_tray: [], nozzles: [], }); }) ); render( ); await waitFor(() => { const offlineBadges = screen.getAllByText('Offline'); expect(offlineBadges.length).toBeGreaterThanOrEqual(1); }); }); it('shows calibration stage name when printer is calibrating', async () => { server.use( http.get('/api/v1/printers/:id/status', () => { return HttpResponse.json({ connected: true, state: 'RUNNING', stg_cur_name: 'Auto bed leveling', ams: [], vt_tray: [], nozzles: [], }); }) ); render( ); await waitFor(() => { const badges = screen.getAllByText('Auto bed leveling'); expect(badges.length).toBeGreaterThanOrEqual(1); }); }); }); describe('stagger start', () => { it('does not show stagger option with single printer in queue mode', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); }); // Select single printer await user.click(screen.getByText('X1 Carbon')); expect(screen.queryByText('Stagger printer starts')).not.toBeInTheDocument(); }); it('shows stagger option when multiple printers selected in queue mode', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText('Stagger printer starts')).toBeInTheDocument(); }); }); it('shows stagger option in create mode with multiple printers', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText(/2 printers selected|3 printers selected/)).toBeInTheDocument(); }); expect(screen.getByText('Stagger printer starts')).toBeInTheDocument(); }); it('shows stagger preview in create mode when enabled', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText('Stagger printer starts')).toBeInTheDocument(); }); await user.click(screen.getByLabelText('Stagger printer starts')); await waitFor(() => { // Default: 3 printers, group size 2 = 2 groups — preview text shown expect(screen.getByText(/3 printers.*2 groups/)).toBeInTheDocument(); }); }); it('does not show stagger option in create mode with single printer', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); }); // Select only one printer await user.click(screen.getByText('X1 Carbon')); expect(screen.queryByText('Stagger printer starts')).not.toBeInTheDocument(); }); it('shows stagger inputs when stagger checkbox is enabled', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText('Stagger printer starts')).toBeInTheDocument(); }); await user.click(screen.getByLabelText('Stagger printer starts')); await waitFor(() => { expect(screen.getByText('Group size')).toBeInTheDocument(); expect(screen.getByText('Interval (min)')).toBeInTheDocument(); }); }); it('shows stagger preview with printer count', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select all')).toBeInTheDocument(); }); await user.click(screen.getByText('Select all')); await waitFor(() => { expect(screen.getByText('Stagger printer starts')).toBeInTheDocument(); }); await user.click(screen.getByLabelText('Stagger printer starts')); await waitFor(() => { // Default: 3 printers, group size 2 = 2 groups — preview text includes printer count expect(screen.getByText(/3 printers.*2 groups/)).toBeInTheDocument(); }); }); }); describe('multi-plate selection', () => { const multiPlateResponse = { is_multi_plate: true, plates: [ { index: 1, name: 'Plate 1', has_thumbnail: false, thumbnail_url: null, objects: ['Part A'], filaments: [{ type: 'PLA', color: '#FF0000' }], print_time_seconds: 1800, filament_used_grams: 50 }, { index: 2, name: 'Plate 2', has_thumbnail: false, thumbnail_url: null, objects: ['Part B'], filaments: [{ type: 'PLA', color: '#00FF00' }], print_time_seconds: 2400, filament_used_grams: 60 }, { index: 3, name: 'Plate 3', has_thumbnail: false, thumbnail_url: null, objects: ['Part C'], filaments: [{ type: 'PETG', color: '#0000FF' }], print_time_seconds: 3000, filament_used_grams: 70 }, ], }; beforeEach(() => { server.use( http.get('/api/v1/archives/:id/plates', () => { return HttpResponse.json(multiPlateResponse); }), ); }); it('shows "Select All" button only in create mode', async () => { render( ); await waitFor(() => { expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument(); }); }); it('shows "Select All" button in create mode', async () => { render( ); await waitFor(() => { expect(screen.getByText('Plate 1')).toBeInTheDocument(); }); expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument(); }); it('selects all plates when "Select All" is clicked', async () => { const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument(); }); await user.click(screen.getByText('Select All 3 Plates')); // All plates should be highlighted (green border) await waitFor(() => { const plateButtons = document.querySelectorAll('button[type="button"].border-bambu-green'); // 3 plate buttons + the "Deselect All" toggle button = 4 green-bordered buttons expect(plateButtons.length).toBeGreaterThanOrEqual(3); }); }); it('allows selecting a subset of plates to queue', async () => { const queueRequests: unknown[] = []; server.use( http.post('/api/v1/queue/', async ({ request }) => { const body = await request.json(); queueRequests.push(body); return HttpResponse.json({ id: queueRequests.length, status: 'pending' }); }), ); const user = userEvent.setup(); render( ); // Wait for plates and select a printer await waitFor(() => { expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument(); expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); }); // Select printer await user.click(screen.getByText('X1 Carbon')); // Plate 1 is auto-selected. Click Plate 3 to add it (multi-select in create mode) await user.click(screen.getByText('Plate 3')); // Submit — should queue plates 1 and 3 const submitButton = document.querySelector('button[type="submit"]') as HTMLElement; await user.click(submitButton); await waitFor(() => { expect(queueRequests.length).toBe(2); }); expect((queueRequests[0] as { plate_id: number }).plate_id).toBe(1); expect((queueRequests[1] as { plate_id: number }).plate_id).toBe(3); }); it('creates one queue item per plate when submitting with select-all', async () => { const queueRequests: unknown[] = []; server.use( http.post('/api/v1/queue/', async ({ request }) => { const body = await request.json(); queueRequests.push(body); return HttpResponse.json({ id: queueRequests.length, status: 'pending' }); }), ); const user = userEvent.setup(); render( ); // Wait for plates and select a printer await waitFor(() => { expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument(); expect(screen.getByText('X1 Carbon')).toBeInTheDocument(); }); // Select printer await user.click(screen.getByText('X1 Carbon')); // Click select all await user.click(screen.getByText('Select All 3 Plates')); // Find the submit button (type="submit") — distinct from the toggle button (type="button") const submitButton = document.querySelector('button[type="submit"]') as HTMLElement; await user.click(submitButton); await waitFor(() => { expect(queueRequests.length).toBe(3); }); // Verify each request has the correct plate_id expect((queueRequests[0] as { plate_id: number }).plate_id).toBe(1); expect((queueRequests[1] as { plate_id: number }).plate_id).toBe(2); expect((queueRequests[2] as { plate_id: number }).plate_id).toBe(3); }); }); describe('batch quantity', () => { it('shows quantity input in create mode', () => { render( ); expect(screen.getByLabelText('Quantity')).toBeInTheDocument(); }); it('shows quantity input in create mode', () => { render( ); expect(screen.getByLabelText('Quantity')).toBeInTheDocument(); }); it('does not show quantity input in edit-queue-item mode', () => { render( ); expect(screen.queryByLabelText('Quantity')).not.toBeInTheDocument(); }); it('defaults quantity to 1', () => { render( ); const input = screen.getByLabelText('Quantity') as HTMLInputElement; expect(input.value).toBe('1'); }); it('quantity input has default value of 1 and accepts changes', async () => { const user = userEvent.setup(); render( ); const input = screen.getByLabelText('Quantity') as HTMLInputElement; expect(input.value).toBe('1'); await user.tripleClick(input); await user.keyboard('5'); expect(input.value).toBe('5'); }); }); describe('reprint G-code injection dispatch (#422 / auto-eject)', () => { // Guards the fix: when "Inject auto-print G-code" is ticked on a create with // quantity > 1, ALL copies must go through the queue so every one is injected by // the scheduler. The first copy must NOT be dispatched immediately via the direct // create path — that path bypasses injection and would leave the first copy stuck // on the plate for auto-eject setups. const withSnippets = () => http.get('/api/v1/settings/', () => HttpResponse.json({ gcode_snippets: { X1C: { start_gcode: '', end_gcode: 'M400' } } }), ); it('injection ON queues all copies and dispatches none immediately', async () => { const queueCalls: Record[] = []; server.use( withSnippets(), http.post('/api/v1/queue/', async ({ request }) => { queueCalls.push((await request.json()) as Record); return HttpResponse.json({ id: queueCalls.length, status: 'pending' }); }), ); const user = userEvent.setup(); render( ); // Quantity > 1 so the injection checkbox is offered const qty = (await screen.findByLabelText('Quantity')) as HTMLInputElement; await user.tripleClick(qty); await user.keyboard('3'); expect(qty.value).toBe('3'); // Checkbox only renders once snippets are loaded AND quantity > 1 await user.click(await screen.findByLabelText(/inject auto-print/i)); await user.click(document.querySelector('button[type="submit"]') as HTMLElement); await waitFor(() => expect(queueCalls.length).toBe(1)); // One queue item carrying all copies, and zero immediate reprint dispatches expect(queueCalls[0].quantity).toBe(3); }); it('quantity 1 + snippets configured: checkbox toggles cleanly (#1852)', async () => { // The previous reset effect in PrintModal/index.tsx silently flipped // gcodeInjection back to false whenever effectiveQuantity <= 1 in create // mode, so a single print with snippets configured couldn't tick the // checkbox — every click visibly un-ticked itself within the next render. // The scheduler reads item.gcode_injection per queue item regardless of // batch size, so quantity 1 must be allowed too. const queueCalls: Record[] = []; server.use( withSnippets(), http.post('/api/v1/queue/', async ({ request }) => { queueCalls.push((await request.json()) as Record); return HttpResponse.json({ id: queueCalls.length, status: 'pending' }); }), ); const user = userEvent.setup(); render( ); // Quantity stays at default (1) — this is the OP's exact scenario. const checkbox = (await screen.findByLabelText(/inject auto-print/i)) as HTMLInputElement; expect(checkbox.checked).toBe(false); await user.click(checkbox); // CRITICAL: after the click the checkbox must stay checked. Under the // pre-fix reset effect, the parent state would flip back to false within // a render and the displayed `checked` attribute would track it. await waitFor(() => expect(checkbox.checked).toBe(true)); await user.click(document.querySelector('button[type="submit"]') as HTMLElement); await waitFor(() => expect(queueCalls.length).toBe(1)); // The injection flag actually reaches the API — pre-fix the parent reset // would have stripped it before submit. expect(queueCalls[0].gcode_injection).toBe(true); }); it('injection OFF queues all copies through the scheduler path', async () => { const queueCalls: Record[] = []; server.use( withSnippets(), http.post('/api/v1/queue/', async ({ request }) => { queueCalls.push((await request.json()) as Record); return HttpResponse.json({ id: queueCalls.length, status: 'pending' }); }), ); const user = userEvent.setup(); render( ); const qty = (await screen.findByLabelText('Quantity')) as HTMLInputElement; await user.tripleClick(qty); await user.keyboard('3'); expect(qty.value).toBe('3'); // Leave injection unticked: unified dispatch still queues all copies. await user.click(document.querySelector('button[type="submit"]') as HTMLElement); await waitFor(() => expect(queueCalls.length).toBe(1)); expect(queueCalls[0].quantity).toBe(3); }); }); describe('project_id forwarding', () => { beforeEach(() => { // Additional handlers needed for library file mode server.use( http.get('/api/v1/library/files/:id', () => { return HttpResponse.json({ id: 5, filename: 'benchy.gcode.3mf', print_name: null, file_type: '3mf', folder_id: null, project_id: null, file_hash: null, file_size_bytes: 1024, thumbnail_path: null, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); }), http.get('/api/v1/library/files/:id/plates', () => { return HttpResponse.json({ is_multi_plate: false, plates: [] }); }), http.get('/api/v1/library/files/:id/filament-requirements', () => { return HttpResponse.json({ file_id: 5, filename: 'benchy.gcode.3mf', filaments: [] }); }), http.get('/api/v1/printers/:id/status', () => { return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] }); }), ); }); it('includes project_id in queue item when printing a library file with projectId set', async () => { let capturedBody: Record | null = null; server.use( http.post('/api/v1/queue/', async ({ request }) => { capturedBody = await request.json() as Record; return HttpResponse.json({ id: 1, status: 'pending' }); }) ); const user = userEvent.setup(); render( ); // Wait for the modal to load printer and file data await waitFor(() => { expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: /^print$/i })); await waitFor(() => { expect(capturedBody).not.toBeNull(); expect(capturedBody?.library_file_id).toBe(5); expect(capturedBody?.project_id).toBe(42); }); }); it('queues archive prints through the scheduler path', async () => { let capturedBody: Record | null = null; server.use( http.post('/api/v1/queue/', async ({ request }) => { capturedBody = await request.json() as Record; return HttpResponse.json({ id: 1, status: 'pending' }); }) ); const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: /^print$/i })); await waitFor(() => { expect(capturedBody).not.toBeNull(); expect(capturedBody?.archive_id).toBe(1); expect(capturedBody?.project_id).toBe(42); }); }); it('adds ASAP prints to the top of the queue', async () => { let capturedBody: Record | null = null; server.use( http.post('/api/v1/queue/', async ({ request }) => { capturedBody = await request.json() as Record; return HttpResponse.json({ id: 1, status: 'pending' }); }) ); const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: /^print$/i })); await waitFor(() => { expect(capturedBody).not.toBeNull(); expect(capturedBody?.insert_at_top).toBe(true); expect(capturedBody?.insert_position).toBe(1); expect(capturedBody?.manual_start).toBe(false); expect(capturedBody?.scheduled_time).toBeUndefined(); }); }); it('adds Queue prints to the back unless manual start is required', async () => { let capturedBody: Record | null = null; server.use( http.post('/api/v1/queue/', async ({ request }) => { capturedBody = await request.json() as Record; return HttpResponse.json({ id: 1, status: 'pending' }); }) ); const user = userEvent.setup(); render( ); await user.click(screen.getByRole('button', { name: /^queue$/i })); expect(screen.getByLabelText(/require manual start/i)).toBeInTheDocument(); await user.click(screen.getByLabelText(/require manual start/i)); await user.click(screen.getByRole('button', { name: /^print$/i })); await waitFor(() => { expect(capturedBody).not.toBeNull(); expect(capturedBody?.insert_at_top).toBeUndefined(); expect(capturedBody?.insert_position).toBeUndefined(); expect(capturedBody?.manual_start).toBe(true); }); }); }); describe('cleanup_library_after_dispatch forwarding (#730)', () => { // The Printers-page Direct-Print flow passes cleanupLibraryAfterDispatch so the // transient LibraryFile created by FileUploadModal is deleted once the archive // owns its own copy. File Manager / Project Detail flows leave the prop unset so // their deliberately-added library entries survive the print. beforeEach(() => { server.use( http.get('/api/v1/library/files/:id', () => { return HttpResponse.json({ id: 5, filename: 'benchy.gcode.3mf', file_type: '3mf', folder_id: null, project_id: null, file_hash: null, file_size_bytes: 1024, thumbnail_path: null, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }); }), http.get('/api/v1/library/files/:id/plates', () => { return HttpResponse.json({ is_multi_plate: false, plates: [] }); }), http.get('/api/v1/library/files/:id/filament-requirements', () => { return HttpResponse.json({ file_id: 5, filename: 'benchy.gcode.3mf', filaments: [] }); }), http.get('/api/v1/printers/:id/status', () => { return HttpResponse.json({ connected: true, state: 'IDLE', ams: [], vt_tray: [] }); }), ); }); it('forwards cleanup_library_after_dispatch=true when the Direct-Print prop is set', async () => { let capturedBody: Record | null = null; server.use( http.post('/api/v1/queue/', async ({ request }) => { capturedBody = (await request.json()) as Record; return HttpResponse.json({ id: 1, status: 'pending' }); }) ); const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: /^print$/i })); await waitFor(() => { expect(capturedBody).not.toBeNull(); expect(capturedBody?.cleanup_library_after_dispatch).toBe(true); }); }); it('defaults to omitting cleanup_library_after_dispatch (File Manager / Project flows survive)', async () => { let capturedBody: Record | null = null; server.use( http.post('/api/v1/queue/', async ({ request }) => { capturedBody = (await request.json()) as Record; return HttpResponse.json({ id: 1, status: 'pending' }); }) ); const user = userEvent.setup(); render( ); await waitFor(() => { expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument(); }); await user.click(screen.getByRole('button', { name: /^print$/i })); await waitFor(() => { expect(capturedBody).not.toBeNull(); }); // Either omitted entirely or explicitly undefined — both interpret as "keep file" expect(capturedBody?.cleanup_library_after_dispatch).toBeUndefined(); }); }); });