UpdatedStreamOverlay.test.tsx 3.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364
  1. import { act, render } from '@testing-library/react';
  2. import { afterEach, describe, expect, it, vi } from 'vitest';
  3. import { UpdatedStreamOverlay } from '../../components/UpdatedStreamOverlay';
  4. const props = {
  5. size: 'medium' as const, camera: null, name: 'Workshop', model: null,
  6. filename: null, status: null, state: null, progress: null, layers: null,
  7. remaining: null, eta: null, temperatures: [],
  8. };
  9. afterEach(() => vi.restoreAllMocks());
  10. describe('Version 2 orientation', () => {
  11. it('follows viewport changes for legacy URLs and removes its listener on unmount', () => {
  12. let change: ((event: { matches: boolean }) => void) | undefined;
  13. const removeEventListener = vi.fn();
  14. vi.spyOn(window, 'matchMedia').mockReturnValue({
  15. matches: true,
  16. addEventListener: (_: string, listener: typeof change) => { change = listener; },
  17. removeEventListener,
  18. } as unknown as MediaQueryList);
  19. const view = render(<UpdatedStreamOverlay {...props} />);
  20. const overlay = view.container.firstElementChild;
  21. expect(overlay).toHaveAttribute('data-layout', 'portrait');
  22. act(() => change?.({ matches: false }));
  23. expect(overlay).toHaveAttribute('data-layout', 'landscape');
  24. act(() => change?.({ matches: true }));
  25. expect(overlay).toHaveAttribute('data-layout', 'portrait');
  26. view.unmount();
  27. expect(removeEventListener).toHaveBeenCalledWith('change', change);
  28. });
  29. it.each(['landscape', 'portrait'] as const)('keeps explicit %s independent of viewport orientation', (layout) => {
  30. vi.spyOn(window, 'matchMedia').mockReturnValue({ matches: layout !== 'portrait' } as MediaQueryList);
  31. const view = render(<UpdatedStreamOverlay {...props} layout={layout} />);
  32. expect(view.container.firstElementChild).toHaveAttribute('data-layout', layout);
  33. });
  34. });
  35. describe('Camera stream renewal', () => {
  36. it('renders the camera without a source while its stream is closed, and hands over the img', () => {
  37. const imgRef = { current: null as HTMLImageElement | null };
  38. const view = render(
  39. <UpdatedStreamOverlay {...props} camera={{ url: undefined, rotation: 0, onError: () => {}, imgRef }} />,
  40. );
  41. const img = view.container.querySelector('.updated-overlay__camera img');
  42. expect(img).not.toBeNull();
  43. expect(img?.getAttribute('src')).toBeNull();
  44. expect(imgRef.current).toBe(img);
  45. });
  46. it('switches the stream on the same img when its URL changes (a new stream token)', () => {
  47. // A replaced img would keep the old connection open.
  48. const camera = { rotation: 0, onError: () => {} };
  49. const view = render(<UpdatedStreamOverlay {...props} camera={{ ...camera, url: '/s?t=1&token=a' }} />);
  50. const img = view.container.querySelector('.updated-overlay__camera img');
  51. view.rerender(<UpdatedStreamOverlay {...props} camera={{ ...camera, url: '/s?t=1&token=b' }} />);
  52. const after = view.container.querySelector('.updated-overlay__camera img');
  53. expect(after).toBe(img);
  54. expect(after?.getAttribute('src')).toBe('/s?t=1&token=b');
  55. });
  56. });