Skip to content

Commit dceea68

Browse files
Improve tests/test_graphical_imu_calibration.py stability
1 parent 9704488 commit dceea68

3 files changed

Lines changed: 84 additions & 33 deletions

File tree

internal_filesystem/lib/mpos/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
get_all_widgets_with_text, find_setting_value_label, get_setting_value_text,
4343
verify_setting_value_text, find_dropdown_widget, get_dropdown_options,
4444
find_dropdown_option_index, select_dropdown_option_by_text,
45-
get_all_children, simulate_long_press
45+
get_all_children, simulate_long_press, wait_for_text, wait_for_widget
4646
)
4747

4848
# UI utility functions
@@ -103,7 +103,7 @@
103103
"get_all_widgets_with_text", "find_setting_value_label", "get_setting_value_text",
104104
"verify_setting_value_text", "find_dropdown_widget", "get_dropdown_options",
105105
"find_dropdown_option_index", "select_dropdown_option_by_text",
106-
"get_all_children", "simulate_long_press",
106+
"get_all_children", "simulate_long_press", "wait_for_text", "wait_for_widget",
107107
# Submodules
108108
"ui", "config", "net", "content", "time", "sensor_manager",
109109
"camera_manager", "sdcard", "audio", "hardware",

internal_filesystem/lib/mpos/ui/testing.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1702,3 +1702,60 @@ def get_all_children(parent):
17021702

17031703
def simulate_long_press(x, y, duration_ms=1000):
17041704
simulate_click(x, y, press_duration_ms=duration_ms)
1705+
1706+
1707+
def wait_for_text(text, timeout=10, interval=0.1):
1708+
"""
1709+
Wait for text to appear on screen, polling periodically.
1710+
1711+
More robust than wait_for_render(N) because it actually checks
1712+
for the desired condition instead of waiting a fixed amount of time.
1713+
Handles slow CI machines gracefully — returns as soon as text appears.
1714+
1715+
Args:
1716+
text: Text to search for (substring match via verify_text_present)
1717+
timeout: Maximum time to wait in seconds (default: 10)
1718+
interval: Time between checks in seconds (default: 0.1)
1719+
1720+
Returns:
1721+
True if text found within timeout, False otherwise
1722+
"""
1723+
start = time.time()
1724+
while time.time() - start < timeout:
1725+
if verify_text_present(lv.screen_active(), text):
1726+
return True
1727+
wait_for_render(5)
1728+
time.sleep(interval)
1729+
print(f"wait_for_text: '{text}' not found after {timeout}s")
1730+
return False
1731+
1732+
1733+
def wait_for_widget(find_func, timeout=10, interval=0.1):
1734+
"""
1735+
Wait for a widget condition, polling periodically.
1736+
1737+
find_func should be a callable that returns a widget or truthy value
1738+
when the condition is met, and None/falsy otherwise. For example::
1739+
1740+
btn = wait_for_widget(
1741+
lambda: find_button_with_text(lv.screen_active(), "Submit"),
1742+
timeout=5
1743+
)
1744+
1745+
Args:
1746+
find_func: Callable that returns a widget or truthy value
1747+
timeout: Maximum time to wait in seconds (default: 10)
1748+
interval: Time between checks in seconds (default: 0.1)
1749+
1750+
Returns:
1751+
The result of find_func if found within timeout, None otherwise
1752+
"""
1753+
start = time.time()
1754+
while time.time() - start < timeout:
1755+
result = find_func()
1756+
if result:
1757+
return result
1758+
wait_for_render(5)
1759+
time.sleep(interval)
1760+
print(f"wait_for_widget: condition not met after {timeout}s")
1761+
return None

tests/test_graphical_imu_calibration.py

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,12 @@
1212
import unittest
1313
import lvgl as lv
1414
import mpos.ui
15-
import time
1615
import sys
1716
from mpos import (
18-
wait_for_render,
19-
find_label_with_text,
17+
wait_for_text,
2018
verify_text_present,
2119
print_screen_labels,
22-
simulate_click,
23-
get_widget_coords,
2420
find_button_with_text,
25-
find_text_on_screen,
2621
AppManager
2722
)
2823

@@ -36,17 +31,10 @@ def _start_activity_from_settings_assets(self, filename, classname):
3631
cwd = f"builtin/apps/{app_fullname}/assets/"
3732
result = AppManager.execute_script(entrypoint, classname, cwd, app_fullname=app_fullname)
3833
self.assertTrue(result, f"Failed to start {classname} from {entrypoint}")
39-
wait_for_render(iterations=60)
4034

4135
def tearDown(self):
4236
"""Clean up after test."""
43-
# Navigate back to launcher
44-
try:
45-
for _ in range(3): # May need multiple backs
46-
mpos.ui.back_screen()
47-
wait_for_render(10)
48-
except:
49-
pass
37+
mpos.ui.back_screen()
5038

5139
def test_check_calibration_activity_loads(self):
5240
"""Test that CheckIMUCalibrationActivity loads and displays."""
@@ -55,10 +43,13 @@ def test_check_calibration_activity_loads(self):
5543
# Navigate directly to activity to avoid flaky settings clicks
5644
self._start_activity_from_settings_assets("check_imu_calibration.py", "CheckIMUCalibrationActivity")
5745

46+
# Wait for activity UI to render (polling, not fixed — handles slow CI)
47+
self.assertTrue(wait_for_text("Quality", timeout=10),
48+
"CheckIMUCalibrationActivity: 'Quality' label not found within timeout")
49+
5850
# Verify key elements are present
5951
screen = lv.screen_active()
6052
print_screen_labels(screen)
61-
self.assertTrue(verify_text_present(screen, "Quality"), "Quality label not found")
6253
self.assertTrue(verify_text_present(screen, "Accel."), "Accel. label not found")
6354
self.assertTrue(verify_text_present(screen, "Gyro"), "Gyro label not found")
6455

@@ -71,31 +62,29 @@ def test_calibrate_activity_flow(self):
7162
# Navigate directly to activity to avoid flaky settings clicks
7263
self._start_activity_from_settings_assets("calibrate_imu.py", "CalibrateIMUActivity")
7364

74-
# Verify activity loaded and shows instructions
65+
# Wait for activity UI to render (polling, not fixed)
66+
self.assertTrue(wait_for_text("IMU Calibration", timeout=10),
67+
"CalibrateIMUActivity title not found within timeout")
7568
screen = lv.screen_active()
7669
print_screen_labels(screen)
77-
self.assertTrue(verify_text_present(screen, "IMU Calibration"),
78-
"CalibrateIMUActivity title not found")
7970
self.assertTrue(verify_text_present(screen, "Place device on flat"),
8071
"Instructions not shown")
8172

8273
# Click "Calibrate Now" button to start calibration
8374
calibrate_btn = find_button_with_text(screen, "Calibrate Now")
8475
self.assertIsNotNone(calibrate_btn, "Could not find 'Calibrate Now' button")
8576
calibrate_btn.send_event(lv.EVENT.CLICKED, None)
86-
wait_for_render(25)
8777

88-
# Wait for calibration to complete (mock takes ~3 seconds)
89-
time.sleep(4)
90-
wait_for_render(50)
78+
# Wait for calibration to complete — poll for the success message
79+
# instead of fixed sleep (handles slow/fast CI equally well)
80+
self.assertTrue(
81+
wait_for_text("Calibration successful!", timeout=15),
82+
"Calibration completion message not found within timeout"
83+
)
9184

92-
# Verify calibration completed
85+
# Verify offsets are shown
9386
screen = lv.screen_active()
9487
print_screen_labels(screen)
95-
self.assertTrue(verify_text_present(screen, "Calibration successful!"),
96-
"Calibration completion message not found")
97-
98-
# Verify offsets are shown
9988
self.assertTrue(verify_text_present(screen, "Accel offsets") or
10089
verify_text_present(screen, "offsets"),
10190
"Calibration offsets not shown")
@@ -109,6 +98,10 @@ def test_navigation_from_check_to_calibrate(self):
10998
# Navigate directly to Check activity
11099
self._start_activity_from_settings_assets("check_imu_calibration.py", "CheckIMUCalibrationActivity")
111100

101+
# Wait for Check activity to render
102+
self.assertTrue(wait_for_text("Quality", timeout=10),
103+
"CheckIMUCalibrationActivity: 'Quality' label not found")
104+
112105
# Click "Calibrate" button to navigate to Calibrate activity
113106
screen = lv.screen_active()
114107
calibrate_btn = find_button_with_text(screen, "Calibrate")
@@ -121,19 +114,20 @@ def test_navigation_from_check_to_calibrate(self):
121114
added_path = True
122115
try:
123116
calibrate_btn.send_event(lv.EVENT.CLICKED, None)
124-
wait_for_render(60)
125117
finally:
126118
if added_path:
127119
try:
128120
sys.path.remove(assets_path)
129121
except ValueError:
130122
pass
131123

132-
# Verify CalibrateIMUActivity loaded
124+
# Wait for CalibrateIMUActivity to load (polling, not fixed)
125+
self.assertTrue(
126+
wait_for_text("Calibrate Now", timeout=10),
127+
"Did not navigate to CalibrateIMUActivity within timeout"
128+
)
133129
screen = lv.screen_active()
134130
print_screen_labels(screen)
135-
self.assertTrue(verify_text_present(screen, "Calibrate Now"),
136-
"Did not navigate to CalibrateIMUActivity")
137131
self.assertTrue(verify_text_present(screen, "Place device on flat"),
138132
"CalibrateIMUActivity instructions not shown")
139133

0 commit comments

Comments
 (0)