diff --git a/internal_filesystem/lib/mpos/ui/testing.py b/internal_filesystem/lib/mpos/ui/testing.py index ba405f42..385efb52 100644 --- a/internal_filesystem/lib/mpos/ui/testing.py +++ b/internal_filesystem/lib/mpos/ui/testing.py @@ -275,6 +275,83 @@ def print_screen_labels(obj): print(f" {i}: {text}") +def get_widget_coords(widget): + """ + Get the coordinates of a widget. + + Returns the bounding box coordinates of the widget, useful for + clicking on it or verifying its position. + + Args: + widget: LVGL widget object + + Returns: + dict: Dictionary with keys 'x1', 'y1', 'x2', 'y2', 'center_x', 'center_y' + Returns None if widget is invalid or has no coordinates + + Example: + # Find and click on a button + button = find_label_with_text(lv.screen_active(), "Submit") + if button: + coords = get_widget_coords(button.get_parent()) # Get parent button + if coords: + simulate_click(coords['center_x'], coords['center_y']) + """ + try: + area = lv.area_t() + widget.get_coords(area) + return { + 'x1': area.x1, + 'y1': area.y1, + 'x2': area.x2, + 'y2': area.y2, + 'center_x': (area.x1 + area.x2) // 2, + 'center_y': (area.y1 + area.y2) // 2, + 'width': area.x2 - area.x1, + 'height': area.y2 - area.y1, + } + except: + return None + + +def find_button_with_text(obj, search_text): + """ + Find a button widget containing specific text in its label. + + This is specifically for finding buttons (which contain labels as children) + rather than just labels. Very useful for testing UI interactions. + + Args: + obj: LVGL object to search (typically lv.screen_active()) + search_text: Text to search for in button labels (can be substring) + + Returns: + LVGL button object if found, None otherwise + + Example: + submit_btn = find_button_with_text(lv.screen_active(), "Submit") + if submit_btn: + coords = get_widget_coords(submit_btn) + simulate_click(coords['center_x'], coords['center_y']) + """ + # Find the label first + label = find_label_with_text(obj, search_text) + if label: + # Try to get the parent button + try: + parent = label.get_parent() + # Check if parent is a button + if parent.get_class() == lv.button_class: + return parent + # Sometimes there's an extra container layer + grandparent = parent.get_parent() + if grandparent and grandparent.get_class() == lv.button_class: + return grandparent + except: + pass + return None + + def _touch_read_cb(indev_drv, data): """ Internal callback for simulated touch input device. diff --git a/tests/test_graphical_keyboard_q_button_bug.py b/tests/test_graphical_keyboard_q_button_bug.py new file mode 100644 index 00000000..ad119df8 --- /dev/null +++ b/tests/test_graphical_keyboard_q_button_bug.py @@ -0,0 +1,236 @@ +""" +Test for keyboard "q" button bug. + +This test reproduces the issue where typing "q" on the keyboard results in +the button lighting up but no character being added to the textarea, while +the "a" button beneath it works correctly. + +The test uses helper functions to locate buttons by their text, get their +coordinates, and simulate clicks using simulate_click(). + +Usage: + Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py + Device: ./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py --ondevice +""" + +import unittest +import lvgl as lv +from mpos.ui.keyboard import MposKeyboard +from mpos.ui.testing import ( + wait_for_render, + find_button_with_text, + get_widget_coords, + simulate_click, + print_screen_labels +) + + +class TestKeyboardQButtonBug(unittest.TestCase): + """Test keyboard 'q' button behavior vs 'a' button.""" + + def setUp(self): + """Set up test fixtures.""" + self.screen = lv.obj() + self.screen.set_size(320, 240) + lv.screen_load(self.screen) + wait_for_render(5) + + def tearDown(self): + """Clean up.""" + lv.screen_load(lv.obj()) + wait_for_render(5) + + def test_q_button_bug(self): + """ + Test that clicking the 'q' button adds 'q' to textarea. + + This test demonstrates the bug where: + 1. Clicking 'q' button lights it up but doesn't add to textarea + 2. Clicking 'a' button works correctly + + Steps: + 1. Create textarea and keyboard + 2. Find 'q' button index in keyboard map + 3. Get button coordinates from keyboard widget + 4. Click it using simulate_click() + 5. Verify 'q' appears in textarea (EXPECTED TO FAIL due to bug) + 6. Repeat with 'a' button + 7. Verify 'a' appears correctly (EXPECTED TO PASS) + """ + print("\n=== Testing keyboard 'q' and 'a' button behavior ===") + + # Create textarea + textarea = lv.textarea(self.screen) + textarea.set_size(200, 30) + textarea.set_one_line(True) + textarea.align(lv.ALIGN.TOP_MID, 0, 10) + textarea.set_text("") # Start empty + wait_for_render(5) + + # Create keyboard and connect to textarea + keyboard = MposKeyboard(self.screen) + keyboard.set_textarea(textarea) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + print(f"Initial textarea: '{textarea.get_text()}'") + self.assertEqual(textarea.get_text(), "", "Textarea should start empty") + + # --- Test 'q' button --- + print("\n--- Testing 'q' button ---") + + # Find button index for 'q' in the keyboard + q_button_id = None + for i in range(100): # Check first 100 button indices + try: + text = keyboard.get_button_text(i) + if text == "q": + q_button_id = i + print(f"Found 'q' button at index {i}") + break + except: + break # No more buttons + + self.assertIsNotNone(q_button_id, "Should find 'q' button on keyboard") + + # Get the keyboard widget coordinates to calculate button position + keyboard_area = lv.area_t() + keyboard.get_coords(keyboard_area) + print(f"Keyboard area: x1={keyboard_area.x1}, y1={keyboard_area.y1}, x2={keyboard_area.x2}, y2={keyboard_area.y2}") + + # LVGL keyboards organize buttons in a grid + # From the map: "q" is at index 0, in top row (10 buttons per row) + # Let's estimate position based on keyboard layout + # Top row starts at y1 + some padding, each button is ~width/10 + keyboard_width = keyboard_area.x2 - keyboard_area.x1 + keyboard_height = keyboard_area.y2 - keyboard_area.y1 + button_width = keyboard_width // 10 # ~10 buttons per row + button_height = keyboard_height // 4 # ~4 rows + + # 'q' is first button (index 0), top row + q_x = keyboard_area.x1 + button_width // 2 + q_y = keyboard_area.y1 + button_height // 2 + + print(f"Estimated 'q' button position: ({q_x}, {q_y})") + + # Click the 'q' button + print(f"Clicking 'q' button at ({q_x}, {q_y})") + simulate_click(q_x, q_y) + wait_for_render(10) + + # Check textarea content + text_after_q = textarea.get_text() + print(f"Textarea after clicking 'q': '{text_after_q}'") + + # THIS IS THE BUG: 'q' should be added but isn't + if text_after_q != "q": + print("BUG REPRODUCED: 'q' button was clicked but 'q' was NOT added to textarea!") + print("Expected: 'q'") + print(f"Got: '{text_after_q}'") + + self.assertEqual(text_after_q, "q", + "Clicking 'q' button should add 'q' to textarea (BUG: This test will fail)") + + # --- Test 'a' button for comparison --- + print("\n--- Testing 'a' button (for comparison) ---") + + # Clear textarea + textarea.set_text("") + wait_for_render(5) + print("Cleared textarea") + + # Find button index for 'a' + a_button_id = None + for i in range(100): + try: + text = keyboard.get_button_text(i) + if text == "a": + a_button_id = i + print(f"Found 'a' button at index {i}") + break + except: + break + + self.assertIsNotNone(a_button_id, "Should find 'a' button on keyboard") + + # 'a' is at index 11 (second row, first position) + a_x = keyboard_area.x1 + button_width // 2 + a_y = keyboard_area.y1 + button_height + button_height // 2 + + print(f"Estimated 'a' button position: ({a_x}, {a_y})") + + # Click the 'a' button + print(f"Clicking 'a' button at ({a_x}, {a_y})") + simulate_click(a_x, a_y) + wait_for_render(10) + + # Check textarea content + text_after_a = textarea.get_text() + print(f"Textarea after clicking 'a': '{text_after_a}'") + + # The 'a' button should work correctly + self.assertEqual(text_after_a, "a", + "Clicking 'a' button should add 'a' to textarea (should PASS)") + + print("\nSummary:") + print(f" 'q' button result: '{text_after_q}' (expected 'q')") + print(f" 'a' button result: '{text_after_a}' (expected 'a')") + if text_after_q != "q" and text_after_a == "a": + print(" BUG CONFIRMED: 'q' doesn't work but 'a' does!") + + def test_keyboard_button_discovery(self): + """ + Debug test: Discover all buttons on the keyboard. + + This test helps understand the keyboard layout and button structure. + It prints all found buttons and their text. + """ + print("\n=== Discovering keyboard buttons ===") + + # Create keyboard without textarea to inspect it + keyboard = MposKeyboard(self.screen) + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) + wait_for_render(10) + + # Iterate through button indices to find all buttons + print("\nEnumerating keyboard buttons by index:") + found_buttons = [] + + for i in range(100): # Check first 100 indices + try: + text = keyboard.get_button_text(i) + if text: # Skip None/empty + found_buttons.append((i, text)) + # Only print first 20 to avoid clutter + if i < 20: + print(f" Button {i}: '{text}'") + except: + # No more buttons + break + + if len(found_buttons) > 20: + print(f" ... (showing first 20 of {len(found_buttons)} buttons)") + + print(f"\nTotal buttons found: {len(found_buttons)}") + + # Try to find specific letters + letters_to_test = ['q', 'w', 'e', 'r', 'a', 's', 'd', 'f'] + print("\nLooking for specific letters:") + + for letter in letters_to_test: + found = False + for idx, text in found_buttons: + if text == letter: + print(f" '{letter}' at index {idx}") + found = True + break + if not found: + print(f" '{letter}' NOT FOUND") + + # Verify we can find at least some buttons + self.assertTrue(len(found_buttons) > 0, + "Should find at least some buttons on keyboard") + + +if __name__ == "__main__": + unittest.main()