From 1aa9a7a093f1e8e80af10152dc0997bddf2562eb Mon Sep 17 00:00:00 2001 From: Paul Armstrong Date: Fri, 12 Feb 2021 08:23:58 -0800 Subject: [PATCH] test(web): CameraImage (basic) Testing Image and Canvas calls requires a lot of heavy dependencies, so this skips that part of the tests --- web/src/components/CameraImage.jsx | 19 ++-------- .../components/__tests__/CameraImage.test.jsx | 36 +++++++++++++++++++ web/src/hooks/index.jsx | 30 ++++++++++++++++ web/src/routes/CameraMap.jsx | 26 +++----------- 4 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 web/src/components/__tests__/CameraImage.test.jsx create mode 100644 web/src/hooks/index.jsx diff --git a/web/src/components/CameraImage.jsx b/web/src/components/CameraImage.jsx index 21e041f3c..a609ac916 100644 --- a/web/src/components/CameraImage.jsx +++ b/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 (
- + {!hasLoaded ? (
diff --git a/web/src/components/__tests__/CameraImage.test.jsx b/web/src/components/__tests__/CameraImage.test.jsx new file mode 100644 index 000000000..7228a1137 --- /dev/null +++ b/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(); + 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(); + expect(screen.queryByLabelText('Loading…')).toBeInTheDocument(); + expect(screen.queryByTestId('cameraimage-canvas')).toMatchInlineSnapshot(` + + `); + }); +}); diff --git a/web/src/hooks/index.jsx b/web/src/hooks/index.jsx new file mode 100644 index 000000000..385313cba --- /dev/null +++ b/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; +} diff --git a/web/src/routes/CameraMap.jsx b/web/src/routes/CameraMap.jsx index d169f26a0..58b2e64bd 100644 --- a/web/src/routes/CameraMap.jsx +++ b/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)