|
|
@@ -3222,3 +3222,223 @@ describe('PrintModal default printer', () => {
|
|
|
expect(screen.getByText('P1S').closest('button')).not.toHaveClass('border-bambu-green');
|
|
|
});
|
|
|
});
|
|
|
+
|
|
|
+describe('PrintModal — "Force color match" on a specific printer is saved (#3445)', () => {
|
|
|
+ const mockOnClose = vi.fn();
|
|
|
+
|
|
|
+ const PRINTERS = [{ id: 1, name: 'Printer 01', model: 'P1S', ip_address: '192.168.1.101', enabled: true, is_active: true }];
|
|
|
+ // Sliced in white; the printer holds green (tray 0) and black (tray 1), no white.
|
|
|
+ const SLOT_1_WHITE = { slot_id: 1, type: 'PLA', color: '#CBC6B8', tray_info_idx: 'GFA01', used_grams: 50 };
|
|
|
+
|
|
|
+ type Override = { slot_id: number; type: string; color: string; tray_info_idx?: string; force_color_match: boolean };
|
|
|
+ type Sent = { printer_id?: number | null; ams_mapping?: number[] | null; filament_overrides?: Override[] | null };
|
|
|
+ let patched: Sent[];
|
|
|
+ let posted: Sent[];
|
|
|
+
|
|
|
+ beforeEach(() => {
|
|
|
+ vi.clearAllMocks();
|
|
|
+ patched = [];
|
|
|
+ posted = [];
|
|
|
+ server.use(
|
|
|
+ http.get('/api/v1/printers/', () => HttpResponse.json(PRINTERS)),
|
|
|
+ http.get('/api/v1/archives/:id/plates', () => HttpResponse.json({ is_multi_plate: false, plates: [] })),
|
|
|
+ http.get('/api/v1/archives/:id/filament-requirements', () => HttpResponse.json({ filaments: [SLOT_1_WHITE] })),
|
|
|
+ http.get('/api/v1/printers/:id/status', () =>
|
|
|
+ HttpResponse.json({
|
|
|
+ connected: true,
|
|
|
+ state: 'IDLE',
|
|
|
+ ams: [
|
|
|
+ {
|
|
|
+ id: 0,
|
|
|
+ tray: [
|
|
|
+ { id: 0, tray_type: 'PLA', tray_color: '68724DFF', tray_info_idx: 'GFA01' },
|
|
|
+ { id: 1, tray_type: 'PLA', tray_color: '000000FF', tray_info_idx: 'GFA01' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ vt_tray: [],
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ http.get('/api/v1/printers/:id/assignments', () => HttpResponse.json([])),
|
|
|
+ http.patch('/api/v1/queue/:id', async ({ request }) => {
|
|
|
+ patched.push((await request.json()) as Sent);
|
|
|
+ return HttpResponse.json({ id: 1, status: 'pending' });
|
|
|
+ }),
|
|
|
+ http.post('/api/v1/queue/', async ({ request }) => {
|
|
|
+ posted.push((await request.json()) as Sent);
|
|
|
+ return HttpResponse.json({ id: 1, status: 'pending' });
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ const printer01Item = (overrides: Partial<PrintQueueItem> = {}) =>
|
|
|
+ createMockQueueItem({ printer_id: 1, target_model: null, ams_mapping: [1], ...overrides } as Partial<PrintQueueItem>);
|
|
|
+
|
|
|
+ const BLACK_FORCED = { slot_id: 1, type: 'PLA', color: '#000000', tray_info_idx: 'GFA01', force_color_match: true };
|
|
|
+
|
|
|
+ const openMapping = async (user: ReturnType<typeof userEvent.setup>) => {
|
|
|
+ await user.click(await screen.findByText(/filament mapping/i));
|
|
|
+ return screen.findByRole('checkbox', { name: /force color match/i });
|
|
|
+ };
|
|
|
+
|
|
|
+ const submit = async (user: ReturnType<typeof userEvent.setup>) => {
|
|
|
+ await user.click(document.querySelector('button[type="submit"]') as HTMLElement);
|
|
|
+ };
|
|
|
+
|
|
|
+ it("saves a ticked slot with the filament of the tray it was mapped to, not the sliced one", async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={printer01Item()} onClose={mockOnClose} />);
|
|
|
+
|
|
|
+ await user.click(await openMapping(user));
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(patched).toHaveLength(1));
|
|
|
+ expect(patched[0].ams_mapping).toEqual([1]);
|
|
|
+ // Black, so a recompute at dispatch asks for black and never takes green.
|
|
|
+ expect(patched[0].filament_overrides).toEqual([expect.objectContaining(BLACK_FORCED)]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('shows the box ticked again when the job is reopened, and keeps it on save', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(
|
|
|
+ <PrintModal
|
|
|
+ mode="edit-queue-item"
|
|
|
+ archiveId={1}
|
|
|
+ archiveName="Job"
|
|
|
+ queueItem={printer01Item({ filament_overrides: [{ ...BLACK_FORCED, color_name: 'Black' }] } as Partial<PrintQueueItem>)}
|
|
|
+ onClose={mockOnClose}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(await openMapping(user)).toBeChecked();
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(patched).toHaveLength(1));
|
|
|
+ expect(patched[0].filament_overrides).toEqual([expect.objectContaining(BLACK_FORCED)]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('saves an untick', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(
|
|
|
+ <PrintModal
|
|
|
+ mode="edit-queue-item"
|
|
|
+ archiveId={1}
|
|
|
+ archiveName="Job"
|
|
|
+ queueItem={printer01Item({ filament_overrides: [BLACK_FORCED] } as Partial<PrintQueueItem>)}
|
|
|
+ onClose={mockOnClose}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+
|
|
|
+ await user.click(await openMapping(user));
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(patched).toHaveLength(1));
|
|
|
+ // Black stays asked for, as picked; only no longer strictly.
|
|
|
+ expect(patched[0].filament_overrides).toEqual([
|
|
|
+ expect.objectContaining({ slot_id: 1, color: '#000000', force_color_match: false }),
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("keeps a reopened job's forced filament when its tray has been reloaded since", async () => {
|
|
|
+ // Tray 1 now holds a green third-party spool with no preset.
|
|
|
+ server.use(
|
|
|
+ http.get('/api/v1/printers/:id/status', () =>
|
|
|
+ HttpResponse.json({
|
|
|
+ connected: true,
|
|
|
+ state: 'IDLE',
|
|
|
+ ams: [{ id: 0, tray: [{ id: 1, tray_type: 'PLA', tray_color: '68724DFF', tray_info_idx: '' }] }],
|
|
|
+ vt_tray: [],
|
|
|
+ }),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(
|
|
|
+ <PrintModal
|
|
|
+ mode="edit-queue-item"
|
|
|
+ archiveId={1}
|
|
|
+ archiveName="Job"
|
|
|
+ queueItem={printer01Item({ filament_overrides: [BLACK_FORCED] } as Partial<PrintQueueItem>)}
|
|
|
+ onClose={mockOnClose}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+
|
|
|
+ expect(await openMapping(user)).toBeChecked();
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(patched).toHaveLength(1));
|
|
|
+ // Still black, variant pin included: saving the schedule must not turn it green.
|
|
|
+ expect(patched[0].filament_overrides).toEqual([expect.objectContaining(BLACK_FORCED)]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it("takes the new tray's filament when a reopened forced slot is moved to another tray", async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(
|
|
|
+ <PrintModal
|
|
|
+ mode="edit-queue-item"
|
|
|
+ archiveId={1}
|
|
|
+ archiveName="Job"
|
|
|
+ queueItem={printer01Item({ filament_overrides: [BLACK_FORCED] } as Partial<PrintQueueItem>)}
|
|
|
+ onClose={mockOnClose}
|
|
|
+ />,
|
|
|
+ );
|
|
|
+
|
|
|
+ await openMapping(user);
|
|
|
+ await user.click(await screen.findByRole('combobox', { name: /printer slot for/i }));
|
|
|
+ await user.click(within(await screen.findByRole('listbox')).getByRole('option', { name: /A1/ }));
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(patched).toHaveLength(1));
|
|
|
+ expect(patched[0].ams_mapping).toEqual([0]);
|
|
|
+ expect(patched[0].filament_overrides).toEqual([
|
|
|
+ expect.objectContaining({ slot_id: 1, color: '#68724D', force_color_match: true }),
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('waits for the sliced colour when the box is ticked on a tray the dialog matched itself', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(
|
|
|
+ <PrintModal mode="create" archiveId={1} archiveName="Job" initialSelectedPrinterIds={[1]} onClose={mockOnClose} />,
|
|
|
+ );
|
|
|
+
|
|
|
+ // No white loaded: the dialog stands in a PLA of another colour. The tick
|
|
|
+ // asks for white, not for that stand-in.
|
|
|
+ await user.click(await screen.findByRole('checkbox', { name: /force color match/i }));
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(posted).toHaveLength(1));
|
|
|
+ expect(posted[0].filament_overrides).toEqual([
|
|
|
+ expect.objectContaining({ slot_id: 1, type: 'PLA', color: '#CBC6B8', force_color_match: true }),
|
|
|
+ ]);
|
|
|
+ });
|
|
|
+
|
|
|
+ it('sends nothing for a slot left unticked and unchanged', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(<PrintModal mode="edit-queue-item" archiveId={1} archiveName="Job" queueItem={printer01Item()} onClose={mockOnClose} />);
|
|
|
+
|
|
|
+ await openMapping(user);
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(patched).toHaveLength(1));
|
|
|
+ expect(patched[0].filament_overrides ?? null).toBeNull();
|
|
|
+ });
|
|
|
+
|
|
|
+ it('saves the tick on a new job queued for one printer', async () => {
|
|
|
+ const user = userEvent.setup();
|
|
|
+ render(
|
|
|
+ <PrintModal mode="create" archiveId={1} archiveName="Job" initialSelectedPrinterIds={[1]} onClose={mockOnClose} />,
|
|
|
+ );
|
|
|
+
|
|
|
+ // Pick black for the white slot, then tick it. A preselected printer opens
|
|
|
+ // the mapping panel expanded.
|
|
|
+ await user.click(await screen.findByRole('combobox', { name: /printer slot for/i }));
|
|
|
+ const listbox = await screen.findByRole('listbox');
|
|
|
+ await user.click(within(listbox).getByRole('option', { name: /A2/ }));
|
|
|
+ await user.click(screen.getByRole('checkbox', { name: /force color match/i }));
|
|
|
+ await submit(user);
|
|
|
+
|
|
|
+ await waitFor(() => expect(posted).toHaveLength(1));
|
|
|
+ expect(posted[0].printer_id).toBe(1);
|
|
|
+ expect(posted[0].ams_mapping).toEqual([1]);
|
|
|
+ expect(posted[0].filament_overrides).toEqual([expect.objectContaining(BLACK_FORCED)]);
|
|
|
+ });
|
|
|
+});
|