| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555 |
- /**
- * 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> = {}): 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument();
- });
- it('shows archive name', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- expect(screen.getByText('Benchy')).toBeInTheDocument();
- });
- it('shows printer selection with checkboxes for multi-select', async () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
- expect(screen.getByText('P1S')).toBeInTheDocument();
- });
- });
- it('has print button', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
- });
- it('calls onClose when cancel is clicked', async () => {
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- await user.click(screen.getByRole('button', { name: /cancel/i }));
- expect(mockOnClose).toHaveBeenCalled();
- });
- it('print button is disabled until printer is selected', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('No active printers available')).toBeInTheDocument();
- });
- });
- it('shows print options toggle', async () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('Print Options')).toBeInTheDocument();
- });
- });
- });
- describe('create mode', () => {
- it('renders the modal title', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('heading', { name: 'Print' })).toBeInTheDocument();
- });
- it('shows archive name', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- expect(screen.getByText('Test Print')).toBeInTheDocument();
- });
- it('shows add button', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('button', { name: /^print$/i })).toBeInTheDocument();
- });
- it('shows cancel button', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
- });
- it('shows Queue option', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument();
- });
- it('shows power off option', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- expect(screen.getByText(/power off/i)).toBeInTheDocument();
- });
- it('shows schedule options', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- expect(screen.getByText('Edit Queue Item')).toBeInTheDocument();
- });
- it('shows save button', () => {
- const item = createMockQueueItem();
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
- });
- it('shows cancel button', () => {
- const item = createMockQueueItem();
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument();
- });
- it('shows print options toggle', () => {
- const item = createMockQueueItem();
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- expect(screen.getByText('Print Options')).toBeInTheDocument();
- });
- it('shows Queue option', () => {
- const item = createMockQueueItem();
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- expect(screen.getByRole('button', { name: /queue/i })).toBeInTheDocument();
- });
- it('shows power off option', () => {
- const item = createMockQueueItem();
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- expect(screen.getByText(/power off/i)).toBeInTheDocument();
- });
- it('calls onClose when cancel button is clicked', async () => {
- const user = userEvent.setup();
- const item = createMockQueueItem();
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Test Print"
- queueItem={item}
- onClose={mockOnClose}
- />
- );
- // 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('Select all')).toBeInTheDocument();
- });
- });
- it('shows selected count when multiple printers selected', async () => {
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Test Print"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="MultiPlate.3mf"
- onClose={mockOnClose}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('Select All 3 Plates')).toBeInTheDocument();
- });
- });
- it('shows "Select All" button in create mode', async () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="MultiPlate.3mf"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="MultiPlate.3mf"
- onClose={mockOnClose}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="MultiPlate.3mf"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="MultiPlate.3mf"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- expect(screen.getByLabelText('Quantity')).toBeInTheDocument();
- });
- it('shows quantity input in create mode', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- expect(screen.getByLabelText('Quantity')).toBeInTheDocument();
- });
- it('does not show quantity input in edit-queue-item mode', () => {
- render(
- <PrintModal
- mode="edit-queue-item"
- archiveId={1}
- archiveName="Benchy"
- queueItem={createMockQueueItem()}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- expect(screen.queryByLabelText('Quantity')).not.toBeInTheDocument();
- });
- it('defaults quantity to 1', () => {
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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<string, unknown>[] = [];
- server.use(
- withSnippets(),
- http.post('/api/v1/queue/', async ({ request }) => {
- queueCalls.push((await request.json()) as Record<string, unknown>);
- return HttpResponse.json({ id: queueCalls.length, status: 'pending' });
- }),
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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<string, unknown>[] = [];
- server.use(
- withSnippets(),
- http.post('/api/v1/queue/', async ({ request }) => {
- queueCalls.push((await request.json()) as Record<string, unknown>);
- return HttpResponse.json({ id: queueCalls.length, status: 'pending' });
- }),
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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<string, unknown>[] = [];
- server.use(
- withSnippets(),
- http.post('/api/v1/queue/', async ({ request }) => {
- queueCalls.push((await request.json()) as Record<string, unknown>);
- return HttpResponse.json({ id: queueCalls.length, status: 'pending' });
- }),
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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<string, unknown> | null = null;
- server.use(
- http.post('/api/v1/queue/', async ({ request }) => {
- capturedBody = await request.json() as Record<string, unknown>;
- return HttpResponse.json({ id: 1, status: 'pending' });
- })
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- libraryFileId={5}
- archiveName="Benchy"
- projectId={42}
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- // 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<string, unknown> | null = null;
- server.use(
- http.post('/api/v1/queue/', async ({ request }) => {
- capturedBody = await request.json() as Record<string, unknown>;
- return HttpResponse.json({ id: 1, status: 'pending' });
- })
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- projectId={42}
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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<string, unknown> | null = null;
- server.use(
- http.post('/api/v1/queue/', async ({ request }) => {
- capturedBody = await request.json() as Record<string, unknown>;
- return HttpResponse.json({ id: 1, status: 'pending' });
- })
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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<string, unknown> | null = null;
- server.use(
- http.post('/api/v1/queue/', async ({ request }) => {
- capturedBody = await request.json() as Record<string, unknown>;
- return HttpResponse.json({ id: 1, status: 'pending' });
- })
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- archiveId={1}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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<string, unknown> | null = null;
- server.use(
- http.post('/api/v1/queue/', async ({ request }) => {
- capturedBody = (await request.json()) as Record<string, unknown>;
- return HttpResponse.json({ id: 1, status: 'pending' });
- })
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- libraryFileId={5}
- archiveName="Benchy"
- cleanupLibraryAfterDispatch
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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<string, unknown> | null = null;
- server.use(
- http.post('/api/v1/queue/', async ({ request }) => {
- capturedBody = (await request.json()) as Record<string, unknown>;
- return HttpResponse.json({ id: 1, status: 'pending' });
- })
- );
- const user = userEvent.setup();
- render(
- <PrintModal
- mode="create"
- libraryFileId={5}
- archiveName="Benchy"
- initialSelectedPrinterIds={[1]}
- onClose={mockOnClose}
- onSuccess={mockOnSuccess}
- />
- );
- 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();
- });
- });
- });
|