/**
* 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(
} />
);
}
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
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();
});
});
});
});