LinkSpoolModal.test.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. /**
  2. * Tests for the LinkSpoolModal component.
  3. *
  4. * Tests the inventory link-to-spool modal including:
  5. * - Rendering modal with tag/tray info
  6. * - Displaying untagged spools
  7. * - Linking a spool via click
  8. * - Search filtering
  9. */
  10. import { describe, it, expect, vi, beforeEach } from 'vitest';
  11. import { screen, waitFor, fireEvent } from '@testing-library/react';
  12. import { render } from '../utils';
  13. import { LinkSpoolModal } from '../../components/LinkSpoolModal';
  14. // Mock the API client
  15. vi.mock('../../api/client', () => ({
  16. api: {
  17. getUnlinkedSpools: vi.fn(),
  18. linkSpool: vi.fn(),
  19. getSettings: vi.fn().mockResolvedValue({}),
  20. getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
  21. },
  22. }));
  23. // Mock the toast context
  24. const mockShowToast = vi.fn();
  25. vi.mock('../../contexts/ToastContext', async (importOriginal) => {
  26. const actual = await importOriginal<typeof import('../../contexts/ToastContext')>();
  27. return {
  28. ...actual,
  29. useToast: () => ({ showToast: mockShowToast }),
  30. };
  31. });
  32. // Import mocked module
  33. import { api } from '../../api/client';
  34. describe('LinkSpoolModal', () => {
  35. const defaultProps = {
  36. isOpen: true,
  37. onClose: vi.fn(),
  38. tagUid: 'ABCD1234',
  39. trayUuid: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
  40. printerId: 1,
  41. amsId: 0,
  42. trayId: 0,
  43. };
  44. const mockSpools = [
  45. {
  46. id: 1,
  47. filament_name: 'Generic PLA Red',
  48. filament_material: 'PLA',
  49. filament_color_hex: 'FF0000',
  50. remaining_weight: 800,
  51. location: null,
  52. },
  53. {
  54. id: 2,
  55. filament_name: 'Bambu PETG Blue',
  56. filament_material: 'PETG',
  57. filament_color_hex: '0000FF',
  58. remaining_weight: 500,
  59. location: null,
  60. },
  61. ];
  62. beforeEach(() => {
  63. vi.clearAllMocks();
  64. vi.mocked(api.getUnlinkedSpools).mockResolvedValue(mockSpools);
  65. vi.mocked(api.linkSpool).mockResolvedValue({ success: true, message: 'ok' });
  66. });
  67. describe('rendering', () => {
  68. it('renders modal title', async () => {
  69. render(<LinkSpoolModal {...defaultProps} />);
  70. await waitFor(() => {
  71. expect(screen.getByRole('heading', { name: /select spool/i })).toBeInTheDocument();
  72. });
  73. });
  74. it('displays printer and tray info', async () => {
  75. render(<LinkSpoolModal {...defaultProps} />);
  76. await waitFor(() => {
  77. expect(screen.getByText(/AMS 0 T0/)).toBeInTheDocument();
  78. expect(screen.getByText(/Printer #1/)).toBeInTheDocument();
  79. });
  80. });
  81. it('shows loading state while fetching spools', async () => {
  82. vi.mocked(api.getUnlinkedSpools).mockImplementation(() => new Promise(() => {}));
  83. render(<LinkSpoolModal {...defaultProps} />);
  84. await waitFor(() => {
  85. expect(document.querySelector('.animate-spin')).toBeInTheDocument();
  86. });
  87. });
  88. it('displays unlinked spools from Spoolman', async () => {
  89. render(<LinkSpoolModal {...defaultProps} />);
  90. await waitFor(() => {
  91. // Should show spools from getUnlinkedSpools
  92. expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
  93. expect(screen.getByText(/Bambu PETG Blue/)).toBeInTheDocument();
  94. });
  95. });
  96. it('does not render when isOpen is false', () => {
  97. render(<LinkSpoolModal {...defaultProps} isOpen={false} />);
  98. expect(screen.queryByRole('heading', { name: /select spool/i })).not.toBeInTheDocument();
  99. });
  100. });
  101. describe('linking', () => {
  102. it('uses trayUuid when linking if present (Bambu spool path)', async () => {
  103. render(<LinkSpoolModal {...defaultProps} />);
  104. await waitFor(() => {
  105. expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
  106. });
  107. fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
  108. await waitFor(() => {
  109. expect(api.linkSpool).toHaveBeenCalledWith(1, {
  110. spoolTag: 'A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4',
  111. printerId: 1,
  112. amsId: 0,
  113. trayId: 0,
  114. });
  115. });
  116. });
  117. it('falls back to tagUid when trayUuid is missing (generic spool path)', async () => {
  118. render(<LinkSpoolModal {...defaultProps} trayUuid="" />);
  119. await waitFor(() => {
  120. expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
  121. });
  122. fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
  123. await waitFor(() => {
  124. expect(api.linkSpool).toHaveBeenCalledWith(1, {
  125. spoolTag: 'ABCD1234',
  126. printerId: 1,
  127. amsId: 0,
  128. trayId: 0,
  129. });
  130. });
  131. });
  132. it('shows success toast and calls onClose', async () => {
  133. render(<LinkSpoolModal {...defaultProps} />);
  134. await waitFor(() => {
  135. expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
  136. });
  137. fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
  138. await waitFor(() => {
  139. expect(mockShowToast).toHaveBeenCalled();
  140. expect(defaultProps.onClose).toHaveBeenCalled();
  141. });
  142. });
  143. it('shows error toast on failure', async () => {
  144. vi.mocked(api.linkSpool).mockRejectedValue(new Error('Link failed'));
  145. render(<LinkSpoolModal {...defaultProps} />);
  146. await waitFor(() => {
  147. expect(screen.getByText(/Generic PLA Red/)).toBeInTheDocument();
  148. });
  149. fireEvent.click(screen.getByText(/Generic PLA Red/).closest('button')!);
  150. await waitFor(() => {
  151. expect(mockShowToast).toHaveBeenCalledWith(
  152. expect.stringContaining('Link failed'),
  153. 'error'
  154. );
  155. });
  156. });
  157. });
  158. describe('modal actions', () => {
  159. it('calls onClose when backdrop is clicked', async () => {
  160. render(<LinkSpoolModal {...defaultProps} />);
  161. await waitFor(() => {
  162. expect(screen.getByRole('heading', { name: /select spool/i })).toBeInTheDocument();
  163. });
  164. const backdrop = document.querySelector('.bg-black\\/60');
  165. if (backdrop) {
  166. fireEvent.click(backdrop);
  167. expect(defaultProps.onClose).toHaveBeenCalled();
  168. }
  169. });
  170. it('calls onClose when X button is clicked', async () => {
  171. render(<LinkSpoolModal {...defaultProps} />);
  172. await waitFor(() => {
  173. expect(screen.getByRole('heading', { name: /select spool/i })).toBeInTheDocument();
  174. });
  175. const closeButtons = screen.getAllByRole('button');
  176. const xButton = closeButtons.find(btn => btn.querySelector('svg.lucide-x'));
  177. if (xButton) {
  178. fireEvent.click(xButton);
  179. expect(defaultProps.onClose).toHaveBeenCalled();
  180. }
  181. });
  182. });
  183. });