Procházet zdrojové kódy

test(web): Switch (and add label back in)

Paul Armstrong před 4 roky
rodič
revize
5eaf8a5448

+ 14 - 3
web/src/components/Switch.jsx

@@ -1,7 +1,7 @@
 import { h } from 'preact';
 import { useCallback, useState } from 'preact/hooks';
 
-export default function Switch({ checked, id, onChange }) {
+export default function Switch({ checked, id, onChange, label, labelPosition = 'before' }) {
   const [isFocused, setFocused] = useState(false);
 
   const handleChange = useCallback(
@@ -24,15 +24,21 @@ export default function Switch({ checked, id, onChange }) {
   return (
     <label
       htmlFor={id}
-      className={`flex items-center justify-center ${onChange ? 'cursor-pointer' : 'cursor-not-allowed'}`}
+      className={`flex items-center space-x-4 w-full ${onChange ? 'cursor-pointer' : 'cursor-not-allowed'}`}
     >
+      {label && labelPosition === 'before' ? (
+        <div data-testid={`${id}-label`} className="inline-flex flex-grow">
+          {label}
+        </div>
+      ) : null}
       <div
         onMouseOver={handleFocus}
         onMouseOut={handleBlur}
-        className={`w-8 h-5 relative ${!onChange ? 'opacity-60' : ''}`}
+        className={`self-end w-8 h-5 relative ${!onChange ? 'opacity-60' : ''}`}
       >
         <div className="relative overflow-hidden">
           <input
+            data-testid={`${id}-input`}
             className="absolute left-48"
             onBlur={handleBlur}
             onFocus={handleFocus}
@@ -55,6 +61,11 @@ export default function Switch({ checked, id, onChange }) {
           style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}
         />
       </div>
+      {label && labelPosition !== 'before' ? (
+        <div data-testid={`${id}-label`} class="inline-flex flex-grow">
+          {label}
+        </div>
+      ) : null}
     </label>
   );
 }

+ 47 - 0
web/src/components/__tests__/Switch.test.jsx

@@ -0,0 +1,47 @@
+import { h } from 'preact';
+import Switch from '../Switch';
+import { fireEvent, render, screen } from '@testing-library/preact';
+
+describe('Switch', () => {
+  test('renders a hidden checkbox', async () => {
+    render(
+      <div>
+        <Switch id="unchecked-switch" />
+        <Switch id="checked-switch" checked={true} />
+      </div>
+    );
+
+    const unchecked = screen.queryByTestId('unchecked-switch-input');
+    expect(unchecked).toHaveAttribute('type', 'checkbox');
+    expect(unchecked).not.toBeChecked();
+
+    const checked = screen.queryByTestId('checked-switch-input');
+    expect(checked).toHaveAttribute('type', 'checkbox');
+    expect(checked).toBeChecked();
+  });
+
+  test('calls onChange callback when checked/unchecked', async () => {
+    const handleChange = jest.fn();
+    const { rerender } = render(<Switch id="check" onChange={handleChange} />);
+    fireEvent.change(screen.queryByTestId('check-input'), { checked: true });
+    expect(handleChange).toHaveBeenCalledWith('check', true);
+
+    rerender(<Switch id="check" onChange={handleChange} checked />);
+    fireEvent.change(screen.queryByTestId('check-input'), { checked: false });
+    expect(handleChange).toHaveBeenCalledWith('check', false);
+  });
+
+  test('renders a label before', async () => {
+    render(<Switch id="check" label="This is the label" />);
+    const items = screen.queryAllByTestId(/check-.+/);
+    expect(items[0]).toHaveTextContent('This is the label');
+    expect(items[1]).toHaveAttribute('data-testid', 'check-input');
+  });
+
+  test('renders a label after', async () => {
+    render(<Switch id="check" label="This is the label" labelPosition="after" />);
+    const items = screen.queryAllByTestId(/check-.+/);
+    expect(items[0]).toHaveAttribute('data-testid', 'check-input');
+    expect(items[1]).toHaveTextContent('This is the label');
+  });
+});

+ 30 - 24
web/src/routes/Camera.jsx

@@ -45,30 +45,36 @@ export default function Camera({ camera }) {
 
   const optionContent = showSettings ? (
     <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
-      <div className="flex space-x-3">
-        <Switch checked={options['bbox']} id="bbox" onChange={handleSetOption} />
-        <span className="inline-flex">Bounding box</span>
-      </div>
-      <div className="flex space-x-3">
-        <Switch checked={options['timestamp']} id="timestamp" onChange={handleSetOption} />
-        <span className="inline-flex">Timestamp</span>
-      </div>
-      <div className="flex space-x-3">
-        <Switch checked={options['zones']} id="zones" onChange={handleSetOption} />
-        <span className="inline-flex">Zones</span>
-      </div>
-      <div className="flex space-x-3">
-        <Switch checked={options['mask']} id="mask" onChange={handleSetOption} />
-        <span className="inline-flex">Masks</span>
-      </div>
-      <div className="flex space-x-3">
-        <Switch checked={options['motion']} id="motion" onChange={handleSetOption} />
-        <span className="inline-flex">Motion boxes</span>
-      </div>
-      <div className="flex space-x-3">
-        <Switch checked={options['regions']} id="regions" onChange={handleSetOption} />
-        <span className="inline-flex">Regions</span>
-      </div>
+      <Switch
+        checked={options['bbox']}
+        id="bbox"
+        onChange={handleSetOption}
+        label="Bounding box"
+        labelPosition="after"
+      />
+      <Switch
+        checked={options['timestamp']}
+        id="timestamp"
+        onChange={handleSetOption}
+        label="Timestamp"
+        labelPosition="after"
+      />
+      <Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
+      <Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Masks" labelPosition="after" />
+      <Switch
+        checked={options['motion']}
+        id="motion"
+        onChange={handleSetOption}
+        label="Motion boxes"
+        labelPosition="after"
+      />
+      <Switch
+        checked={options['regions']}
+        id="regions"
+        onChange={handleSetOption}
+        label="Regions"
+        labelPosition="after"
+      />
       <Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
     </div>
   ) : null;

+ 26 - 26
web/src/routes/CameraMap.jsx

@@ -29,8 +29,8 @@ export default function CameraMasks({ camera, url }) {
     Array.isArray(motionMask)
       ? motionMask.map((mask) => getPolylinePoints(mask))
       : motionMask
-        ? [getPolylinePoints(motionMask)]
-        : []
+      ? [getPolylinePoints(motionMask)]
+      : []
   );
 
   const [zonePoints, setZonePoints] = useState(
@@ -44,8 +44,8 @@ export default function CameraMasks({ camera, url }) {
         [name]: Array.isArray(objectFilters[name].mask)
           ? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
           : objectFilters[name].mask
-            ? [getPolylinePoints(objectFilters[name].mask)]
-            : [],
+          ? [getPolylinePoints(objectFilters[name].mask)]
+          : [],
       }),
       {}
     )
@@ -128,11 +128,11 @@ ${motionMaskPoints.map((mask, i) => `      - ${polylinePointsToPolyline(mask)}`)
   const handleCopyZones = useCallback(async () => {
     await window.navigator.clipboard.writeText(`  zones:
 ${Object.keys(zonePoints)
-    .map(
-      (zoneName) => `    ${zoneName}:
+  .map(
+    (zoneName) => `    ${zoneName}:
       coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
-    )
-    .join('\n')}`);
+  )
+  .join('\n')}`);
   }, [zonePoints]);
 
   // Object methods
@@ -164,14 +164,14 @@ ${Object.keys(zonePoints)
     await window.navigator.clipboard.writeText(`  objects:
     filters:
 ${Object.keys(objectMaskPoints)
-    .map((objectName) =>
-      objectMaskPoints[objectName].length
-        ? `      ${objectName}:
+  .map((objectName) =>
+    objectMaskPoints[objectName].length
+      ? `      ${objectName}:
         mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
-        : ''
-    )
-    .filter(Boolean)
-    .join('\n')}`);
+      : ''
+  )
+  .filter(Boolean)
+  .join('\n')}`);
   }, [objectMaskPoints]);
 
   const handleAddToObjectMask = useCallback(
@@ -222,8 +222,8 @@ ${Object.keys(objectMaskPoints)
             height={height}
           />
         </div>
-        <div className="flex space-x-4">
-          <span>Snap to edges</span> <Switch checked={snap} onChange={handleChangeSnap} />
+        <div className="max-w-xs">
+          <Switch checked={snap} label="Snap to edges" labelPosition="after" onChange={handleChangeSnap} />
         </div>
       </div>
 
@@ -360,15 +360,15 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
       {!scaledPoints
         ? null
         : scaledPoints.map(([x, y], i) => (
-          <PolyPoint
-            boundingRef={boundingRef}
-            index={i}
-            onMove={handleMovePoint}
-            onRemove={handleRemovePoint}
-            x={x + MaskInset}
-            y={y + MaskInset}
-          />
-        ))}
+            <PolyPoint
+              boundingRef={boundingRef}
+              index={i}
+              onMove={handleMovePoint}
+              onRemove={handleRemovePoint}
+              x={x + MaskInset}
+              y={y + MaskInset}
+            />
+          ))}
       <div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
       <svg
         width="100%"

+ 21 - 18
web/src/routes/StyleGuide.jsx

@@ -9,7 +9,7 @@ import TextField from '../components/TextField';
 import { useCallback, useState } from 'preact/hooks';
 
 export default function StyleGuide() {
-  const [switches, setSwitches] = useState({ 0: false, 1: true });
+  const [switches, setSwitches] = useState({ 0: false, 1: true, 2: false, 3: false });
 
   const handleSwitch = useCallback(
     (id, checked) => {
@@ -53,23 +53,26 @@ export default function StyleGuide() {
       </div>
 
       <Heading size="md">Switch</Heading>
-      <div className="flex">
-        <div>
-          <p>Disabled, off</p>
-          <Switch />
-        </div>
-        <div>
-          <p>Disabled, on</p>
-          <Switch checked />
-        </div>
-        <div>
-          <p>Enabled, (off initial)</p>
-          <Switch checked={switches[0]} id={0} onChange={handleSwitch} label="Default" />
-        </div>
-        <div>
-          <p>Enabled, (on initial)</p>
-          <Switch checked={switches[1]} id={1} onChange={handleSwitch} label="Default" />
-        </div>
+      <div className="flex-col space-y-4 max-w-4xl">
+        <Switch label="Disabled, off" labelPosition="after" />
+        <Switch label="Disabled, on" labelPosition="after" checked />
+        <Switch
+          label="Enabled, (off initial)"
+          labelPosition="after"
+          checked={switches[0]}
+          id={0}
+          onChange={handleSwitch}
+        />
+        <Switch
+          label="Enabled, (on initial)"
+          labelPosition="after"
+          checked={switches[1]}
+          id={1}
+          onChange={handleSwitch}
+        />
+
+        <Switch checked={switches[2]} id={2} label="Label before" onChange={handleSwitch} />
+        <Switch checked={switches[3]} id={3} label="Label after" labelPosition="after" onChange={handleSwitch} />
       </div>
 
       <Heading size="md">Select</Heading>