Преглед на файлове

test(web): CameraImage (basic)

Testing Image and Canvas calls requires a lot of heavy dependencies, so this skips that part of the tests
Paul Armstrong преди 4 години
родител
ревизия
1aa9a7a093
променени са 4 файла, в които са добавени 73 реда и са изтрити 38 реда
  1. 3 16
      web/src/components/CameraImage.jsx
  2. 36 0
      web/src/components/__tests__/CameraImage.test.jsx
  3. 30 0
      web/src/hooks/index.jsx
  4. 4 22
      web/src/routes/CameraMap.jsx

+ 3 - 16
web/src/components/CameraImage.jsx

@@ -2,32 +2,19 @@ import { h } from 'preact';
 import ActivityIndicator from './ActivityIndicator';
 import { useApiHost, useConfig } from '../api';
 import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
+import { useResizeObserver } from '../hooks';
 
 export default function CameraImage({ camera, onload, searchParams = '' }) {
   const { data: config } = useConfig();
   const apiHost = useApiHost();
-  const [availableWidth, setAvailableWidth] = useState(0);
   const [hasLoaded, setHasLoaded] = useState(false);
   const containerRef = useRef(null);
   const canvasRef = useRef(null);
+  const [{ width: availableWidth }] = useResizeObserver(containerRef);
 
   const { name, width, height } = config.cameras[camera];
   const aspectRatio = width / height;
 
-  const resizeObserver = useMemo(() => {
-    return new ResizeObserver((entries) => {
-      window.requestAnimationFrame(() => {
-        if (Array.isArray(entries) && entries.length) {
-          setAvailableWidth(entries[0].contentRect.width);
-        }
-      });
-    });
-  }, []);
-
-  useEffect(() => {
-    resizeObserver.observe(containerRef.current);
-  }, [resizeObserver, containerRef]);
-
   const scaledHeight = useMemo(() => Math.min(Math.ceil(availableWidth / aspectRatio), height), [
     availableWidth,
     aspectRatio,
@@ -57,7 +44,7 @@ export default function CameraImage({ camera, onload, searchParams = '' }) {
 
   return (
     <div className="relative w-full" ref={containerRef}>
-      <canvas height={scaledHeight} ref={canvasRef} width={scaledWidth} />
+      <canvas data-testid="cameraimage-canvas" height={scaledHeight} ref={canvasRef} width={scaledWidth} />
       {!hasLoaded ? (
         <div className="absolute inset-0 flex justify-center" style={`height: ${scaledHeight}px`}>
           <ActivityIndicator />

+ 36 - 0
web/src/components/__tests__/CameraImage.test.jsx

@@ -0,0 +1,36 @@
+import { h } from 'preact';
+import * as Api from '../../api';
+import * as Hooks from '../../hooks';
+import CameraImage from '../CameraImage';
+import { render, screen } from '@testing-library/preact';
+
+jest.mock('../../api/baseUrl');
+
+describe('CameraImage', () => {
+  beforeEach(() => {
+    jest.spyOn(Api, 'useConfig').mockImplementation(() => {
+      return { data: { cameras: { front: { name: 'front', width: 1280, height: 720 } } } };
+    });
+    jest.spyOn(Api, 'useApiHost').mockReturnValue('http://base-url.local:5000');
+    jest.spyOn(Hooks, 'useResizeObserver').mockImplementation(() => [{ width: 0 }]);
+  });
+
+  test('renders an activity indicator while loading', async () => {
+    render(<CameraImage camera="front" />);
+    expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
+  });
+
+  test('creates a scaled canvas using the available width & height, preserving camera aspect ratio', async () => {
+    jest.spyOn(Hooks, 'useResizeObserver').mockReturnValueOnce([{ width: 720 }]);
+
+    render(<CameraImage camera="front" />);
+    expect(screen.queryByLabelText('Loading…')).toBeInTheDocument();
+    expect(screen.queryByTestId('cameraimage-canvas')).toMatchInlineSnapshot(`
+      <canvas
+        data-testid="cameraimage-canvas"
+        height="405"
+        width="720"
+      />
+    `);
+  });
+});

+ 30 - 0
web/src/hooks/index.jsx

@@ -0,0 +1,30 @@
+import { useEffect, useMemo, useState } from 'preact/hooks';
+
+export function useResizeObserver(...refs) {
+  const [dimensions, setDimensions] = useState(
+    new Array(refs.length).fill({ width: 0, height: 0, x: -Infinity, y: -Infinity })
+  );
+  const resizeObserver = useMemo(
+    () =>
+      new ResizeObserver((entries) => {
+        window.requestAnimationFrame(() => {
+          setDimensions(entries.map((entry) => entry.contentRect));
+        });
+      }),
+    []
+  );
+
+  useEffect(() => {
+    refs.forEach((ref) => {
+      resizeObserver.observe(ref.current);
+    });
+
+    return () => {
+      refs.forEach((ref) => {
+        resizeObserver.unobserve(ref.current);
+      });
+    };
+  }, [refs, resizeObserver]);
+
+  return dimensions;
+}

+ 4 - 22
web/src/routes/CameraMap.jsx

@@ -3,14 +3,14 @@ import Card from '../components/Card.jsx';
 import Button from '../components/Button.jsx';
 import Heading from '../components/Heading.jsx';
 import Switch from '../components/Switch.jsx';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
+import { useResizeObserver } from '../hooks';
+import { useCallback, useMemo, useRef, useState } from 'preact/hooks';
 import { useApiHost, useConfig } from '../api';
 
 export default function CameraMasks({ camera, url }) {
   const { data: config } = useConfig();
   const apiHost = useApiHost();
   const imageRef = useRef(null);
-  const [imageScale, setImageScale] = useState(1);
   const [snap, setSnap] = useState(true);
 
   const cameraConfig = config.cameras[camera];
@@ -22,26 +22,8 @@ export default function CameraMasks({ camera, url }) {
     zones,
   } = cameraConfig;
 
-  const resizeObserver = useMemo(
-    () =>
-      new ResizeObserver((entries) => {
-        window.requestAnimationFrame(() => {
-          if (Array.isArray(entries) && entries.length) {
-            const scaledWidth = entries[0].contentRect.width;
-            const scale = scaledWidth / width;
-            setImageScale(scale);
-          }
-        });
-      }),
-    [width, setImageScale]
-  );
-
-  useEffect(() => {
-    if (!imageRef.current) {
-      return;
-    }
-    resizeObserver.observe(imageRef.current);
-  }, [resizeObserver, imageRef]);
+  const [{ width: scaledWidth }] = useResizeObserver(imageRef);
+  const imageScale = scaledWidth / width;
 
   const [motionMaskPoints, setMotionMaskPoints] = useState(
     Array.isArray(motionMask)