mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
[Fix]: Send Bambu RFID Tags to Spoolman, Allow Unlink in Manual Mode, Stop Location Clearing for Generic Spools (#719)
224 lines
6.4 KiB
TypeScript
224 lines
6.4 KiB
TypeScript
/**
|
|
* Tests for the LinkSpoolModal component.
|
|
*
|
|
* Tests the inventory link-to-spool modal including:
|
|
* - Rendering modal with tag/tray info
|
|
* - Displaying untagged spools
|
|
* - Linking a spool via click
|
|
* - Search filtering
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { screen, waitFor, fireEvent } from '@testing-library/react';
|
|
import { render } from '../utils';
|
|
import { LinkSpoolModal } from '../../components/LinkSpoolModal';
|
|
|
|
// Mock the API client
|
|
vi.mock('../../api/client', () => ({
|
|
api: {
|
|
getUnlinkedSpools: vi.fn(),
|
|
linkSpool: vi.fn(),
|
|
getSettings: vi.fn().mockResolvedValue({}),
|
|
getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
|
|
},
|
|
}));
|
|
|
|
// Mock the toast context
|
|
const mockShowToast = vi.fn();
|
|
vi.mock('../../contexts/ToastContext', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
|
|
return {
|
|
...actual,
|
|
useToast: () => ({ showToast: mockShowToast }),
|
|
};
|
|
});
|
|
|
|
// Import mocked module
|
|
import { api } from '../../api/client';
|
|
|
|
describe('LinkSpoolModal', () => {
|
|
const defaultProps = {
|
|
isOpen: true,
|
|
onClose: vi.fn(),
|
|
tagUid: 'ABCD1234',
|
|
trayUuid: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
|
|
printerId: 1,
|
|
amsId: 0,
|
|
trayId: 0,
|
|
};
|
|
|
|
const mockSpools = [
|
|
{
|
|
id: 1,
|
|
filament_name: 'Generic PLA Red',
|
|
filament_material: 'PLA',
|
|
filament_color_hex: 'FF0000',
|
|
remaining_weight: 800,
|
|
location: null,
|
|
},
|
|
{
|
|
id: 2,
|
|
filament_name: 'Bambu PETG Blue',
|
|
filament_material: 'PETG',
|
|
filament_color_hex: '0000FF',
|
|
remaining_weight: 500,
|
|
location: null,
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(api.getUnlinkedSpools).mockResolvedValue(mockSpools);
|
|
vi.mocked(api.linkSpool).mockResolvedValue({ success: true, message: 'ok' });
|
|
});
|
|
|
|
describe('rendering', () => {
|
|
it('renders modal title', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { name: /select spool/i })).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays printer and tray info', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/AMS 0 T0/)).toBeInTheDocument();
|
|
expect(screen.getByText(/Printer #1/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('shows loading state while fetching spools', async () => {
|
|
vi.mocked(api.getUnlinkedSpools).mockImplementation(() => new Promise(() => {}));
|
|
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(document.querySelector('.animate-spin')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('displays unlinked spools from Spoolman', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
// Should show spools from getUnlinkedSpools
|
|
expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
|
|
expect(screen.getByText(/Bambu PETG Blue/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('does not render when isOpen is false', () => {
|
|
render(<LinkSpoolModal {...defaultProps} isOpen={false} />);
|
|
expect(screen.queryByRole('heading', { name: /select spool/i })).not.toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('linking', () => {
|
|
it('uses trayUuid when linking if present (Bambu spool path)', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
|
|
|
|
await waitFor(() => {
|
|
expect(api.linkSpool).toHaveBeenCalledWith(1, {
|
|
spoolTag: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
|
|
printerId: 1,
|
|
amsId: 0,
|
|
trayId: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
it('falls back to tagUid when trayUuid is missing (generic spool path)', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} trayUuid="" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
|
|
|
|
await waitFor(() => {
|
|
expect(api.linkSpool).toHaveBeenCalledWith(1, {
|
|
spoolTag: 'ABCD1234',
|
|
printerId: 1,
|
|
amsId: 0,
|
|
trayId: 0,
|
|
});
|
|
});
|
|
});
|
|
|
|
it('shows success toast and calls onClose', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
|
|
|
|
await waitFor(() => {
|
|
expect(mockShowToast).toHaveBeenCalled();
|
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
it('shows error toast on failure', async () => {
|
|
vi.mocked(api.linkSpool).mockRejectedValue(new Error('Link failed'));
|
|
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
|
|
});
|
|
|
|
fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
|
|
|
|
await waitFor(() => {
|
|
expect(mockShowToast).toHaveBeenCalledWith(
|
|
expect.stringContaining('Link failed'),
|
|
'error'
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('modal actions', () => {
|
|
it('calls onClose when backdrop is clicked', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { name: /select spool/i })).toBeInTheDocument();
|
|
});
|
|
|
|
const backdrop = document.querySelector('.bg-black\\/60');
|
|
if (backdrop) {
|
|
fireEvent.click(backdrop);
|
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it('calls onClose when X button is clicked', async () => {
|
|
render(<LinkSpoolModal {...defaultProps} />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('heading', { name: /select spool/i })).toBeInTheDocument();
|
|
});
|
|
|
|
const closeButtons = screen.getAllByRole('button');
|
|
const xButton = closeButtons.find(btn => btn.querySelector('svg.lucide-x'));
|
|
if (xButton) {
|
|
fireEvent.click(xButton);
|
|
expect(defaultProps.onClose).toHaveBeenCalled();
|
|
}
|
|
});
|
|
});
|
|
});
|