SpoolInfoCard.test.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import { describe, it, expect, vi, beforeEach } from 'vitest';
  2. import { screen, fireEvent, waitFor } from '@testing-library/react';
  3. import { render } from '../utils';
  4. import { SpoolInfoCard, UnknownTagCard } from '../../components/spoolbuddy/SpoolInfoCard';
  5. import type { MatchedSpool } from '../../hooks/useSpoolBuddyState';
  6. const mockUpdateSpoolWeight = vi.fn();
  7. vi.mock('../../api/client', () => ({
  8. api: {
  9. getSettings: vi.fn().mockResolvedValue({}),
  10. getAuthStatus: vi.fn().mockResolvedValue({ auth_enabled: false }),
  11. },
  12. spoolbuddyApi: {
  13. updateSpoolWeight: (...args: unknown[]) => mockUpdateSpoolWeight(...args),
  14. },
  15. }));
  16. const mockSpool: MatchedSpool = {
  17. id: 42,
  18. tag_uid: 'AABBCCDD11223344',
  19. material: 'PLA',
  20. subtype: 'Matte',
  21. color_name: 'Jade White',
  22. rgba: 'E8F5E9FF',
  23. brand: 'Bambu',
  24. label_weight: 1000,
  25. core_weight: 250,
  26. weight_used: 200,
  27. };
  28. describe('SpoolInfoCard', () => {
  29. beforeEach(() => {
  30. vi.clearAllMocks();
  31. mockUpdateSpoolWeight.mockResolvedValue({ status: 'ok', weight_used: 300 });
  32. });
  33. it('renders spool material, brand, color name', () => {
  34. render(<SpoolInfoCard spool={mockSpool} scaleWeight={null} />);
  35. expect(screen.getByText('Jade White')).toBeInTheDocument();
  36. expect(screen.getByText(/Bambu/)).toBeInTheDocument();
  37. expect(screen.getByText(/PLA/)).toBeInTheDocument();
  38. });
  39. it('shows spool color circle with correct hex color', () => {
  40. const { container } = render(<SpoolInfoCard spool={mockSpool} scaleWeight={null} />);
  41. // SpoolIcon renders an SVG circle with fill=colorHex
  42. const circle = container.querySelector('circle[fill="#E8F5E9"]');
  43. expect(circle).toBeInTheDocument();
  44. });
  45. it('shows remaining weight and fill percentage', () => {
  46. // scaleWeight=900g, core=250g → remaining = 900-250 = 650g
  47. // fillPercent = round(650/1000 * 100) = 65%
  48. render(<SpoolInfoCard spool={mockSpool} scaleWeight={900} />);
  49. expect(screen.getByText('650g')).toBeInTheDocument();
  50. expect(screen.getByText('65%')).toBeInTheDocument();
  51. });
  52. it('calls onAssignToAms when "Assign to AMS" button clicked', () => {
  53. const onAssign = vi.fn();
  54. render(
  55. <SpoolInfoCard spool={mockSpool} scaleWeight={800} onAssignToAms={onAssign} />
  56. );
  57. fireEvent.click(screen.getByText('Assign to AMS'));
  58. expect(onAssign).toHaveBeenCalledTimes(1);
  59. });
  60. it('calls onSyncWeight when sync button clicked', async () => {
  61. const onSync = vi.fn();
  62. render(
  63. <SpoolInfoCard spool={mockSpool} scaleWeight={800} onSyncWeight={onSync} />
  64. );
  65. fireEvent.click(screen.getByText('Sync Weight'));
  66. await waitFor(() => {
  67. expect(mockUpdateSpoolWeight).toHaveBeenCalledWith(42, 800);
  68. });
  69. });
  70. it('calls onClose when close button clicked', () => {
  71. const onClose = vi.fn();
  72. render(
  73. <SpoolInfoCard spool={mockSpool} scaleWeight={null} onClose={onClose} />
  74. );
  75. fireEvent.click(screen.getByText('Close'));
  76. expect(onClose).toHaveBeenCalledTimes(1);
  77. });
  78. it('disables "Assign to AMS" button when isAssigned=true', () => {
  79. render(
  80. <SpoolInfoCard spool={mockSpool} scaleWeight={800} onAssignToAms={vi.fn()} isAssigned />
  81. );
  82. expect(screen.getByText('Assign to AMS')).toBeDisabled();
  83. });
  84. it('enables "Assign to AMS" button when isAssigned=false', () => {
  85. render(
  86. <SpoolInfoCard spool={mockSpool} scaleWeight={800} onAssignToAms={vi.fn()} isAssigned={false} />
  87. );
  88. expect(screen.getByText('Assign to AMS')).not.toBeDisabled();
  89. });
  90. it('shows Unassign button when isAssigned=true and onUnassignFromAms is provided', () => {
  91. render(
  92. <SpoolInfoCard
  93. spool={mockSpool}
  94. scaleWeight={800}
  95. onAssignToAms={vi.fn()}
  96. isAssigned
  97. onUnassignFromAms={vi.fn()}
  98. />
  99. );
  100. expect(screen.getByText(/unassign/i)).toBeInTheDocument();
  101. });
  102. it('does not show Unassign button when onUnassignFromAms is not provided', () => {
  103. render(
  104. <SpoolInfoCard spool={mockSpool} scaleWeight={800} onAssignToAms={vi.fn()} isAssigned />
  105. );
  106. expect(screen.queryByText(/unassign/i)).not.toBeInTheDocument();
  107. });
  108. it('calls onUnassignFromAms when Unassign button is clicked', () => {
  109. const onUnassign = vi.fn();
  110. render(
  111. <SpoolInfoCard
  112. spool={mockSpool}
  113. scaleWeight={800}
  114. onAssignToAms={vi.fn()}
  115. isAssigned
  116. onUnassignFromAms={onUnassign}
  117. />
  118. );
  119. fireEvent.click(screen.getByText(/unassign/i));
  120. expect(onUnassign).toHaveBeenCalledTimes(1);
  121. });
  122. });
  123. describe('UnknownTagCard', () => {
  124. it('renders tag UID', () => {
  125. render(<UnknownTagCard tagUid="DEADBEEF12345678" scaleWeight={null} />);
  126. expect(screen.getByText('DEADBEEF12345678')).toBeInTheDocument();
  127. expect(screen.getByText('New Tag Detected')).toBeInTheDocument();
  128. });
  129. it('shows "Add to Inventory" button', () => {
  130. const onAdd = vi.fn();
  131. render(
  132. <UnknownTagCard tagUid="DEADBEEF" scaleWeight={null} onAddToInventory={onAdd} />
  133. );
  134. const btn = screen.getByText('Add to Inventory');
  135. expect(btn).toBeInTheDocument();
  136. fireEvent.click(btn);
  137. expect(onAdd).toHaveBeenCalledTimes(1);
  138. });
  139. });