mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-26 13:47:03 +02:00
test(web): Switch (and add label back in)
This commit is contained in:
parent
f70fb12c3d
commit
5eaf8a5448
@ -1,7 +1,7 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useCallback, useState } from 'preact/hooks';
|
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 [isFocused, setFocused] = useState(false);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
@ -24,15 +24,21 @@ export default function Switch({ checked, id, onChange }) {
|
|||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
htmlFor={id}
|
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
|
<div
|
||||||
onMouseOver={handleFocus}
|
onMouseOver={handleFocus}
|
||||||
onMouseOut={handleBlur}
|
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">
|
<div className="relative overflow-hidden">
|
||||||
<input
|
<input
|
||||||
|
data-testid={`${id}-input`}
|
||||||
className="absolute left-48"
|
className="absolute left-48"
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
@ -55,6 +61,11 @@ export default function Switch({ checked, id, onChange }) {
|
|||||||
style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}
|
style={checked ? 'transform: translateX(100%);' : 'transform: translateX(0%);'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{label && labelPosition !== 'before' ? (
|
||||||
|
<div data-testid={`${id}-label`} class="inline-flex flex-grow">
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
47
web/src/components/__tests__/Switch.test.jsx
Normal file
47
web/src/components/__tests__/Switch.test.jsx
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -45,30 +45,36 @@ export default function Camera({ camera }) {
|
|||||||
|
|
||||||
const optionContent = showSettings ? (
|
const optionContent = showSettings ? (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
<div className="flex space-x-3">
|
<Switch
|
||||||
<Switch checked={options['bbox']} id="bbox" onChange={handleSetOption} />
|
checked={options['bbox']}
|
||||||
<span className="inline-flex">Bounding box</span>
|
id="bbox"
|
||||||
</div>
|
onChange={handleSetOption}
|
||||||
<div className="flex space-x-3">
|
label="Bounding box"
|
||||||
<Switch checked={options['timestamp']} id="timestamp" onChange={handleSetOption} />
|
labelPosition="after"
|
||||||
<span className="inline-flex">Timestamp</span>
|
/>
|
||||||
</div>
|
<Switch
|
||||||
<div className="flex space-x-3">
|
checked={options['timestamp']}
|
||||||
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} />
|
id="timestamp"
|
||||||
<span className="inline-flex">Zones</span>
|
onChange={handleSetOption}
|
||||||
</div>
|
label="Timestamp"
|
||||||
<div className="flex space-x-3">
|
labelPosition="after"
|
||||||
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} />
|
/>
|
||||||
<span className="inline-flex">Masks</span>
|
<Switch checked={options['zones']} id="zones" onChange={handleSetOption} label="Zones" labelPosition="after" />
|
||||||
</div>
|
<Switch checked={options['mask']} id="mask" onChange={handleSetOption} label="Masks" labelPosition="after" />
|
||||||
<div className="flex space-x-3">
|
<Switch
|
||||||
<Switch checked={options['motion']} id="motion" onChange={handleSetOption} />
|
checked={options['motion']}
|
||||||
<span className="inline-flex">Motion boxes</span>
|
id="motion"
|
||||||
</div>
|
onChange={handleSetOption}
|
||||||
<div className="flex space-x-3">
|
label="Motion boxes"
|
||||||
<Switch checked={options['regions']} id="regions" onChange={handleSetOption} />
|
labelPosition="after"
|
||||||
<span className="inline-flex">Regions</span>
|
/>
|
||||||
</div>
|
<Switch
|
||||||
|
checked={options['regions']}
|
||||||
|
id="regions"
|
||||||
|
onChange={handleSetOption}
|
||||||
|
label="Regions"
|
||||||
|
labelPosition="after"
|
||||||
|
/>
|
||||||
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
<Link href={`/cameras/${camera}/editor`}>Mask & Zone creator</Link>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
@ -29,8 +29,8 @@ export default function CameraMasks({ camera, url }) {
|
|||||||
Array.isArray(motionMask)
|
Array.isArray(motionMask)
|
||||||
? motionMask.map((mask) => getPolylinePoints(mask))
|
? motionMask.map((mask) => getPolylinePoints(mask))
|
||||||
: motionMask
|
: motionMask
|
||||||
? [getPolylinePoints(motionMask)]
|
? [getPolylinePoints(motionMask)]
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
|
|
||||||
const [zonePoints, setZonePoints] = useState(
|
const [zonePoints, setZonePoints] = useState(
|
||||||
@ -44,8 +44,8 @@ export default function CameraMasks({ camera, url }) {
|
|||||||
[name]: Array.isArray(objectFilters[name].mask)
|
[name]: Array.isArray(objectFilters[name].mask)
|
||||||
? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
|
? objectFilters[name].mask.map((mask) => getPolylinePoints(mask))
|
||||||
: objectFilters[name].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 () => {
|
const handleCopyZones = useCallback(async () => {
|
||||||
await window.navigator.clipboard.writeText(` zones:
|
await window.navigator.clipboard.writeText(` zones:
|
||||||
${Object.keys(zonePoints)
|
${Object.keys(zonePoints)
|
||||||
.map(
|
.map(
|
||||||
(zoneName) => ` ${zoneName}:
|
(zoneName) => ` ${zoneName}:
|
||||||
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
|
coordinates: ${polylinePointsToPolyline(zonePoints[zoneName])}`
|
||||||
)
|
)
|
||||||
.join('\n')}`);
|
.join('\n')}`);
|
||||||
}, [zonePoints]);
|
}, [zonePoints]);
|
||||||
|
|
||||||
// Object methods
|
// Object methods
|
||||||
@ -164,14 +164,14 @@ ${Object.keys(zonePoints)
|
|||||||
await window.navigator.clipboard.writeText(` objects:
|
await window.navigator.clipboard.writeText(` objects:
|
||||||
filters:
|
filters:
|
||||||
${Object.keys(objectMaskPoints)
|
${Object.keys(objectMaskPoints)
|
||||||
.map((objectName) =>
|
.map((objectName) =>
|
||||||
objectMaskPoints[objectName].length
|
objectMaskPoints[objectName].length
|
||||||
? ` ${objectName}:
|
? ` ${objectName}:
|
||||||
mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
|
mask: ${polylinePointsToPolyline(objectMaskPoints[objectName])}`
|
||||||
: ''
|
: ''
|
||||||
)
|
)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')}`);
|
.join('\n')}`);
|
||||||
}, [objectMaskPoints]);
|
}, [objectMaskPoints]);
|
||||||
|
|
||||||
const handleAddToObjectMask = useCallback(
|
const handleAddToObjectMask = useCallback(
|
||||||
@ -222,8 +222,8 @@ ${Object.keys(objectMaskPoints)
|
|||||||
height={height}
|
height={height}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-4">
|
<div className="max-w-xs">
|
||||||
<span>Snap to edges</span> <Switch checked={snap} onChange={handleChangeSnap} />
|
<Switch checked={snap} label="Snap to edges" labelPosition="after" onChange={handleChangeSnap} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -360,15 +360,15 @@ function EditableMask({ onChange, points, scale, snap, width, height }) {
|
|||||||
{!scaledPoints
|
{!scaledPoints
|
||||||
? null
|
? null
|
||||||
: scaledPoints.map(([x, y], i) => (
|
: scaledPoints.map(([x, y], i) => (
|
||||||
<PolyPoint
|
<PolyPoint
|
||||||
boundingRef={boundingRef}
|
boundingRef={boundingRef}
|
||||||
index={i}
|
index={i}
|
||||||
onMove={handleMovePoint}
|
onMove={handleMovePoint}
|
||||||
onRemove={handleRemovePoint}
|
onRemove={handleRemovePoint}
|
||||||
x={x + MaskInset}
|
x={x + MaskInset}
|
||||||
y={y + MaskInset}
|
y={y + MaskInset}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
|
<div className="absolute inset-0 right-0 bottom-0" onClick={handleAddPoint} ref={boundingRef} />
|
||||||
<svg
|
<svg
|
||||||
width="100%"
|
width="100%"
|
||||||
|
@ -9,7 +9,7 @@ import TextField from '../components/TextField';
|
|||||||
import { useCallback, useState } from 'preact/hooks';
|
import { useCallback, useState } from 'preact/hooks';
|
||||||
|
|
||||||
export default function StyleGuide() {
|
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(
|
const handleSwitch = useCallback(
|
||||||
(id, checked) => {
|
(id, checked) => {
|
||||||
@ -53,23 +53,26 @@ export default function StyleGuide() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Heading size="md">Switch</Heading>
|
<Heading size="md">Switch</Heading>
|
||||||
<div className="flex">
|
<div className="flex-col space-y-4 max-w-4xl">
|
||||||
<div>
|
<Switch label="Disabled, off" labelPosition="after" />
|
||||||
<p>Disabled, off</p>
|
<Switch label="Disabled, on" labelPosition="after" checked />
|
||||||
<Switch />
|
<Switch
|
||||||
</div>
|
label="Enabled, (off initial)"
|
||||||
<div>
|
labelPosition="after"
|
||||||
<p>Disabled, on</p>
|
checked={switches[0]}
|
||||||
<Switch checked />
|
id={0}
|
||||||
</div>
|
onChange={handleSwitch}
|
||||||
<div>
|
/>
|
||||||
<p>Enabled, (off initial)</p>
|
<Switch
|
||||||
<Switch checked={switches[0]} id={0} onChange={handleSwitch} label="Default" />
|
label="Enabled, (on initial)"
|
||||||
</div>
|
labelPosition="after"
|
||||||
<div>
|
checked={switches[1]}
|
||||||
<p>Enabled, (on initial)</p>
|
id={1}
|
||||||
<Switch checked={switches[1]} id={1} onChange={handleSwitch} label="Default" />
|
onChange={handleSwitch}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
<Heading size="md">Select</Heading>
|
<Heading size="md">Select</Heading>
|
||||||
|
Loading…
Reference in New Issue
Block a user