Add internal_filesystem/lib/mpos/ui/testing.py

This commit is contained in:
Thomas Farstrike
2025-11-18 15:25:47 +01:00
parent b6869d592a
commit 5596ac42b5
16 changed files with 106 additions and 33 deletions
@@ -1,14 +1,18 @@
"""
Graphical testing helper module for MicroPythonOS.
Graphical testing utilities for MicroPythonOS.
This module provides utilities for graphical/visual testing that work on both
desktop (unix/macOS) and device (ESP32).
This module provides utilities for graphical/visual testing and UI automation
that work on both desktop (unix/macOS) and device (ESP32). These functions can
be used by:
- Unit tests for verifying UI behavior
- Apps that want to implement automation or testing features
- Integration tests and end-to-end testing
Important: Tests using this module should be run with boot and main files
already executed (so display, theme, and UI infrastructure are initialized).
Important: Functions in this module assume the display, theme, and UI
infrastructure are already initialized (boot.py and main.py executed).
Usage:
from graphical_test_helper import wait_for_render, capture_screenshot
Usage in tests:
from mpos.ui.testing import wait_for_render, capture_screenshot
# Start your app
mpos.apps.start_app("com.example.myapp")
@@ -22,8 +26,18 @@ Usage:
# Capture screenshot
capture_screenshot("tests/screenshots/mytest.raw")
# Simulate click at coordinates
# Simulate user interaction
simulate_click(160, 120) # Click at center of 320x240 screen
Usage in apps:
from mpos.ui.testing import simulate_click, find_label_with_text
# Automated demo mode
label = find_label_with_text(self.screen, "Start")
if label:
area = lv.area_t()
label.get_coords(area)
simulate_click(area.x1 + 10, area.y1 + 10)
"""
import lvgl as lv
@@ -41,9 +55,15 @@ def wait_for_render(iterations=10):
This processes the LVGL task handler multiple times to ensure
all UI updates, animations, and layout changes are complete.
Essential for tests to avoid race conditions.
Args:
iterations: Number of task handler iterations to run (default: 10)
Example:
mpos.apps.start_app("com.example.myapp")
wait_for_render() # Ensure UI is ready
assert verify_text_present(lv.screen_active(), "Welcome")
"""
import time
for _ in range(iterations):
@@ -52,14 +72,19 @@ def wait_for_render(iterations=10):
def capture_screenshot(filepath, width=320, height=240, color_format=lv.COLOR_FORMAT.RGB565):
print(f"capture_screenshot writing to {filepath}")
"""
Capture screenshot of current screen using LVGL snapshot.
The screenshot is saved as raw binary data in the specified color format.
To convert RGB565 to PNG, use:
Useful for visual regression testing or documentation.
To convert RGB565 to PNG:
ffmpeg -vcodec rawvideo -f rawvideo -pix_fmt rgb565 -s 320x240 -i file.raw file.png
Or use the conversion script:
cd tests/screenshots
./convert_to_png.sh
Args:
filepath: Path where to save the raw screenshot data
width: Screen width in pixels (default: 320)
@@ -71,7 +96,13 @@ def capture_screenshot(filepath, width=320, height=240, color_format=lv.COLOR_FO
Raises:
Exception: If screenshot capture fails
Example:
from mpos.ui.testing import capture_screenshot
capture_screenshot("tests/screenshots/home.raw")
"""
print(f"capture_screenshot writing to {filepath}")
# Calculate buffer size based on color format
if color_format == lv.COLOR_FORMAT.RGB565:
bytes_per_pixel = 2
@@ -99,7 +130,8 @@ def get_all_labels(obj, labels=None):
Recursively find all label widgets in the object hierarchy.
This traverses the entire widget tree starting from obj and
collects all LVGL label objects.
collects all LVGL label objects. Useful for comprehensive
text verification or debugging.
Args:
obj: LVGL object to search (typically lv.screen_active())
@@ -107,6 +139,10 @@ def get_all_labels(obj, labels=None):
Returns:
list: List of all label objects found in the hierarchy
Example:
labels = get_all_labels(lv.screen_active())
print(f"Found {len(labels)} labels")
"""
if labels is None:
labels = []
@@ -135,7 +171,8 @@ def find_label_with_text(obj, search_text):
Find a label widget containing specific text.
Searches the entire widget hierarchy for a label whose text
contains the search string (substring match).
contains the search string (substring match). Returns the
first match found.
Args:
obj: LVGL object to search (typically lv.screen_active())
@@ -143,6 +180,11 @@ def find_label_with_text(obj, search_text):
Returns:
LVGL label object if found, None otherwise
Example:
label = find_label_with_text(lv.screen_active(), "Settings")
if label:
print(f"Found Settings label at {label.get_coords()}")
"""
labels = get_all_labels(obj)
for label in labels:
@@ -160,12 +202,18 @@ def get_screen_text_content(obj):
Extract all text content from all labels on screen.
Useful for debugging or comprehensive text verification.
Returns a list of all text strings found in label widgets.
Args:
obj: LVGL object to search (typically lv.screen_active())
Returns:
list: List of all text strings found in labels
Example:
texts = get_screen_text_content(lv.screen_active())
assert "Welcome" in texts
assert "Version 1.0" in texts
"""
labels = get_all_labels(obj)
texts = []
@@ -184,7 +232,7 @@ def verify_text_present(obj, expected_text):
Verify that expected text is present somewhere on screen.
This is the primary verification method for graphical tests.
It searches all labels for the expected text.
It searches all labels for the expected text (substring match).
Args:
obj: LVGL object to search (typically lv.screen_active())
@@ -192,6 +240,10 @@ def verify_text_present(obj, expected_text):
Returns:
bool: True if text found, False otherwise
Example:
assert verify_text_present(lv.screen_active(), "Settings")
assert verify_text_present(lv.screen_active(), "Version")
"""
return find_label_with_text(obj, expected_text) is not None
@@ -201,9 +253,21 @@ def print_screen_labels(obj):
Debug helper: Print all label text found on screen.
Useful for debugging tests to see what text is actually present.
Prints to stdout with numbered list.
Args:
obj: LVGL object to search (typically lv.screen_active())
Example:
# When a test fails, use this to see what's on screen
print_screen_labels(lv.screen_active())
# Output:
# Found 5 labels on screen:
# 0: MicroPythonOS
# 1: Version 0.3.3
# 2: Settings
# 3: About
# 4: WiFi
"""
texts = get_screen_text_content(obj)
print(f"Found {len(texts)} labels on screen:")
@@ -216,7 +280,7 @@ def _touch_read_cb(indev_drv, data):
Internal callback for simulated touch input device.
This callback is registered with LVGL and provides touch state
when simulate_click() is used.
when simulate_click() is used. Not intended for direct use.
Args:
indev_drv: Input device driver (LVGL internal)
@@ -237,6 +301,7 @@ def _ensure_touch_indev():
This is called automatically by simulate_click() on first use.
Creates a pointer-type input device that uses _touch_read_cb.
Not intended for direct use.
"""
global _touch_indev
if _touch_indev is None:
@@ -255,7 +320,13 @@ def simulate_click(x, y, press_duration_ms=50):
processed through LVGL's normal input handling, so it triggers
click events, focus changes, scrolling, etc. just like real input.
To find object coordinates for clicking, use:
Useful for:
- Automated testing of UI interactions
- Demo modes in apps
- Accessibility automation
- Integration testing
To find object coordinates for clicking:
obj_area = lv.area_t()
obj.get_coords(obj_area)
center_x = (obj_area.x1 + obj_area.x2) // 2
@@ -268,13 +339,17 @@ def simulate_click(x, y, press_duration_ms=50):
press_duration_ms: How long to hold the press (default: 50ms)
Example:
from mpos.ui.testing import simulate_click, wait_for_render
# Click at screen center (320x240)
simulate_click(160, 120)
wait_for_render()
# Click on a specific button
button_area = lv.area_t()
button.get_coords(button_area)
my_button.get_coords(button_area)
simulate_click(button_area.x1 + 10, button_area.y1 + 10)
wait_for_render()
"""
global _touch_x, _touch_y, _touch_pressed
+1 -1
View File
@@ -10,7 +10,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestAbcButtonDebug(unittest.TestCase):
+1 -1
View File
@@ -21,7 +21,7 @@ import mpos.apps
import mpos.info
import mpos.ui
import os
from graphical_test_helper import (
from mpos.ui.testing import (
wait_for_render,
capture_screenshot,
find_label_with_text,
@@ -19,7 +19,7 @@ import unittest
import lvgl as lv
import mpos.ui.anim
import time
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestAnimationDeletedWidget(unittest.TestCase):
+1 -1
View File
@@ -14,7 +14,7 @@ import lvgl as lv
import sys
import os
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import (
from mpos.ui.testing import (
wait_for_render,
capture_screenshot,
)
@@ -11,7 +11,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import simulate_click, wait_for_render
from mpos.ui.testing import simulate_click, wait_for_render
class TestMposKeyboard(unittest.TestCase):
@@ -10,7 +10,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestKeyboardCrash(unittest.TestCase):
@@ -11,7 +11,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestDefaultVsCustomKeyboard(unittest.TestCase):
@@ -12,7 +12,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestKeyboardLayoutSwitching(unittest.TestCase):
+1 -1
View File
@@ -12,7 +12,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestKeyboardModeSwitch(unittest.TestCase):
@@ -12,7 +12,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestRapidModeSwitching(unittest.TestCase):
+1 -1
View File
@@ -22,7 +22,7 @@ import mpos.ui
import mpos.config
import sys
import os
from graphical_test_helper import (
from mpos.ui.testing import (
wait_for_render,
capture_screenshot,
)
+1 -3
View File
@@ -12,10 +12,8 @@ import time
# This is a graphical test - needs boot and main to run first
# Add tests directory to path for helpers
if '../tests' not in sys.path:
sys.path.insert(0, '../tests')
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
import mpos.apps
import mpos.ui
from mpos.content.package_manager import PackageManager
+1 -1
View File
@@ -6,7 +6,7 @@ import sys
import os
# Import graphical test helper
from graphical_test_helper import (
from mpos.ui.testing import (
wait_for_render,
capture_screenshot,
find_label_with_text,
+1 -1
View File
@@ -15,7 +15,7 @@ Usage:
import unittest
import mpos.apps
import mpos.ui
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestStartApp(unittest.TestCase):
+1 -1
View File
@@ -12,7 +12,7 @@ Usage:
import unittest
import lvgl as lv
from mpos.ui.keyboard import MposKeyboard
from graphical_test_helper import wait_for_render
from mpos.ui.testing import wait_for_render
class TestWiFiKeyboard(unittest.TestCase):