Skip to content

Commit a31ac2f

Browse files
Update tests
1 parent 8a931e0 commit a31ac2f

File tree

5 files changed

+615
-101
lines changed

5 files changed

+615
-101
lines changed

internal_filesystem/lib/mpos/ui/testing.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,3 +774,100 @@ def click_label(label_text, timeout=5, use_send_event=True):
774774
def find_text_on_screen(text):
775775
"""Check if text is present on screen."""
776776
return find_label_with_text(lv.screen_active(), text) is not None
777+
778+
779+
def click_keyboard_button(keyboard, button_text, use_direct=True):
780+
"""
781+
Click a keyboard button reliably.
782+
783+
This function handles the complexity of clicking keyboard buttons.
784+
For MposKeyboard, it directly manipulates the textarea (most reliable).
785+
For raw lv.keyboard, it uses simulate_click with coordinates.
786+
787+
Args:
788+
keyboard: MposKeyboard instance or lv.keyboard widget
789+
button_text: Text of the button to click (e.g., "q", "a", "1")
790+
use_direct: If True (default), directly manipulate textarea for MposKeyboard.
791+
If False, use simulate_click with coordinates.
792+
793+
Returns:
794+
bool: True if button was found and clicked, False otherwise
795+
796+
Example:
797+
from mpos.ui.keyboard import MposKeyboard
798+
from mpos.ui.testing import click_keyboard_button, wait_for_render
799+
800+
keyboard = MposKeyboard(screen)
801+
keyboard.set_textarea(textarea)
802+
803+
# Click the 'q' button
804+
success = click_keyboard_button(keyboard, "q")
805+
wait_for_render(10)
806+
807+
# Verify text was added
808+
assert textarea.get_text() == "q"
809+
"""
810+
# Check if this is an MposKeyboard wrapper
811+
is_mpos_keyboard = hasattr(keyboard, '_keyboard') and hasattr(keyboard, '_textarea')
812+
813+
if is_mpos_keyboard:
814+
lvgl_keyboard = keyboard._keyboard
815+
else:
816+
lvgl_keyboard = keyboard
817+
818+
# Find button index by searching through all buttons
819+
button_idx = None
820+
for i in range(100): # Check up to 100 buttons
821+
try:
822+
text = lvgl_keyboard.get_button_text(i)
823+
if text == button_text:
824+
button_idx = i
825+
break
826+
except:
827+
break # No more buttons
828+
829+
if button_idx is None:
830+
print(f"click_keyboard_button: Button '{button_text}' not found on keyboard")
831+
return False
832+
833+
if use_direct and is_mpos_keyboard:
834+
# For MposKeyboard, directly manipulate the textarea
835+
# This is the most reliable approach for testing
836+
textarea = keyboard._textarea
837+
if textarea is None:
838+
print(f"click_keyboard_button: No textarea connected to keyboard")
839+
return False
840+
841+
current_text = textarea.get_text()
842+
843+
# Handle special keys (matching keyboard.py logic)
844+
if button_text == lv.SYMBOL.BACKSPACE:
845+
new_text = current_text[:-1]
846+
elif button_text == " " or button_text == keyboard.LABEL_SPACE:
847+
new_text = current_text + " "
848+
elif button_text in [lv.SYMBOL.UP, lv.SYMBOL.DOWN, keyboard.LABEL_LETTERS,
849+
keyboard.LABEL_NUMBERS_SPECIALS, keyboard.LABEL_SPECIALS,
850+
lv.SYMBOL.OK]:
851+
# Mode switching or OK - don't modify text
852+
print(f"click_keyboard_button: '{button_text}' is a control key, not adding to textarea")
853+
wait_for_render(10)
854+
return True
855+
else:
856+
# Regular character
857+
new_text = current_text + button_text
858+
859+
textarea.set_text(new_text)
860+
wait_for_render(10)
861+
print(f"click_keyboard_button: Clicked '{button_text}' at index {button_idx} using direct textarea manipulation")
862+
else:
863+
# Use coordinate-based clicking
864+
coords = get_keyboard_button_coords(keyboard, button_text)
865+
if coords:
866+
simulate_click(coords['center_x'], coords['center_y'])
867+
wait_for_render(20) # More time for event processing
868+
print(f"click_keyboard_button: Clicked '{button_text}' at ({coords['center_x']}, {coords['center_y']}) using simulate_click")
869+
else:
870+
print(f"click_keyboard_button: Could not get coordinates for '{button_text}'")
871+
return False
872+
873+
return True

tests/base/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
Base test classes for MicroPythonOS testing.
3+
4+
This module provides base classes that encapsulate common test patterns:
5+
- GraphicalTestBase: For tests that require LVGL/UI
6+
- KeyboardTestBase: For tests that involve keyboard interaction
7+
8+
Usage:
9+
from base import GraphicalTestBase, KeyboardTestBase
10+
11+
class TestMyApp(GraphicalTestBase):
12+
def test_something(self):
13+
# self.screen is already set up
14+
# self.screenshot_dir is configured
15+
pass
16+
"""
17+
18+
from .graphical_test_base import GraphicalTestBase
19+
from .keyboard_test_base import KeyboardTestBase
20+
21+
__all__ = [
22+
'GraphicalTestBase',
23+
'KeyboardTestBase',
24+
]

tests/base/graphical_test_base.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
"""
2+
Base class for graphical tests in MicroPythonOS.
3+
4+
This class provides common setup/teardown patterns for tests that require
5+
LVGL/UI initialization. It handles:
6+
- Screen creation and cleanup
7+
- Screenshot directory configuration
8+
- Common UI testing utilities
9+
10+
Usage:
11+
from base import GraphicalTestBase
12+
13+
class TestMyApp(GraphicalTestBase):
14+
def test_something(self):
15+
# self.screen is already set up (320x240)
16+
# self.screenshot_dir is configured
17+
label = lv.label(self.screen)
18+
label.set_text("Hello")
19+
self.wait_for_render()
20+
self.capture_screenshot("my_test")
21+
"""
22+
23+
import unittest
24+
import lvgl as lv
25+
import sys
26+
import os
27+
28+
29+
class GraphicalTestBase(unittest.TestCase):
30+
"""
31+
Base class for all graphical tests.
32+
33+
Provides:
34+
- Automatic screen creation and cleanup
35+
- Screenshot directory configuration
36+
- Common UI testing utilities
37+
38+
Class Attributes:
39+
SCREEN_WIDTH: Default screen width (320)
40+
SCREEN_HEIGHT: Default screen height (240)
41+
DEFAULT_RENDER_ITERATIONS: Default iterations for wait_for_render (5)
42+
43+
Instance Attributes:
44+
screen: The LVGL screen object for the test
45+
screenshot_dir: Path to the screenshots directory
46+
"""
47+
48+
SCREEN_WIDTH = 320
49+
SCREEN_HEIGHT = 240
50+
DEFAULT_RENDER_ITERATIONS = 5
51+
52+
@classmethod
53+
def setUpClass(cls):
54+
"""
55+
Set up class-level fixtures.
56+
57+
Configures the screenshot directory based on platform.
58+
"""
59+
# Determine screenshot directory based on platform
60+
if sys.platform == "esp32":
61+
cls.screenshot_dir = "tests/screenshots"
62+
else:
63+
# On desktop, tests directory is in parent
64+
cls.screenshot_dir = "../tests/screenshots"
65+
66+
# Ensure screenshots directory exists
67+
try:
68+
os.mkdir(cls.screenshot_dir)
69+
except OSError:
70+
pass # Directory already exists
71+
72+
def setUp(self):
73+
"""
74+
Set up test fixtures before each test method.
75+
76+
Creates a new screen and loads it.
77+
"""
78+
# Create and load a new screen
79+
self.screen = lv.obj()
80+
self.screen.set_size(self.SCREEN_WIDTH, self.SCREEN_HEIGHT)
81+
lv.screen_load(self.screen)
82+
self.wait_for_render()
83+
84+
def tearDown(self):
85+
"""
86+
Clean up after each test method.
87+
88+
Loads an empty screen to clean up.
89+
"""
90+
# Load an empty screen to clean up
91+
lv.screen_load(lv.obj())
92+
self.wait_for_render()
93+
94+
def wait_for_render(self, iterations=None):
95+
"""
96+
Wait for LVGL to render.
97+
98+
Args:
99+
iterations: Number of render iterations (default: DEFAULT_RENDER_ITERATIONS)
100+
"""
101+
from mpos.ui.testing import wait_for_render
102+
if iterations is None:
103+
iterations = self.DEFAULT_RENDER_ITERATIONS
104+
wait_for_render(iterations)
105+
106+
def capture_screenshot(self, name, width=None, height=None):
107+
"""
108+
Capture a screenshot with standardized naming.
109+
110+
Args:
111+
name: Name for the screenshot (without extension)
112+
width: Screenshot width (default: SCREEN_WIDTH)
113+
height: Screenshot height (default: SCREEN_HEIGHT)
114+
115+
Returns:
116+
bytes: The screenshot buffer
117+
"""
118+
from mpos.ui.testing import capture_screenshot
119+
120+
if width is None:
121+
width = self.SCREEN_WIDTH
122+
if height is None:
123+
height = self.SCREEN_HEIGHT
124+
125+
path = f"{self.screenshot_dir}/{name}.raw"
126+
return capture_screenshot(path, width=width, height=height)
127+
128+
def find_label_with_text(self, text, parent=None):
129+
"""
130+
Find a label containing the specified text.
131+
132+
Args:
133+
text: Text to search for
134+
parent: Parent widget to search in (default: current screen)
135+
136+
Returns:
137+
The label widget if found, None otherwise
138+
"""
139+
from mpos.ui.testing import find_label_with_text
140+
if parent is None:
141+
parent = lv.screen_active()
142+
return find_label_with_text(parent, text)
143+
144+
def verify_text_present(self, text, parent=None):
145+
"""
146+
Verify that text is present on screen.
147+
148+
Args:
149+
text: Text to search for
150+
parent: Parent widget to search in (default: current screen)
151+
152+
Returns:
153+
bool: True if text is found
154+
"""
155+
from mpos.ui.testing import verify_text_present
156+
if parent is None:
157+
parent = lv.screen_active()
158+
return verify_text_present(parent, text)
159+
160+
def print_screen_labels(self, parent=None):
161+
"""
162+
Print all labels on screen (for debugging).
163+
164+
Args:
165+
parent: Parent widget to search in (default: current screen)
166+
"""
167+
from mpos.ui.testing import print_screen_labels
168+
if parent is None:
169+
parent = lv.screen_active()
170+
print_screen_labels(parent)
171+
172+
def click_button(self, text, use_send_event=True):
173+
"""
174+
Click a button by its text.
175+
176+
Args:
177+
text: Button text to find and click
178+
use_send_event: If True, use send_event (more reliable)
179+
180+
Returns:
181+
bool: True if button was found and clicked
182+
"""
183+
from mpos.ui.testing import click_button
184+
return click_button(text, use_send_event=use_send_event)
185+
186+
def click_label(self, text, use_send_event=True):
187+
"""
188+
Click a label by its text.
189+
190+
Args:
191+
text: Label text to find and click
192+
use_send_event: If True, use send_event (more reliable)
193+
194+
Returns:
195+
bool: True if label was found and clicked
196+
"""
197+
from mpos.ui.testing import click_label
198+
return click_label(text, use_send_event=use_send_event)
199+
200+
def simulate_click(self, x, y):
201+
"""
202+
Simulate a click at specific coordinates.
203+
204+
Note: For most UI testing, prefer click_button() or click_label()
205+
which are more reliable. Use this only when testing touch behavior.
206+
207+
Args:
208+
x: X coordinate
209+
y: Y coordinate
210+
"""
211+
from mpos.ui.testing import simulate_click
212+
simulate_click(x, y)
213+
self.wait_for_render()
214+
215+
def assertTextPresent(self, text, msg=None):
216+
"""
217+
Assert that text is present on screen.
218+
219+
Args:
220+
text: Text to search for
221+
msg: Optional failure message
222+
"""
223+
if msg is None:
224+
msg = f"Text '{text}' not found on screen"
225+
self.assertTrue(self.verify_text_present(text), msg)
226+
227+
def assertTextNotPresent(self, text, msg=None):
228+
"""
229+
Assert that text is NOT present on screen.
230+
231+
Args:
232+
text: Text to search for
233+
msg: Optional failure message
234+
"""
235+
if msg is None:
236+
msg = f"Text '{text}' should not be on screen"
237+
self.assertFalse(self.verify_text_present(text), msg)

0 commit comments

Comments
 (0)