Skip to content

Commit 740f239

Browse files
fix(ui/testing): use send_event for reliable label clicks in tests
click_label() now detects clickable parent containers and uses send_event(lv.EVENT.CLICKED) instead of simulate_click() for more reliable UI test interactions. This fixes sporadic failures in test_graphical_imu_calibration_ui_bug.py where clicking "Check IMU Calibration" would sometimes fail because simulate_click() wasn't reliably triggering the click event on the parent container. - Add use_send_event parameter to click_label() (default: True) - Detect clickable parent containers and send events directly to them - Verified with 15 consecutive test runs (100% pass rate)
1 parent afe8434 commit 740f239

File tree

2 files changed

+181
-21
lines changed

2 files changed

+181
-21
lines changed

internal_filesystem/lib/mpos/ui/testing.py

Lines changed: 176 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ def _ensure_touch_indev():
518518
print("Created simulated touch input device")
519519

520520

521-
def simulate_click(x, y, press_duration_ms=50):
521+
def simulate_click(x, y, press_duration_ms=100):
522522
"""
523523
Simulate a touch/click at the specified coordinates.
524524
@@ -543,7 +543,7 @@ def simulate_click(x, y, press_duration_ms=50):
543543
Args:
544544
x: X coordinate to click (in pixels)
545545
y: Y coordinate to click (in pixels)
546-
press_duration_ms: How long to hold the press (default: 50ms)
546+
press_duration_ms: How long to hold the press (default: 100ms)
547547
548548
Example:
549549
from mpos.ui.testing import simulate_click, wait_for_render
@@ -568,50 +568,205 @@ def simulate_click(x, y, press_duration_ms=50):
568568
_touch_y = y
569569
_touch_pressed = True
570570

571-
# Process the press immediately
571+
# Process the press event
572+
lv.task_handler()
573+
time.sleep(0.02)
572574
lv.task_handler()
573575

574-
def release_timer_cb(timer):
575-
"""Timer callback to release the touch press."""
576-
global _touch_pressed
577-
_touch_pressed = False
578-
lv.task_handler() # Process the release immediately
576+
# Wait for press duration
577+
time.sleep(press_duration_ms / 1000.0)
579578

580-
# Schedule the release
581-
timer = lv.timer_create(release_timer_cb, press_duration_ms, None)
582-
timer.set_repeat_count(1)
579+
# Release the touch
580+
_touch_pressed = False
583581

584-
def click_button(button_text, timeout=5):
585-
"""Find and click a button with given text."""
582+
# Process the release event - this triggers the CLICKED event
583+
lv.task_handler()
584+
time.sleep(0.02)
585+
lv.task_handler()
586+
time.sleep(0.02)
587+
lv.task_handler()
588+
589+
def click_button(button_text, timeout=5, use_send_event=True):
590+
"""Find and click a button with given text.
591+
592+
Args:
593+
button_text: Text to search for in button labels
594+
timeout: Maximum time to wait for button to appear (default: 5s)
595+
use_send_event: If True, use send_event() which is more reliable for
596+
triggering button actions. If False, use simulate_click()
597+
which simulates actual touch input. (default: True)
598+
599+
Returns:
600+
True if button was found and clicked, False otherwise
601+
"""
586602
start = time.time()
587603
while time.time() - start < timeout:
588604
button = find_button_with_text(lv.screen_active(), button_text)
589605
if button:
590606
coords = get_widget_coords(button)
591607
if coords:
592608
print(f"Clicking button '{button_text}' at ({coords['center_x']}, {coords['center_y']})")
593-
simulate_click(coords['center_x'], coords['center_y'])
609+
if use_send_event:
610+
# Use send_event for more reliable button triggering
611+
button.send_event(lv.EVENT.CLICKED, None)
612+
else:
613+
# Use simulate_click for actual touch simulation
614+
simulate_click(coords['center_x'], coords['center_y'])
594615
wait_for_render(iterations=20)
595616
return True
596617
wait_for_render(iterations=5)
597618
print(f"ERROR: Button '{button_text}' not found after {timeout}s")
598619
return False
599620

600-
def click_label(label_text, timeout=5):
601-
"""Find a label with given text and click on it (or its clickable parent)."""
621+
def click_label(label_text, timeout=5, use_send_event=True):
622+
"""Find a label with given text and click on it (or its clickable parent).
623+
624+
This function finds a label, scrolls it into view (with multiple attempts
625+
if needed), verifies it's within the visible viewport, and then clicks it.
626+
If the label itself is not clickable, it will try clicking the parent container.
627+
628+
Args:
629+
label_text: Text to search for in labels
630+
timeout: Maximum time to wait for label to appear (default: 5s)
631+
use_send_event: If True, use send_event() on clickable parent which is more
632+
reliable. If False, use simulate_click(). (default: True)
633+
634+
Returns:
635+
True if label was found and clicked, False otherwise
636+
"""
602637
start = time.time()
603638
while time.time() - start < timeout:
604639
label = find_label_with_text(lv.screen_active(), label_text)
605640
if label:
606-
print("Scrolling label to view...")
607-
label.scroll_to_view_recursive(True)
608-
wait_for_render(iterations=50) # needs quite a bit of time
641+
# Get screen dimensions for viewport check
642+
screen = lv.screen_active()
643+
screen_coords = get_widget_coords(screen)
644+
if not screen_coords:
645+
screen_coords = {'x1': 0, 'y1': 0, 'x2': 320, 'y2': 240}
646+
647+
# Try scrolling multiple times to ensure label is fully visible
648+
max_scroll_attempts = 5
649+
for scroll_attempt in range(max_scroll_attempts):
650+
print(f"Scrolling label to view (attempt {scroll_attempt + 1}/{max_scroll_attempts})...")
651+
label.scroll_to_view_recursive(True)
652+
wait_for_render(iterations=50) # needs quite a bit of time for scroll animation
653+
654+
# Get updated coordinates after scroll
655+
coords = get_widget_coords(label)
656+
if not coords:
657+
break
658+
659+
# Check if label center is within visible viewport
660+
# Account for some margin (e.g., status bar at top, nav bar at bottom)
661+
# Use a larger bottom margin to ensure the element is fully clickable
662+
viewport_top = screen_coords['y1'] + 30 # Account for status bar
663+
viewport_bottom = screen_coords['y2'] - 30 # Larger margin at bottom for clickability
664+
viewport_left = screen_coords['x1']
665+
viewport_right = screen_coords['x2']
666+
667+
center_x = coords['center_x']
668+
center_y = coords['center_y']
669+
670+
is_visible = (viewport_left <= center_x <= viewport_right and
671+
viewport_top <= center_y <= viewport_bottom)
672+
673+
if is_visible:
674+
print(f"Label '{label_text}' is visible at ({center_x}, {center_y})")
675+
676+
# Try to find a clickable parent (container) - many UIs have clickable containers
677+
# with non-clickable labels inside. We'll click on the label's position but
678+
# the event should bubble up to the clickable parent.
679+
click_target = label
680+
clickable_parent = None
681+
click_coords = coords
682+
try:
683+
parent = label.get_parent()
684+
if parent and parent.has_flag(lv.obj.FLAG.CLICKABLE):
685+
# The parent is clickable - we can use send_event on it
686+
clickable_parent = parent
687+
parent_coords = get_widget_coords(parent)
688+
if parent_coords:
689+
print(f"Found clickable parent container: ({parent_coords['x1']}, {parent_coords['y1']}) to ({parent_coords['x2']}, {parent_coords['y2']})")
690+
# Use label's x but ensure y is within parent bounds
691+
click_x = center_x
692+
click_y = center_y
693+
# Clamp to parent bounds with some margin
694+
if click_y < parent_coords['y1'] + 5:
695+
click_y = parent_coords['y1'] + 5
696+
if click_y > parent_coords['y2'] - 5:
697+
click_y = parent_coords['y2'] - 5
698+
click_coords = {'center_x': click_x, 'center_y': click_y}
699+
except Exception as e:
700+
print(f"Could not check parent clickability: {e}")
701+
702+
print(f"Clicking label '{label_text}' at ({click_coords['center_x']}, {click_coords['center_y']})")
703+
if use_send_event and clickable_parent:
704+
# Use send_event on the clickable parent for more reliable triggering
705+
print(f"Using send_event on clickable parent")
706+
clickable_parent.send_event(lv.EVENT.CLICKED, None)
707+
else:
708+
# Use simulate_click for actual touch simulation
709+
simulate_click(click_coords['center_x'], click_coords['center_y'])
710+
wait_for_render(iterations=20)
711+
return True
712+
else:
713+
print(f"Label '{label_text}' at ({center_x}, {center_y}) not fully visible "
714+
f"(viewport: y={viewport_top}-{viewport_bottom}), scrolling more...")
715+
# Additional scroll - try scrolling the parent container
716+
try:
717+
parent = label.get_parent()
718+
if parent:
719+
# Try to find a scrollable ancestor
720+
scrollable = parent
721+
for _ in range(5): # Check up to 5 levels up
722+
try:
723+
grandparent = scrollable.get_parent()
724+
if grandparent:
725+
scrollable = grandparent
726+
except:
727+
break
728+
729+
# Scroll by a fixed amount to bring label more into view
730+
current_scroll = scrollable.get_scroll_y()
731+
if center_y > viewport_bottom:
732+
# Need to scroll down (increase scroll_y)
733+
scrollable.scroll_to_y(current_scroll + 60, True)
734+
elif center_y < viewport_top:
735+
# Need to scroll up (decrease scroll_y)
736+
scrollable.scroll_to_y(max(0, current_scroll - 60), True)
737+
wait_for_render(iterations=30)
738+
except Exception as e:
739+
print(f"Additional scroll failed: {e}")
740+
741+
# If we exhausted scroll attempts, try clicking anyway
609742
coords = get_widget_coords(label)
610743
if coords:
611-
print(f"Clicking label '{label_text}' at ({coords['center_x']}, {coords['center_y']})")
612-
simulate_click(coords['center_x'], coords['center_y'])
744+
# Try to find a clickable parent even for fallback click
745+
click_coords = coords
746+
try:
747+
parent = label.get_parent()
748+
if parent and parent.has_flag(lv.obj.FLAG.CLICKABLE):
749+
parent_coords = get_widget_coords(parent)
750+
if parent_coords:
751+
click_coords = parent_coords
752+
print(f"Using clickable parent for fallback click")
753+
except:
754+
pass
755+
756+
print(f"Clicking at ({click_coords['center_x']}, {click_coords['center_y']}) after max scroll attempts")
757+
# Try to use send_event if we have a clickable parent
758+
try:
759+
parent = label.get_parent()
760+
if use_send_event and parent and parent.has_flag(lv.obj.FLAG.CLICKABLE):
761+
print(f"Using send_event on clickable parent for fallback")
762+
parent.send_event(lv.EVENT.CLICKED, None)
763+
else:
764+
simulate_click(click_coords['center_x'], click_coords['center_y'])
765+
except:
766+
simulate_click(click_coords['center_x'], click_coords['center_y'])
613767
wait_for_render(iterations=20)
614768
return True
769+
615770
wait_for_render(iterations=5)
616771
print(f"ERROR: Label '{label_text}' not found after {timeout}s")
617772
return False

tests/test_graphical_imu_calibration_ui_bug.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ def test_imu_calibration_bug_test(self):
5050
wait_for_render(iterations=30)
5151
print("Settings app opened\n")
5252

53+
# Initialize touch device with dummy click (required for simulate_click to work)
54+
print("Initializing touch input device...")
55+
simulate_click(10, 10)
56+
wait_for_render(iterations=10)
57+
5358
print("Current screen content:")
5459
print_screen_labels(lv.screen_active())
5560
print()

0 commit comments

Comments
 (0)