SpoolBuddySettings.test.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. /**
  2. * Tests for the SpoolBuddySettings component.
  3. *
  4. * Covers:
  5. * - Lists all devices (not just the first), including stale duplicates
  6. * - Shows a duplicate warning when more than one device is registered
  7. * - Unregister button opens a confirm modal and calls the delete API
  8. */
  9. import { describe, it, expect, vi, beforeEach } from 'vitest';
  10. import { screen, waitFor } from '@testing-library/react';
  11. import userEvent from '@testing-library/user-event';
  12. import { render } from '../utils';
  13. import { SpoolBuddySettings } from '../../components/SpoolBuddySettings';
  14. vi.mock('../../api/client', async (importOriginal) => {
  15. const actual = await importOriginal<typeof import('../../api/client')>();
  16. return {
  17. ...actual,
  18. spoolbuddyApi: {
  19. ...actual.spoolbuddyApi,
  20. getDevices: vi.fn(),
  21. deleteDevice: vi.fn(),
  22. },
  23. };
  24. });
  25. import { spoolbuddyApi } from '../../api/client';
  26. const baseDevice = {
  27. id: 1,
  28. device_id: 'sb-0001',
  29. hostname: 'spoolbuddy-kitchen',
  30. ip_address: '10.0.0.11',
  31. backend_url: null,
  32. firmware_version: '1.2.0',
  33. has_nfc: true,
  34. has_scale: true,
  35. tare_offset: 0,
  36. calibration_factor: 1.0,
  37. nfc_reader_type: 'pn532',
  38. nfc_connection: 'i2c',
  39. display_brightness: 100,
  40. display_blank_timeout: 0,
  41. has_backlight: true,
  42. last_calibrated_at: null,
  43. last_seen: new Date().toISOString(),
  44. pending_command: null,
  45. nfc_ok: true,
  46. scale_ok: true,
  47. uptime_s: 3600,
  48. update_status: null,
  49. update_message: null,
  50. system_stats: {
  51. os: { os: 'Raspbian', kernel: '6.1', arch: 'aarch64', python: '3.11' },
  52. cpu_temp_c: 45.2,
  53. memory: { total_mb: 4000, available_mb: 2500, used_mb: 1500, percent: 37 },
  54. disk: { total_gb: 32, used_gb: 8, free_gb: 24, percent: 25 },
  55. system_uptime_s: 86400,
  56. },
  57. online: true,
  58. };
  59. describe('SpoolBuddySettings', () => {
  60. beforeEach(() => {
  61. vi.clearAllMocks();
  62. vi.mocked(spoolbuddyApi.deleteDevice).mockResolvedValue({ status: 'deleted', device_id: 'sb-0002' });
  63. });
  64. it('renders every registered device, not just the first', async () => {
  65. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([
  66. { ...baseDevice, id: 1, device_id: 'sb-0001', hostname: 'spoolbuddy-kitchen' },
  67. { ...baseDevice, id: 2, device_id: 'sb-0002', hostname: 'spoolbuddy-ghost', online: false },
  68. ]);
  69. render(<SpoolBuddySettings />);
  70. expect(await screen.findByText('spoolbuddy-kitchen')).toBeInTheDocument();
  71. expect(await screen.findByText('spoolbuddy-ghost')).toBeInTheDocument();
  72. expect(screen.getByText('sb-0001')).toBeInTheDocument();
  73. expect(screen.getByText('sb-0002')).toBeInTheDocument();
  74. });
  75. it('shows duplicate warning when multiple devices registered', async () => {
  76. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([
  77. { ...baseDevice, id: 1, device_id: 'sb-0001', hostname: 'spoolbuddy-kitchen' },
  78. { ...baseDevice, id: 2, device_id: 'sb-0002', hostname: 'spoolbuddy-ghost' },
  79. ]);
  80. render(<SpoolBuddySettings />);
  81. // Warning text mentions device count
  82. expect(await screen.findByText(/2 devices registered/i)).toBeInTheDocument();
  83. });
  84. it('does not show duplicate warning with a single device', async () => {
  85. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([
  86. { ...baseDevice, id: 1, device_id: 'sb-0001', hostname: 'spoolbuddy-kitchen' },
  87. ]);
  88. render(<SpoolBuddySettings />);
  89. await screen.findByText('spoolbuddy-kitchen');
  90. expect(screen.queryByText(/devices registered/i)).not.toBeInTheDocument();
  91. });
  92. it('opens confirm modal and unregisters device on confirm', async () => {
  93. const user = userEvent.setup();
  94. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([
  95. { ...baseDevice, id: 1, device_id: 'sb-0001', hostname: 'spoolbuddy-kitchen' },
  96. { ...baseDevice, id: 2, device_id: 'sb-0002', hostname: 'spoolbuddy-ghost', online: false },
  97. ]);
  98. render(<SpoolBuddySettings />);
  99. // Wait for both devices to render
  100. await screen.findByText('spoolbuddy-ghost');
  101. // Click the unregister button on the ghost device card
  102. const unregisterButtons = screen.getAllByRole('button', { name: /unregister/i });
  103. // Two unregister buttons (one per card) — click the second one (ghost)
  104. await user.click(unregisterButtons[1]);
  105. // Confirm modal opens with title
  106. expect(await screen.findByText(/unregister spoolbuddy device/i)).toBeInTheDocument();
  107. // Click the confirm button inside the modal
  108. const confirmButtons = screen.getAllByRole('button', { name: /^unregister$/i });
  109. // Last one will be the modal's confirm button
  110. await user.click(confirmButtons[confirmButtons.length - 1]);
  111. await waitFor(() => {
  112. expect(spoolbuddyApi.deleteDevice).toHaveBeenCalledWith('sb-0002');
  113. });
  114. });
  115. it('does not call delete API when user cancels confirm modal', async () => {
  116. const user = userEvent.setup();
  117. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([
  118. { ...baseDevice, id: 1, device_id: 'sb-0001', hostname: 'spoolbuddy-kitchen' },
  119. ]);
  120. render(<SpoolBuddySettings />);
  121. await screen.findByText('spoolbuddy-kitchen');
  122. const unregisterButton = screen.getByRole('button', { name: /unregister/i });
  123. await user.click(unregisterButton);
  124. const cancelButton = await screen.findByRole('button', { name: /cancel/i });
  125. await user.click(cancelButton);
  126. expect(spoolbuddyApi.deleteDevice).not.toHaveBeenCalled();
  127. });
  128. it('shows empty state when no devices are registered', async () => {
  129. vi.mocked(spoolbuddyApi.getDevices).mockResolvedValue([]);
  130. render(<SpoolBuddySettings />);
  131. expect(await screen.findByText(/no spoolbuddy devices/i)).toBeInTheDocument();
  132. });
  133. });