Address review feedback: drag edge cases and test clarity

- Add onMouseLeave handler to cancel drag when cursor leaves overlay
- Preserve autotracking zooming disable-on-failure during init
- Extract 20px drag threshold as MIN_DRAG_DISTANCE constant
- Add sync-with-source notes to replicated test helpers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Gregg 2026-03-18 02:18:44 +00:00
parent 4dfc248df4
commit 4a00dbca9d
3 changed files with 26 additions and 6 deletions

View File

@ -295,6 +295,13 @@ class OnvifController:
):
del move_request["Speed"]["Zoom"]
except Exception as e:
if (
self.config.cameras[camera_name].onvif.autotracking.zooming
== ZoomingModeEnum.relative
):
self.config.cameras[
camera_name
].onvif.autotracking.zooming = ZoomingModeEnum.disabled
logger.warning(
f"Relative zoom not supported for {camera_name}: {e}"
)

View File

@ -20,7 +20,12 @@ class OnvifCommandEnum(str, Enum):
class TestMoveRelativeParsing(TestCase):
"""Test the move_relative command parameter parsing logic."""
"""Test the move_relative command parameter parsing logic.
NOTE: _parse_move_relative replicates logic from
OnvifController.handle_command_async (frigate/ptz/onvif.py).
If that parsing changes, this helper must be updated to match.
"""
def _parse_move_relative(self, param: str):
"""Replicate the parsing logic from OnvifController.handle_command_async."""
@ -57,8 +62,10 @@ class TestMoveRelativeParsing(TestCase):
class TestDispatcherPtzParsing(TestCase):
"""Test the dispatcher's _on_ptz_command parsing pipeline.
Replicates the logic from FrigateDispatcher._on_ptz_command to verify
that MQTT payloads are correctly decomposed into command + param.
Replicates the logic from FrigateDispatcher._on_ptz_command
(frigate/comms/dispatcher.py) to verify that MQTT payloads are
correctly decomposed into command + param.
If that parsing changes, this helper must be updated to match.
"""
def _parse_ptz_payload(self, payload: str):

View File

@ -131,6 +131,9 @@ type LiveCameraViewProps = {
toggleFullscreen: () => void;
};
/** Minimum drag distance (px) to distinguish a drag-to-zoom from a click-to-move. */
const MIN_DRAG_DISTANCE = 20;
function getClientPos(
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>,
): { x: number; y: number } | null {
@ -293,8 +296,7 @@ export default function LiveCameraView({
const dx = Math.abs(pos.x - dragStart.x);
const dy = Math.abs(pos.y - dragStart.y);
// Minimum drag distance of 20px to distinguish from click
if (dx < 20 && dy < 20) {
if (dx < MIN_DRAG_DISTANCE && dy < MIN_DRAG_DISTANCE) {
// Click (not drag) — move to point without zoom
const normalizedX = (pos.x - rect.left) / rect.width;
const normalizedY = (pos.y - rect.top) / rect.height;
@ -352,7 +354,7 @@ export default function LiveCameraView({
if (!isDragging || !clickOverlayRef.current) return null;
const dx = Math.abs(dragCurrent.x - dragStart.x);
const dy = Math.abs(dragCurrent.y - dragStart.y);
if (dx < 20 && dy < 20) return null; // Don't show rectangle for small movements
if (dx < MIN_DRAG_DISTANCE && dy < MIN_DRAG_DISTANCE) return null;
const rect = clickOverlayRef.current.getBoundingClientRect();
const x1 = Math.min(dragStart.x, dragCurrent.x) - rect.left;
@ -762,6 +764,10 @@ export default function LiveCameraView({
onMouseDown={handleOverlayMouseDown}
onMouseMove={handleOverlayMouseMove}
onMouseUp={handleOverlayMouseUp}
onMouseLeave={() => {
setDragStart(null);
setDragCurrent(null);
}}
onTouchStart={handleOverlayMouseDown}
onTouchMove={handleOverlayMouseMove}
onTouchEnd={handleOverlayMouseUp}