1010import pytest
1111from PySide6 .QtCore import QPointF , Qt
1212
13- from ngl import Vec3
14- from ngl .pyside_event_handling_mixin import PySideEventHandlingMixin
13+ from ngl import PySideEventHandlingMixin , Vec3
1514
1615
1716class MockEventHandlingWindow (PySideEventHandlingMixin ):
@@ -350,10 +349,10 @@ def test_get_camera_state(event_window):
350349 expected = {
351350 "spin_x_face" : 45 ,
352351 "spin_y_face" : 90 ,
353- "model_position" : [1 , 2 , 3 ],
352+ "model_position" : [pytest . approx ( 1 ), pytest . approx ( 2 ), pytest . approx ( 3 ) ],
354353 "rotation_sensitivity" : pytest .approx (0.8 ),
355354 "translation_sensitivity" : pytest .approx (0.02 ),
356- "zoom_sensitivity" : pytest .approx (0.15 ) ,
355+ "zoom_sensitivity" : pytest .approx (0.15 ),
357356 }
358357
359358 assert state == expected
@@ -374,9 +373,9 @@ def test_set_camera_state(event_window):
374373
375374 assert event_window .spin_x_face == 30
376375 assert event_window .spin_y_face == 60
377- assert event_window .model_position .x == 5
378- assert event_window .model_position .y == 10
379- assert event_window .model_position .z == 15
376+ assert event_window .model_position .x == pytest . approx ( 5 )
377+ assert event_window .model_position .y == pytest . approx ( 10 )
378+ assert event_window .model_position .z == pytest . approx ( 15 )
380379 assert event_window .rotation_sensitivity == pytest .approx (0.75 )
381380 assert event_window .translation_sensitivity == pytest .approx (0.05 )
382381 assert event_window .zoom_sensitivity == pytest .approx (0.25 )
@@ -393,9 +392,9 @@ def test_set_camera_state_partial(event_window):
393392
394393 assert event_window .spin_x_face == 15
395394 assert event_window .spin_y_face == 0 # Default
396- assert event_window .model_position .x == 2
397- assert event_window .model_position .y == 4
398- assert event_window .model_position .z == 6
395+ assert event_window .model_position .x == pytest . approx ( 2 )
396+ assert event_window .model_position .y == pytest . approx ( 4 )
397+ assert event_window .model_position .z == pytest . approx ( 6 )
399398 assert event_window .rotation_sensitivity == PySideEventHandlingMixin .DEFAULT_ROTATION_SENSITIVITY
400399 assert event_window .translation_sensitivity == PySideEventHandlingMixin .DEFAULT_TRANSLATION_SENSITIVITY
401400 assert event_window .zoom_sensitivity == PySideEventHandlingMixin .DEFAULT_ZOOM_SENSITIVITY
@@ -408,9 +407,9 @@ def test_set_camera_state_empty_dict(event_window):
408407 # Should use all defaults
409408 assert event_window .spin_x_face == 0
410409 assert event_window .spin_y_face == 0
411- assert event_window .model_position .x == 0
412- assert event_window .model_position .y == 0
413- assert event_window .model_position .z == 0
410+ assert event_window .model_position .x == pytest . approx ( 0 )
411+ assert event_window .model_position .y == pytest . approx ( 0 )
412+ assert event_window .model_position .z == pytest . approx ( 0 )
414413 assert event_window .rotation_sensitivity == PySideEventHandlingMixin .DEFAULT_ROTATION_SENSITIVITY
415414 assert event_window .translation_sensitivity == PySideEventHandlingMixin .DEFAULT_TRANSLATION_SENSITIVITY
416415 assert event_window .zoom_sensitivity == PySideEventHandlingMixin .DEFAULT_ZOOM_SENSITIVITY
@@ -439,9 +438,9 @@ def test_camera_state_round_trip(event_window):
439438 # Check values are restored
440439 assert event_window .spin_x_face == 25
441440 assert event_window .spin_y_face == 50
442- assert event_window .model_position .x == 3
443- assert event_window .model_position .y == 6
444- assert event_window .model_position .z == 9
441+ assert event_window .model_position .x == pytest . approx ( 3 )
442+ assert event_window .model_position .y == pytest . approx ( 6 )
443+ assert event_window .model_position .z == pytest . approx ( 9 )
445444 assert event_window .rotation_sensitivity == pytest .approx (0.6 )
446445 assert event_window .translation_sensitivity == pytest .approx (0.03 )
447446 assert event_window .zoom_sensitivity == pytest .approx (0.12 )
@@ -509,3 +508,134 @@ def test_event_handling_target_protocol():
509508 assert hasattr (window , "close" )
510509 assert callable (window .update )
511510 assert callable (window .close )
511+
512+
513+ def test_mouseMoveEvent_rotation_without_left_button (event_window ):
514+ """Test mouse movement during rotation mode but without left button pressed"""
515+ # Setup rotation state
516+ event_window .rotate = True
517+ event_window .original_x_rotation = 100
518+ event_window .original_y_rotation = 200
519+
520+ event = Mock ()
521+ event .buttons .return_value = Qt .NoButton # No button pressed
522+ event .position .return_value = QPointF (120 , 180 )
523+
524+ event_window .mouseMoveEvent (event )
525+
526+ # Should not update rotation values
527+ assert event_window .spin_x_face == 0
528+ assert event_window .spin_y_face == 0
529+ assert event_window .update_called is False
530+
531+
532+ def test_mouseMoveEvent_translation_without_right_button (event_window ):
533+ """Test mouse movement during translation mode but without right button pressed"""
534+ # Setup translation state
535+ event_window .translate = True
536+ event_window .original_x_pos = 100
537+ event_window .original_y_pos = 200
538+
539+ event = Mock ()
540+ event .buttons .return_value = Qt .NoButton # No button pressed
541+ event .position .return_value = QPointF (110 , 190 )
542+
543+ event_window .mouseMoveEvent (event )
544+
545+ # Should not update position
546+ assert event_window .model_position .x == 0
547+ assert event_window .model_position .y == 0
548+ assert event_window .update_called is False
549+
550+
551+ def test_mousePressEvent_middle_button (event_window ):
552+ """Test middle mouse button press (should be ignored)"""
553+ event = Mock ()
554+ event .button .return_value = Qt .MiddleButton
555+ event .position .return_value = QPointF (100 , 200 )
556+
557+ event_window .mousePressEvent (event )
558+
559+ # Should not change any state
560+ assert event_window .rotate is False
561+ assert event_window .translate is False
562+
563+
564+ def test_mouseReleaseEvent_middle_button (event_window ):
565+ """Test middle mouse button release (should be ignored)"""
566+ event_window .rotate = True
567+ event_window .translate = True
568+
569+ event = Mock ()
570+ event .button .return_value = Qt .MiddleButton
571+
572+ event_window .mouseReleaseEvent (event )
573+
574+ # Should not change state
575+ assert event_window .rotate is True
576+ assert event_window .translate is True
577+
578+
579+ def test_setup_event_handling_with_none_position ():
580+ """Test setup_event_handling with None initial position"""
581+ window = MockEventHandlingWindow ()
582+ window .setup_event_handling (initial_position = None )
583+
584+ assert window .model_position .x == 0
585+ assert window .model_position .y == 0
586+ assert window .model_position .z == 0
587+
588+
589+ def test_wheelEvent_priority_y_over_x (event_window ):
590+ """Test that y delta takes priority over x delta when both are non-zero"""
591+ event_window .zoom_sensitivity = 0.5
592+ initial_z = event_window .model_position .z
593+
594+ event = Mock ()
595+ angle_delta = Mock ()
596+ angle_delta .y .return_value = 120 # Positive y delta
597+ angle_delta .x .return_value = - 120 # Negative x delta
598+ event .angleDelta .return_value = angle_delta
599+
600+ event_window .wheelEvent (event )
601+
602+ # Should use y delta (120), not x delta (-120)
603+ assert event_window .model_position .z == initial_z + 0.5
604+ assert event_window .update_called is True
605+
606+
607+ def test_set_camera_state_partial_model_position ():
608+ """Test set_camera_state with partial model position data"""
609+ window = MockEventHandlingWindow ()
610+ window .setup_event_handling ()
611+
612+ state = {
613+ "spin_x_face" : 25 ,
614+ "model_position" : [7 , 8 ], # Only 2 values instead of 3
615+ }
616+
617+ # This should handle the IndexError gracefully
618+ window .set_camera_state (state )
619+
620+ assert window .spin_x_face == 25
621+ assert window .model_position .x == pytest .approx (7 )
622+ assert window .model_position .y == pytest .approx (8 )
623+ # z should remain default
624+ assert window .model_position .z == pytest .approx (0 )
625+
626+
627+ def test_set_camera_state_empty_model_position ():
628+ """Test set_camera_state with empty model position list"""
629+ window = MockEventHandlingWindow ()
630+ window .setup_event_handling ()
631+
632+ state = {
633+ "model_position" : [], # Empty list
634+ }
635+
636+ # This should handle gracefully and use defaults
637+ window .set_camera_state (state )
638+
639+ assert window .model_position .x == pytest .approx (0 )
640+ assert window .model_position .y == pytest .approx (0 )
641+ assert window .model_position .z == pytest .approx (0 )
0 commit comments