| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- /**
- * Tests for the CameraPage component.
- */
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
- import { screen, waitFor, render as rtlRender } from '@testing-library/react';
- import { CameraPage } from '../../pages/CameraPage';
- import { http, HttpResponse } from 'msw';
- import { server } from '../mocks/server';
- import { MemoryRouter, Route, Routes } from 'react-router-dom';
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
- import { ThemeProvider } from '../../contexts/ThemeContext';
- import { ToastProvider } from '../../contexts/ToastContext';
- import { AuthProvider } from '../../contexts/AuthContext';
- import { I18nextProvider } from 'react-i18next';
- import i18n from '../../i18n';
- // Mock navigator.sendBeacon which isn't available in jsdom
- vi.stubGlobal('navigator', {
- ...navigator,
- sendBeacon: vi.fn().mockReturnValue(true),
- });
- const mockPrinter = {
- id: 1,
- name: 'X1 Carbon',
- ip_address: '192.168.1.100',
- serial_number: '00M09A350100001',
- access_code: '12345678',
- model: 'X1C',
- enabled: true,
- };
- // Custom render for CameraPage which needs specific route params
- function renderCameraPage(printerId: number, search = '') {
- const queryClient = new QueryClient({
- defaultOptions: {
- queries: { retry: false, gcTime: 0 },
- mutations: { retry: false },
- },
- });
- return rtlRender(
- <QueryClientProvider client={queryClient}>
- <I18nextProvider i18n={i18n}>
- <MemoryRouter initialEntries={[`/cameras/${printerId}${search}`]}>
- <ThemeProvider>
- <AuthProvider>
- <ToastProvider>
- <Routes>
- <Route path="/cameras/:printerId" element={<CameraPage />} />
- </Routes>
- </ToastProvider>
- </AuthProvider>
- </ThemeProvider>
- </MemoryRouter>
- </I18nextProvider>
- </QueryClientProvider>
- );
- }
- describe('CameraPage', () => {
- const originalTitle = document.title;
- beforeEach(() => {
- server.use(
- http.get('/api/v1/printers/:id', () => {
- return HttpResponse.json(mockPrinter);
- }),
- http.get('/api/v1/printers/:id/status', () => {
- return HttpResponse.json({
- connected: true,
- state: 'IDLE',
- progress: 0,
- });
- }),
- http.post('/api/v1/printers/:id/camera/stop', () => {
- return HttpResponse.json({ success: true });
- }),
- http.get('/api/v1/printers/:id/camera/status', () => {
- return HttpResponse.json({ active: true, stalled: false });
- })
- );
- });
- afterEach(() => {
- document.title = originalTitle;
- });
- describe('rendering', () => {
- it('renders camera page for printer', async () => {
- renderCameraPage(1);
- // Camera page should load - look for the header with camera icon
- await waitFor(() => {
- expect(screen.getByRole('heading')).toBeInTheDocument();
- });
- });
- it('shows live and snapshot mode buttons', async () => {
- renderCameraPage(1);
- await waitFor(() => {
- // Check for translation key or translated text
- expect(screen.getByText(/Live|camera\.live/)).toBeInTheDocument();
- expect(screen.getByText(/Snapshot|camera\.snapshot/)).toBeInTheDocument();
- });
- });
- it('shows printer name in header', async () => {
- renderCameraPage(1);
- await waitFor(() => {
- expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
- });
- });
- });
- describe('camera controls', () => {
- it('renders without crashing', async () => {
- renderCameraPage(1);
- // Just verify no crash during render
- await waitFor(() => {
- expect(document.body).toBeInTheDocument();
- });
- });
- it('shows the camera diagnostic (stethoscope) button in the control bar (#1395)', async () => {
- // The diagnostic shipped wired into the embedded viewer only; window mode
- // (this page) was missing it. The control-bar button must be present here.
- renderCameraPage(1);
- await waitFor(() => {
- expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
- });
- expect(screen.getByTitle('Diagnose')).toBeInTheDocument();
- });
- });
- describe('stream token handling (#979)', () => {
- it('does not render image src until stream token arrives when auth is enabled', async () => {
- let resolveToken!: (value: unknown) => void;
- const tokenPromise = new Promise((resolve) => {
- resolveToken = resolve;
- });
- server.use(
- http.get('*/api/v1/auth/status', () =>
- HttpResponse.json({ auth_enabled: true, requires_setup: false })
- ),
- http.post('*/api/v1/printers/camera/stream-token', async () => {
- await tokenPromise;
- return HttpResponse.json({ token: 'tok-abc' });
- })
- );
- renderCameraPage(1);
- // Before the token resolves the <img> should not have a src pointing at
- // the stream endpoint — otherwise the backend would 401 with the
- // "Valid camera stream token required" error from #979.
- await waitFor(() => {
- expect(screen.getByText('X1 Carbon')).toBeInTheDocument();
- });
- const img = document.querySelector('img') as HTMLImageElement | null;
- expect(img).not.toBeNull();
- expect(img?.getAttribute('src') || '').not.toContain('/camera/stream');
- resolveToken(undefined);
- // After the token resolves the image src picks it up as ?token=...
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain('/camera/stream');
- expect(src).toContain('token=tok-abc');
- });
- });
- it('renders image src immediately when auth is disabled (no token required)', async () => {
- renderCameraPage(1);
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain(`/api/v1/printers/1/camera/stream`);
- expect(src).not.toContain('token=');
- });
- });
- });
- describe('fps URL parameter (#1131)', () => {
- it('defaults to fps=15 when no query parameter is provided', async () => {
- renderCameraPage(1);
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain('fps=15');
- });
- });
- it('honors fps query parameter from URL', async () => {
- renderCameraPage(1, '?fps=5');
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain('fps=5');
- });
- });
- it('clamps fps above 30 to 30', async () => {
- renderCameraPage(1, '?fps=60');
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain('fps=30');
- });
- });
- it('clamps fps below 1 to 1', async () => {
- renderCameraPage(1, '?fps=0');
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain('fps=1');
- });
- });
- it('falls back to 15 for non-numeric fps', async () => {
- renderCameraPage(1, '?fps=invalid');
- await waitFor(() => {
- const src = (document.querySelector('img') as HTMLImageElement | null)?.getAttribute('src') || '';
- expect(src).toContain('fps=15');
- });
- });
- });
- describe('invalid printer', () => {
- it('shows invalid printer message for ID 0', async () => {
- renderCameraPage(0);
- await waitFor(() => {
- // Check for translation key or translated text
- expect(screen.getByText(/Invalid printer ID|camera\.invalidPrinterId/)).toBeInTheDocument();
- });
- });
- });
- });
|