| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290 |
- /**
- * Tests for the LinkSpoolModal component.
- *
- * Tests the Spoolman link spool modal including:
- * - Displaying unlinked spools
- * - Selecting a spool to link
- * - Link success with toast notification
- * - Link error with toast notification
- */
- 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({ enabled: false, configured: 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(),
- trayUuid: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
- trayInfo: {
- type: 'PLA Basic',
- color: 'FF0000',
- location: 'AMS A1',
- },
- };
- const mockUnlinkedSpools = [
- {
- id: 1,
- filament_name: 'PLA Red',
- filament_material: 'PLA',
- filament_color_hex: 'FF0000',
- remaining_weight: 800,
- location: 'Shelf A',
- },
- {
- id: 2,
- filament_name: 'PETG Blue',
- filament_material: 'PETG',
- filament_color_hex: '0000FF',
- remaining_weight: 500,
- location: null,
- },
- ];
- beforeEach(() => {
- vi.clearAllMocks();
- vi.mocked(api.getUnlinkedSpools).mockResolvedValue(mockUnlinkedSpools);
- vi.mocked(api.linkSpool).mockResolvedValue({ success: true, message: 'Linked' });
- });
- describe('rendering', () => {
- it('renders modal title', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('Link to Spoolman')).toBeInTheDocument();
- });
- });
- it('displays tray info', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Basic')).toBeInTheDocument();
- expect(screen.getByText('(AMS A1)')).toBeInTheDocument();
- });
- });
- it('displays tray UUID', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText(defaultProps.trayUuid)).toBeInTheDocument();
- });
- });
- it('shows loading state while fetching spools', async () => {
- // Delay the response
- vi.mocked(api.getUnlinkedSpools).mockImplementation(
- () => new Promise(() => {})
- );
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(document.querySelector('.animate-spin')).toBeInTheDocument();
- });
- });
- it('displays unlinked spools list', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- expect(screen.getByText('PETG Blue')).toBeInTheDocument();
- });
- });
- it('shows message when no unlinked spools', async () => {
- vi.mocked(api.getUnlinkedSpools).mockResolvedValue([]);
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('No unlinked spools found in Spoolman.')).toBeInTheDocument();
- });
- });
- it('does not render when isOpen is false', () => {
- render(<LinkSpoolModal {...defaultProps} isOpen={false} />);
- expect(screen.queryByText('Link to Spoolman')).not.toBeInTheDocument();
- });
- });
- describe('spool selection', () => {
- it('allows selecting a spool', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- });
- // Click to select spool
- fireEvent.click(screen.getByText('PLA Red'));
- // Should show check mark (via visual styling)
- const selectedButton = screen.getByText('PLA Red').closest('button');
- expect(selectedButton).toHaveClass('border-bambu-green');
- });
- it('link button is disabled until spool is selected', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- });
- const linkButton = screen.getByRole('button', { name: /link spool/i });
- expect(linkButton).toBeDisabled();
- // Select a spool
- fireEvent.click(screen.getByText('PLA Red'));
- expect(linkButton).not.toBeDisabled();
- });
- });
- describe('linking', () => {
- it('calls linkSpool API on submit', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- });
- // Select a spool
- fireEvent.click(screen.getByText('PLA Red'));
- // Click link button
- fireEvent.click(screen.getByRole('button', { name: /link spool/i }));
- await waitFor(() => {
- expect(api.linkSpool).toHaveBeenCalledWith(1, defaultProps.trayUuid);
- });
- });
- it('shows success toast on successful link', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByText('PLA Red'));
- fireEvent.click(screen.getByRole('button', { name: /link spool/i }));
- await waitFor(() => {
- expect(mockShowToast).toHaveBeenCalledWith(
- 'Spool linked to Spoolman successfully',
- 'success'
- );
- });
- });
- it('calls onClose after successful link', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByText('PLA Red'));
- fireEvent.click(screen.getByRole('button', { name: /link spool/i }));
- await waitFor(() => {
- expect(defaultProps.onClose).toHaveBeenCalled();
- });
- });
- it('shows error toast on link failure', async () => {
- const errorMessage = 'Failed to update spool';
- vi.mocked(api.linkSpool).mockRejectedValue(new Error(errorMessage));
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('PLA Red')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByText('PLA Red'));
- fireEvent.click(screen.getByRole('button', { name: /link spool/i }));
- await waitFor(() => {
- expect(mockShowToast).toHaveBeenCalledWith(
- `Failed to link spool: ${errorMessage}`,
- 'error'
- );
- });
- });
- });
- describe('modal actions', () => {
- it('calls onClose when cancel button is clicked', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('Cancel')).toBeInTheDocument();
- });
- fireEvent.click(screen.getByText('Cancel'));
- expect(defaultProps.onClose).toHaveBeenCalled();
- });
- it('calls onClose when backdrop is clicked', async () => {
- render(<LinkSpoolModal {...defaultProps} />);
- await waitFor(() => {
- expect(screen.getByText('Link to Spoolman')).toBeInTheDocument();
- });
- // Click the backdrop (the element with bg-black/60)
- 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.getByText('Link to Spoolman')).toBeInTheDocument();
- });
- // Find and click the X button in the header
- const closeButtons = screen.getAllByRole('button');
- const xButton = closeButtons.find(btn => btn.querySelector('svg.lucide-x'));
- if (xButton) {
- fireEvent.click(xButton);
- expect(defaultProps.onClose).toHaveBeenCalled();
- }
- });
- });
- });
|