/**
* Tests for AssignToAmsModal — verifies that spoolmanMode prop
* routes to assignSpoolmanSlot (not assignSpool) when assigning.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { render } from '../utils';
vi.mock('../../api/client', () => ({
api: {
getPrinterStatus: vi.fn(),
getPrinter: vi.fn(),
getPrinters: vi.fn(),
getSettings: vi.fn().mockResolvedValue({}),
assignSpool: vi.fn(),
assignSpoolmanSlot: vi.fn(),
updateSpool: vi.fn(),
getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
getAssignments: vi.fn().mockResolvedValue([]),
getSpoolmanSlotAssignments: vi.fn().mockResolvedValue([]),
},
}));
import { AssignToAmsModal } from '../../components/spoolbuddy/AssignToAmsModal';
import { api } from '../../api/client';
import { __resetColorCatalogForTests, setColorCatalog } from '../../utils/colors';
const SPOOL = {
id: 42,
material: 'PLA',
subtype: 'Basic',
brand: 'BrandX',
color_name: 'Red',
rgba: 'FF0000FF',
label_weight: 1000,
weight_used: 0,
tag_uid: null,
tray_uuid: null,
slicer_filament_name: 'PLA',
data_origin: 'spoolman',
k_profiles: [],
};
const BLANK_TRAY = {
tray_color: null,
tray_type: null,
tray_sub_brands: null,
tray_id_name: null,
tray_info_idx: null,
remain: 100,
k: null,
cali_idx: null,
tag_uid: null,
tray_uuid: null,
nozzle_temp_min: null,
nozzle_temp_max: null,
drying_temp: null,
drying_time: null,
state: null,
};
const PRINTER_STATUS_ONLINE = {
connected: true,
state: 'idle',
ams: [
{
id: 0,
humidity: null,
temp: null,
is_ams_ht: false,
serial_number: '',
sw_ver: '',
dry_time: 0,
dry_status: 0,
dry_sub_status: 0,
tray: [
{ id: 0, ...BLANK_TRAY },
{ id: 1, ...BLANK_TRAY },
{ id: 2, ...BLANK_TRAY },
{ id: 3, ...BLANK_TRAY },
],
},
],
nozzles: [{ nozzle_diameter: '0.4', nozzle_type: 'stainless' }],
ams_extruder_map: { '0': 0 },
dual_nozzle: false,
};
describe('AssignToAmsModal', () => {
beforeEach(() => {
vi.clearAllMocks();
__resetColorCatalogForTests();
vi.mocked(api.getPrinterStatus).mockResolvedValue(PRINTER_STATUS_ONLINE as never);
vi.mocked(api.getPrinter).mockResolvedValue({ id: 1, name: 'Test Printer' } as never);
vi.mocked(api.getPrinters).mockResolvedValue([
{ id: 7, name: 'Farm Printer 7', is_active: true },
] as never);
vi.mocked(api.assignSpool).mockResolvedValue({} as never);
vi.mocked(api.assignSpoolmanSlot).mockResolvedValue({} as never);
vi.mocked(api.updateSpool).mockResolvedValue({} as never);
});
it('renders modal when open', async () => {
render(
);
await waitFor(() => {
expect(screen.getByText(/Assign.*to AMS/i)).toBeInTheDocument();
});
});
it('moves focus into the dialog and traps Tab navigation', async () => {
render(
<>
>
);
const dialog = await screen.findByRole('dialog');
await waitFor(() => expect(dialog).toContainElement(document.activeElement as HTMLElement));
const focusable = Array.from(dialog.querySelectorAll(
'button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
));
const first = focusable[0];
const last = focusable[focusable.length - 1];
expect(first).toBeTruthy();
expect(last).toBeTruthy();
last.focus();
fireEvent.keyDown(document, { key: 'Tab' });
expect(document.activeElement).toBe(first);
first.focus();
fireEvent.keyDown(document, { key: 'Tab', shiftKey: true });
expect(document.activeElement).toBe(last);
screen.getByTestId('outside-control').focus();
fireEvent.keyDown(document, { key: 'Tab' });
expect(document.activeElement).toBe(first);
});
it('renders nothing when closed', () => {
render(
);
expect(screen.queryByText(/Assign.*to AMS/i)).not.toBeInTheDocument();
});
it('selects a printer and requires explicit slot confirmation in the inventory flow (#2978)', async () => {
const user = userEvent.setup();
render(
);
const printerSelect = await screen.findByLabelText(/select printer/i);
await screen.findByRole('option', { name: 'Farm Printer 7' });
await user.selectOptions(printerSelect, '7');
await waitFor(() => {
expect(screen.queryAllByTestId('ams-slot').length).toBeGreaterThan(0);
});
const slotButtons = screen.queryAllByTestId('ams-slot');
expect(slotButtons[0]).toHaveAccessibleName('AMS A Slot 1');
await user.click(slotButtons[0]);
expect(api.assignSpool).not.toHaveBeenCalled();
await user.click(screen.getByRole('button', { name: /assign spool/i }));
await waitFor(() => {
expect(api.assignSpool).toHaveBeenCalledWith(
expect.objectContaining({ spool_id: 42, printer_id: 7, ams_id: 0, tray_id: 0 })
);
});
});
it.each([
{
caseName: 'missing colour name resolved from the catalog',
spool: { color_name: null, rgba: '00AE42FF' },
catalog: { '00ae42': 'Bambu Green' },
},
{
caseName: 'stored Bambu colour code',
spool: { color_name: 'A1-B2', rgba: 'FF00AAFF' },
catalog: {},
},
])('does not rewrite $caseName when assigning without a colour edit', async ({ spool, catalog }) => {
const user = userEvent.setup();
setColorCatalog(catalog);
render(
);
const printerSelect = await screen.findByLabelText(/select printer/i);
await screen.findByRole('option', { name: 'Farm Printer 7' });
await user.selectOptions(printerSelect, '7');
await waitFor(() => {
expect(screen.queryAllByTestId('ams-slot').length).toBeGreaterThan(0);
});
await user.click(screen.queryAllByTestId('ams-slot')[0]);
await user.click(screen.getByRole('button', { name: /assign spool/i }));
await waitFor(() => expect(api.assignSpool).toHaveBeenCalled());
expect(api.updateSpool).not.toHaveBeenCalled();
});
it('keeps the kiosk layout full-screen while selecting a printer', async () => {
render(
);
const dialog = await screen.findByRole('dialog');
expect(dialog).toHaveClass('w-full', 'h-full');
expect(dialog).not.toHaveClass('max-w-3xl', 'max-w-5xl');
expect(await screen.findByLabelText(/select printer/i)).toBeInTheDocument();
expect(screen.queryByLabelText(/color name/i)).not.toBeInTheDocument();
});
it('names the spool by Bambuddy\'s catalog-resolved colour in the header (#3090)', async () => {
setColorCatalog({ d02727: 'Candy Red' });
render(
);
expect((await screen.findAllByText(/Candy Red/)).length).toBeGreaterThan(0);
expect(screen.queryByText(/Silk\+/)).toBeNull();
});
it('labels and assigns the left external slot correctly on dual-nozzle printers', async () => {
const user = userEvent.setup();
vi.mocked(api.getPrinter).mockResolvedValue({
id: 7,
name: 'Farm Printer 7',
nozzle_count: 2,
} as never);
vi.mocked(api.getPrinterStatus).mockResolvedValue({
...PRINTER_STATUS_ONLINE,
ams: [],
vt_tray: [
{ id: 254, ...BLANK_TRAY },
{ id: 255, ...BLANK_TRAY },
],
} as never);
render(
);
const printerSelect = await screen.findByLabelText(/select printer/i);
await screen.findByRole('option', { name: 'Farm Printer 7' });
await user.selectOptions(printerSelect, '7');
const leftExternalSlot = await screen.findByTitle('Ext-L');
await user.click(leftExternalSlot);
expect(screen.getAllByText('Ext-L').length).toBeGreaterThan(0);
await user.click(screen.getByRole('button', { name: /assign spool/i }));
await waitFor(() => {
expect(api.assignSpool).toHaveBeenCalledWith(
expect.objectContaining({ printer_id: 7, ams_id: 255, tray_id: 0 })
);
});
});
describe('API routing based on spoolmanMode', () => {
it('calls assignSpool when spoolmanMode is false', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.queryAllByTestId('ams-slot').length).toBeGreaterThan(0);
});
// Click first available slot button
const slotButtons = screen.queryAllByTestId('ams-slot');
await user.click(slotButtons[0]);
await waitFor(() => {
expect(api.assignSpool).toHaveBeenCalledWith(
expect.objectContaining({ spool_id: 42, printer_id: 1 })
);
});
expect(api.assignSpoolmanSlot).not.toHaveBeenCalled();
});
it('calls assignSpoolmanSlot when spoolmanMode is true', async () => {
const user = userEvent.setup();
render(
);
await waitFor(() => {
expect(screen.queryAllByTestId('ams-slot').length).toBeGreaterThan(0);
});
const slotButtons = screen.queryAllByTestId('ams-slot');
await user.click(slotButtons[0]);
await waitFor(() => {
expect(api.assignSpoolmanSlot).toHaveBeenCalledWith(
expect.objectContaining({ spoolman_spool_id: 42, printer_id: 1 })
);
});
expect(api.assignSpool).not.toHaveBeenCalled();
});
});
describe('query invalidation after successful assign', () => {
it('calls assignSpoolmanSlot successfully — invalidation fires on success', async () => {
const user = userEvent.setup();
vi.mocked(api.assignSpoolmanSlot).mockResolvedValue({} as never);
render(
);
await waitFor(() => {
expect(screen.queryAllByTestId('ams-slot').length).toBeGreaterThan(0);
});
const slotButtons = screen.queryAllByTestId('ams-slot');
await user.click(slotButtons[0]);
await waitFor(() => {
expect(api.assignSpoolmanSlot).toHaveBeenCalledWith(
expect.objectContaining({ spoolman_spool_id: 42, printer_id: 1 })
);
});
});
});
describe('slot highlighting', () => {
it('shows no ring-bambu-green when spool is not assigned', async () => {
vi.mocked(api.getSpoolmanSlotAssignments).mockResolvedValue([]);
const { container } = render(
);
await waitFor(() => {
expect(screen.queryAllByRole('button').length).toBeGreaterThan(0);
});
expect(container.querySelector('.ring-bambu-green')).toBeNull();
});
it('highlights assigned slot with ring-bambu-green when spool is assigned to tray 2', async () => {
vi.mocked(api.getSpoolmanSlotAssignments).mockResolvedValue([
{ printer_id: 1, ams_id: 0, tray_id: 2, spoolman_spool_id: 42 },
] as never);
const { container } = render(
);
await waitFor(() => {
expect(container.querySelector('.ring-bambu-green')).toBeInTheDocument();
});
});
});
});