| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- /**
- * Tests for the PrinterQueueWidget clear plate behavior.
- *
- * When the printer is in FINISH or FAILED state and has pending queue items,
- * the widget shows a "Clear Plate & Start Next" button instead of the
- * passive queue link. After clicking, it shows a confirmation state.
- */
- 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 { PrinterQueueWidget } from '../../components/PrinterQueueWidget';
- import { http, HttpResponse } from 'msw';
- import { server } from '../mocks/server';
- const mockQueueItems = [
- {
- id: 1,
- printer_id: 1,
- archive_id: 1,
- position: 1,
- status: 'pending',
- archive_name: 'First Print',
- printer_name: 'X1 Carbon',
- print_time_seconds: 3600,
- scheduled_time: null,
- },
- {
- id: 2,
- printer_id: 1,
- archive_id: 2,
- position: 2,
- status: 'pending',
- archive_name: 'Second Print',
- printer_name: 'X1 Carbon',
- print_time_seconds: 7200,
- scheduled_time: null,
- },
- ];
- describe('PrinterQueueWidget - Clear Plate', () => {
- beforeEach(() => {
- server.use(
- http.get('/api/v1/queue/', ({ request }) => {
- const url = new URL(request.url);
- const printerId = url.searchParams.get('printer_id');
- if (printerId === '1') {
- return HttpResponse.json(mockQueueItems);
- }
- return HttpResponse.json([]);
- }),
- http.post('/api/v1/printers/:id/clear-plate', () => {
- return HttpResponse.json({ success: true, message: 'Plate cleared' });
- })
- );
- });
- describe('clear plate button visibility', () => {
- it('shows clear plate button when printer state is FINISH', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('shows clear plate button when printer state is FAILED', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('shows passive link when printer state is IDLE', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="IDLE" />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
- });
- it('shows passive link when printer state is RUNNING', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="RUNNING" />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- });
- it('shows passive link when printerState is not provided', async () => {
- render(<PrinterQueueWidget printerId={1} />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- });
- it('shows passive link when FINISH but plateCleared is true', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={false} />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
- });
- it('shows passive link when FAILED but plateCleared is true', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={false} />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
- });
- // Regression for #961: after Auto Off cycles the printer it boots into IDLE while
- // still awaiting plate-clear ack. The prompt must still show — the ack state, not
- // the reported printer state, is the authoritative signal.
- it('shows clear plate button in IDLE state when awaitingPlateClear is true (#961)', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="IDLE" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('shows clear plate button with no printerState when awaitingPlateClear is true', async () => {
- // State may be null briefly after a reconnect; the widget must still gate on the flag.
- render(<PrinterQueueWidget printerId={1} awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- });
- describe('clear plate button shows queue info', () => {
- it('shows next item name in clear plate mode', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('First Print')).toBeInTheDocument();
- });
- });
- it('shows additional items badge in clear plate mode', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('+1')).toBeInTheDocument();
- });
- });
- });
- describe('clear plate action', () => {
- it('shows confirmation state after clicking clear plate', async () => {
- const user = userEvent.setup();
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- await user.click(screen.getByText('Clear Plate & Start Next'));
- await waitFor(() => {
- // Both the widget confirmation and the toast show this text
- const elements = screen.getAllByText('Plate cleared — ready for next print');
- expect(elements.length).toBeGreaterThanOrEqual(1);
- });
- });
- it('shows error toast on API failure', async () => {
- server.use(
- http.post('/api/v1/printers/:id/clear-plate', () => {
- return HttpResponse.json(
- { detail: 'Printer not connected' },
- { status: 400 }
- );
- })
- );
- const user = userEvent.setup();
- render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- await user.click(screen.getByText('Clear Plate & Start Next'));
- // Button should remain visible (not transition to success state)
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- });
- describe('empty queue', () => {
- it('renders nothing in FINISH state with no queue items', async () => {
- const { container } = render(<PrinterQueueWidget printerId={999} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(container.querySelector('button')).not.toBeInTheDocument();
- });
- });
- });
- describe('filament compatibility filtering', () => {
- const petgQueueItems = [
- {
- id: 10,
- printer_id: 1,
- archive_id: 10,
- position: 1,
- status: 'pending',
- archive_name: 'PETG Print',
- printer_name: 'H2S',
- print_time_seconds: 3600,
- scheduled_time: null,
- required_filament_types: ['PETG'],
- },
- ];
- it('hides widget when queue item requires filament not loaded on printer', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(petgQueueItems))
- );
- const { container } = render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PLA'])}
- />
- );
- // Wait for query to settle, then confirm widget is not rendered
- await waitFor(() => {
- expect(container.querySelector('button')).not.toBeInTheDocument();
- });
- expect(screen.queryByText('PETG Print')).not.toBeInTheDocument();
- });
- it('shows widget when queue item required filaments match loaded', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(petgQueueItems))
- );
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PLA', 'PETG'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('PETG Print')).toBeInTheDocument();
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('shows widget when queue item has no required_filament_types', async () => {
- // Default mockQueueItems have no required_filament_types
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PLA'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('First Print')).toBeInTheDocument();
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('shows widget when loadedFilamentTypes prop is not provided', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(petgQueueItems))
- );
- render(
- <PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />
- );
- await waitFor(() => {
- expect(screen.getByText('PETG Print')).toBeInTheDocument();
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('skips incompatible first item and shows compatible second item', async () => {
- const mixedQueue = [
- {
- id: 10,
- printer_id: 1,
- archive_id: 10,
- position: 1,
- status: 'pending',
- archive_name: 'PETG Print',
- printer_name: 'H2S',
- print_time_seconds: 3600,
- scheduled_time: null,
- required_filament_types: ['PETG'],
- },
- {
- id: 11,
- printer_id: 1,
- archive_id: 11,
- position: 2,
- status: 'pending',
- archive_name: 'PLA Print',
- printer_name: 'H2S',
- print_time_seconds: 1800,
- scheduled_time: null,
- required_filament_types: ['PLA'],
- },
- ];
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(mixedQueue))
- );
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PLA'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('PLA Print')).toBeInTheDocument();
- });
- expect(screen.queryByText('PETG Print')).not.toBeInTheDocument();
- });
- it('matches filament types case-insensitively', async () => {
- const lowercaseQueue = [
- {
- id: 10,
- printer_id: 1,
- archive_id: 10,
- position: 1,
- status: 'pending',
- archive_name: 'Petg Print',
- printer_name: 'H2S',
- print_time_seconds: 3600,
- scheduled_time: null,
- required_filament_types: ['petg'],
- },
- ];
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(lowercaseQueue))
- );
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PETG'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('Petg Print')).toBeInTheDocument();
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- });
- describe('filament override color filtering', () => {
- const whitePetgOverrideItem = [
- {
- id: 20,
- printer_id: null,
- archive_id: 20,
- position: 1,
- status: 'pending',
- archive_name: 'White PETG Print',
- printer_name: null,
- print_time_seconds: 3600,
- scheduled_time: null,
- required_filament_types: ['PETG'],
- filament_overrides: [{ slot_id: 1, type: 'PETG', color: '#FFFFFF' }],
- },
- ];
- it('hides widget when override color does not match loaded filaments', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
- );
- const { container } = render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PETG'])}
- loadedFilaments={new Set(['PETG:0000ff'])}
- />
- );
- await waitFor(() => {
- expect(container.querySelector('button')).not.toBeInTheDocument();
- });
- expect(screen.queryByText('White PETG Print')).not.toBeInTheDocument();
- });
- it('shows widget when override color matches loaded filaments', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
- );
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PETG'])}
- loadedFilaments={new Set(['PETG:ffffff'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('White PETG Print')).toBeInTheDocument();
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('normalizes override color format (strips # and lowercases)', async () => {
- const upperCaseColorItem = [
- {
- id: 21,
- printer_id: null,
- archive_id: 21,
- position: 1,
- status: 'pending',
- archive_name: 'Red PLA Print',
- printer_name: null,
- print_time_seconds: 3600,
- scheduled_time: null,
- required_filament_types: ['PLA'],
- filament_overrides: [{ slot_id: 1, type: 'PLA', color: '#FF0000' }],
- },
- ];
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(upperCaseColorItem))
- );
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PLA'])}
- loadedFilaments={new Set(['PLA:ff0000'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('Red PLA Print')).toBeInTheDocument();
- });
- });
- it('shows widget when no loadedFilaments prop is provided (no color filtering)', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(whitePetgOverrideItem))
- );
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PETG'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('White PETG Print')).toBeInTheDocument();
- });
- });
- it('shows widget when queue item has no filament overrides', async () => {
- // Default mockQueueItems have no filament_overrides
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilaments={new Set(['PLA:000000'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('First Print')).toBeInTheDocument();
- });
- });
- it('matches any override when multiple overrides exist', async () => {
- const multiOverrideItem = [
- {
- id: 22,
- printer_id: null,
- archive_id: 22,
- position: 1,
- status: 'pending',
- archive_name: 'Multi Color Print',
- printer_name: null,
- print_time_seconds: 3600,
- scheduled_time: null,
- required_filament_types: ['PLA'],
- filament_overrides: [
- { slot_id: 1, type: 'PLA', color: '#FF0000' },
- { slot_id: 2, type: 'PLA', color: '#00FF00' },
- ],
- },
- ];
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(multiOverrideItem))
- );
- // Printer has green PLA but not red — should still match (at least one override)
- render(
- <PrinterQueueWidget
- printerId={1}
- printerState="FINISH"
- awaitingPlateClear={true}
- loadedFilamentTypes={new Set(['PLA'])}
- loadedFilaments={new Set(['PLA:00ff00'])}
- />
- );
- await waitFor(() => {
- expect(screen.getByText('Multi Color Print')).toBeInTheDocument();
- });
- });
- });
- describe('requirePlateClear setting', () => {
- it('shows passive link when requirePlateClear is false even in FINISH state', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={false} />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
- });
- it('shows passive link when requirePlateClear is false even in FAILED state', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FAILED" awaitingPlateClear={true} requirePlateClear={false} />);
- await waitFor(() => {
- const link = screen.getByRole('link');
- expect(link).toHaveAttribute('href', '/queue');
- });
- expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
- });
- it('shows clear plate button when requirePlateClear is true (explicit)', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('shows clear plate button when requirePlateClear is not provided (defaults to true)', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- it('still shows next item info in passive link when requirePlateClear is false', async () => {
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} requirePlateClear={false} />);
- await waitFor(() => {
- expect(screen.getByText('First Print')).toBeInTheDocument();
- });
- });
- });
- describe('staged (manual_start) items', () => {
- const stagedItems = [
- { id: 10, printer_id: 1, archive_id: 1, position: 1, status: 'pending', archive_name: 'Staged Print 1', manual_start: true, scheduled_time: null },
- { id: 11, printer_id: 1, archive_id: 2, position: 2, status: 'pending', archive_name: 'Staged Print 2', manual_start: true, scheduled_time: null },
- ];
- it('does not show clear plate button when all items are staged', async () => {
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(stagedItems)),
- );
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- // Should show the passive link (not the clear plate button)
- await waitFor(() => {
- expect(screen.getByText('Staged Print 1')).toBeInTheDocument();
- });
- expect(screen.queryByText('Clear Plate & Start Next')).not.toBeInTheDocument();
- });
- it('shows clear plate button when mix of staged and auto-dispatch items', async () => {
- const mixedItems = [
- { id: 10, printer_id: 1, archive_id: 1, position: 1, status: 'pending', archive_name: 'Staged Print', manual_start: true, scheduled_time: null },
- { id: 11, printer_id: 1, archive_id: 2, position: 2, status: 'pending', archive_name: 'Auto Print', manual_start: false, scheduled_time: null },
- ];
- server.use(
- http.get('/api/v1/queue/', () => HttpResponse.json(mixedItems)),
- );
- render(<PrinterQueueWidget printerId={1} printerState="FINISH" awaitingPlateClear={true} />);
- await waitFor(() => {
- expect(screen.getByText('Clear Plate & Start Next')).toBeInTheDocument();
- });
- });
- });
- });
|