SettingsPageUsbCameraWiring.test.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. /**
  2. * Settings > Camera: a USB camera is picked from the video devices the server
  3. * lists, a saved device that is unplugged stays selected, and the path can
  4. * still be typed by hand.
  5. */
  6. import { describe, it, expect, beforeEach } from 'vitest';
  7. import { screen, waitFor, within } from '@testing-library/react';
  8. import userEvent from '@testing-library/user-event';
  9. import { http, HttpResponse } from 'msw';
  10. import { render } from '../utils';
  11. import { server } from '../mocks/server';
  12. import { setAuthToken } from '../../api/client';
  13. import { SettingsPage } from '../../pages/SettingsPage';
  14. const PRINTER = {
  15. id: 7,
  16. name: 'Bench',
  17. serial_number: 'TEST123',
  18. ip_address: '192.168.1.100',
  19. access_code: 'XXXX',
  20. model: 'P1S',
  21. location: null,
  22. nozzle_count: 1,
  23. is_active: true,
  24. auto_archive: true,
  25. external_camera_url: '/dev/video0',
  26. external_camera_type: 'usb',
  27. external_camera_enabled: true,
  28. external_camera_snapshot_url: null,
  29. camera_rotation: 0,
  30. plate_detection_enabled: false,
  31. created_at: '2026-01-01T00:00:00Z',
  32. updated_at: '2026-01-01T00:00:00Z',
  33. };
  34. const WEBCAM = { device: '/dev/video0', name: 'HD Pro Webcam C920', capabilities: ['capture'] };
  35. const SECOND = { device: '/dev/video2', name: 'USB Camera', capabilities: ['capture'] };
  36. let listCalls: number;
  37. let cameras: typeof WEBCAM[];
  38. let patches: Record<string, unknown>[];
  39. function setup(printer: Record<string, unknown> = PRINTER) {
  40. server.use(
  41. http.get('/api/v1/printers/', () => HttpResponse.json([printer])),
  42. http.get('/api/v1/printers/usb-cameras', () => {
  43. listCalls += 1;
  44. return HttpResponse.json({ cameras });
  45. }),
  46. http.patch('/api/v1/printers/7', async ({ request }) => {
  47. const body = (await request.json()) as Record<string, unknown>;
  48. patches.push(body);
  49. return HttpResponse.json({ ...printer, ...body });
  50. }),
  51. );
  52. }
  53. async function openCamera() {
  54. render(<SettingsPage />);
  55. await userEvent.setup().click(await screen.findByLabelText('Camera settings: Bench'));
  56. }
  57. const picker = () => screen.findByLabelText('USB device: Bench') as Promise<HTMLSelectElement>;
  58. beforeEach(() => {
  59. window.history.replaceState({}, '', '/?tab=camera');
  60. setAuthToken(null);
  61. listCalls = 0;
  62. cameras = [WEBCAM, SECOND];
  63. patches = [];
  64. });
  65. describe('USB camera device picker', () => {
  66. it('lists the server\'s video devices for a USB camera', async () => {
  67. setup();
  68. await openCamera();
  69. const select = await picker();
  70. await waitFor(() => expect(within(select).getByRole('option', { name: 'HD Pro Webcam C920 (/dev/video0)' })).toBeInTheDocument());
  71. expect(within(select).getByRole('option', { name: 'USB Camera (/dev/video2)' })).toBeInTheDocument();
  72. expect(select.value).toBe('/dev/video0');
  73. expect(screen.queryByPlaceholderText('Device path (/dev/video0)')).not.toBeInTheDocument();
  74. });
  75. it('saves the picked device as the camera path', async () => {
  76. setup();
  77. await openCamera();
  78. const select = await picker();
  79. await screen.findByRole('option', { name: 'USB Camera (/dev/video2)' });
  80. await userEvent.setup().selectOptions(select, '/dev/video2');
  81. await waitFor(() => expect(patches).toContainEqual({ external_camera_url: '/dev/video2' }), { timeout: 3000 });
  82. });
  83. it('keeps a saved device that is not connected, marked not found', async () => {
  84. cameras = [SECOND];
  85. setup();
  86. await openCamera();
  87. const select = await picker();
  88. await screen.findByRole('option', { name: '/dev/video0 (not found)' });
  89. expect(select.value).toBe('/dev/video0');
  90. expect(patches).toEqual([]);
  91. });
  92. it('says how to pass a camera to Docker when no device is visible', async () => {
  93. cameras = [];
  94. setup();
  95. await openCamera();
  96. expect(await screen.findByText(/No video devices are visible to Bambuddy/)).toBeInTheDocument();
  97. expect(screen.getByText('devices: - /dev/video0:/dev/video0')).toBeInTheDocument();
  98. });
  99. it('lets the path be typed by hand, and goes back to the list', async () => {
  100. setup();
  101. await openCamera();
  102. await picker();
  103. const user = userEvent.setup();
  104. await user.click(screen.getByRole('button', { name: 'Enter path manually' }));
  105. const input = screen.getByPlaceholderText('Device path (/dev/video0)') as HTMLInputElement;
  106. expect(input.value).toBe('/dev/video0');
  107. expect(screen.queryByLabelText('USB device: Bench')).not.toBeInTheDocument();
  108. await user.click(screen.getByRole('button', { name: 'Choose a detected device' }));
  109. expect(await picker()).toBeInTheDocument();
  110. });
  111. it('looks for devices again on refresh', async () => {
  112. setup();
  113. await openCamera();
  114. await screen.findByRole('option', { name: 'USB Camera (/dev/video2)' });
  115. const before = listCalls;
  116. cameras = [WEBCAM, SECOND, { device: '/dev/video4', name: 'Plugged in later', capabilities: ['capture'] }];
  117. await userEvent.setup().click(screen.getByRole('button', { name: 'Look for devices again' }));
  118. expect(await screen.findByRole('option', { name: 'Plugged in later (/dev/video4)' })).toBeInTheDocument();
  119. expect(listCalls).toBeGreaterThan(before);
  120. });
  121. it('does not list devices for a camera that is not USB', async () => {
  122. setup({ ...PRINTER, external_camera_type: 'mjpeg', external_camera_url: 'http://192.168.1.61/stream' });
  123. await openCamera();
  124. expect(await screen.findByPlaceholderText(/Camera URL/i)).toBeInTheDocument();
  125. expect(screen.queryByLabelText('USB device: Bench')).not.toBeInTheDocument();
  126. expect(listCalls).toBe(0);
  127. });
  128. });