|
| 1 | +""" |
| 2 | +Test for keyboard "q" button bug. |
| 3 | +
|
| 4 | +This test reproduces the issue where typing "q" on the keyboard results in |
| 5 | +the button lighting up but no character being added to the textarea, while |
| 6 | +the "a" button beneath it works correctly. |
| 7 | +
|
| 8 | +The test uses helper functions to locate buttons by their text, get their |
| 9 | +coordinates, and simulate clicks using simulate_click(). |
| 10 | +
|
| 11 | +Usage: |
| 12 | + Desktop: ./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py |
| 13 | + Device: ./tests/unittest.sh tests/test_graphical_keyboard_q_button_bug.py --ondevice |
| 14 | +""" |
| 15 | + |
| 16 | +import unittest |
| 17 | +import lvgl as lv |
| 18 | +from mpos.ui.keyboard import MposKeyboard |
| 19 | +from mpos.ui.testing import ( |
| 20 | + wait_for_render, |
| 21 | + find_button_with_text, |
| 22 | + get_widget_coords, |
| 23 | + simulate_click, |
| 24 | + print_screen_labels |
| 25 | +) |
| 26 | + |
| 27 | + |
| 28 | +class TestKeyboardQButtonBug(unittest.TestCase): |
| 29 | + """Test keyboard 'q' button behavior vs 'a' button.""" |
| 30 | + |
| 31 | + def setUp(self): |
| 32 | + """Set up test fixtures.""" |
| 33 | + self.screen = lv.obj() |
| 34 | + self.screen.set_size(320, 240) |
| 35 | + lv.screen_load(self.screen) |
| 36 | + wait_for_render(5) |
| 37 | + |
| 38 | + def tearDown(self): |
| 39 | + """Clean up.""" |
| 40 | + lv.screen_load(lv.obj()) |
| 41 | + wait_for_render(5) |
| 42 | + |
| 43 | + def test_q_button_bug(self): |
| 44 | + """ |
| 45 | + Test that clicking the 'q' button adds 'q' to textarea. |
| 46 | +
|
| 47 | + This test demonstrates the bug where: |
| 48 | + 1. Clicking 'q' button lights it up but doesn't add to textarea |
| 49 | + 2. Clicking 'a' button works correctly |
| 50 | +
|
| 51 | + Steps: |
| 52 | + 1. Create textarea and keyboard |
| 53 | + 2. Find 'q' button index in keyboard map |
| 54 | + 3. Get button coordinates from keyboard widget |
| 55 | + 4. Click it using simulate_click() |
| 56 | + 5. Verify 'q' appears in textarea (EXPECTED TO FAIL due to bug) |
| 57 | + 6. Repeat with 'a' button |
| 58 | + 7. Verify 'a' appears correctly (EXPECTED TO PASS) |
| 59 | + """ |
| 60 | + print("\n=== Testing keyboard 'q' and 'a' button behavior ===") |
| 61 | + |
| 62 | + # Create textarea |
| 63 | + textarea = lv.textarea(self.screen) |
| 64 | + textarea.set_size(200, 30) |
| 65 | + textarea.set_one_line(True) |
| 66 | + textarea.align(lv.ALIGN.TOP_MID, 0, 10) |
| 67 | + textarea.set_text("") # Start empty |
| 68 | + wait_for_render(5) |
| 69 | + |
| 70 | + # Create keyboard and connect to textarea |
| 71 | + keyboard = MposKeyboard(self.screen) |
| 72 | + keyboard.set_textarea(textarea) |
| 73 | + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) |
| 74 | + wait_for_render(10) |
| 75 | + |
| 76 | + print(f"Initial textarea: '{textarea.get_text()}'") |
| 77 | + self.assertEqual(textarea.get_text(), "", "Textarea should start empty") |
| 78 | + |
| 79 | + # --- Test 'q' button --- |
| 80 | + print("\n--- Testing 'q' button ---") |
| 81 | + |
| 82 | + # Find button index for 'q' in the keyboard |
| 83 | + q_button_id = None |
| 84 | + for i in range(100): # Check first 100 button indices |
| 85 | + try: |
| 86 | + text = keyboard.get_button_text(i) |
| 87 | + if text == "q": |
| 88 | + q_button_id = i |
| 89 | + print(f"Found 'q' button at index {i}") |
| 90 | + break |
| 91 | + except: |
| 92 | + break # No more buttons |
| 93 | + |
| 94 | + self.assertIsNotNone(q_button_id, "Should find 'q' button on keyboard") |
| 95 | + |
| 96 | + # Get the keyboard widget coordinates to calculate button position |
| 97 | + keyboard_area = lv.area_t() |
| 98 | + keyboard.get_coords(keyboard_area) |
| 99 | + print(f"Keyboard area: x1={keyboard_area.x1}, y1={keyboard_area.y1}, x2={keyboard_area.x2}, y2={keyboard_area.y2}") |
| 100 | + |
| 101 | + # LVGL keyboards organize buttons in a grid |
| 102 | + # From the map: "q" is at index 0, in top row (10 buttons per row) |
| 103 | + # Let's estimate position based on keyboard layout |
| 104 | + # Top row starts at y1 + some padding, each button is ~width/10 |
| 105 | + keyboard_width = keyboard_area.x2 - keyboard_area.x1 |
| 106 | + keyboard_height = keyboard_area.y2 - keyboard_area.y1 |
| 107 | + button_width = keyboard_width // 10 # ~10 buttons per row |
| 108 | + button_height = keyboard_height // 4 # ~4 rows |
| 109 | + |
| 110 | + # 'q' is first button (index 0), top row |
| 111 | + q_x = keyboard_area.x1 + button_width // 2 |
| 112 | + q_y = keyboard_area.y1 + button_height // 2 |
| 113 | + |
| 114 | + print(f"Estimated 'q' button position: ({q_x}, {q_y})") |
| 115 | + |
| 116 | + # Click the 'q' button |
| 117 | + print(f"Clicking 'q' button at ({q_x}, {q_y})") |
| 118 | + simulate_click(q_x, q_y) |
| 119 | + wait_for_render(10) |
| 120 | + |
| 121 | + # Check textarea content |
| 122 | + text_after_q = textarea.get_text() |
| 123 | + print(f"Textarea after clicking 'q': '{text_after_q}'") |
| 124 | + |
| 125 | + # THIS IS THE BUG: 'q' should be added but isn't |
| 126 | + if text_after_q != "q": |
| 127 | + print("BUG REPRODUCED: 'q' button was clicked but 'q' was NOT added to textarea!") |
| 128 | + print("Expected: 'q'") |
| 129 | + print(f"Got: '{text_after_q}'") |
| 130 | + |
| 131 | + self.assertEqual(text_after_q, "q", |
| 132 | + "Clicking 'q' button should add 'q' to textarea (BUG: This test will fail)") |
| 133 | + |
| 134 | + # --- Test 'a' button for comparison --- |
| 135 | + print("\n--- Testing 'a' button (for comparison) ---") |
| 136 | + |
| 137 | + # Clear textarea |
| 138 | + textarea.set_text("") |
| 139 | + wait_for_render(5) |
| 140 | + print("Cleared textarea") |
| 141 | + |
| 142 | + # Find button index for 'a' |
| 143 | + a_button_id = None |
| 144 | + for i in range(100): |
| 145 | + try: |
| 146 | + text = keyboard.get_button_text(i) |
| 147 | + if text == "a": |
| 148 | + a_button_id = i |
| 149 | + print(f"Found 'a' button at index {i}") |
| 150 | + break |
| 151 | + except: |
| 152 | + break |
| 153 | + |
| 154 | + self.assertIsNotNone(a_button_id, "Should find 'a' button on keyboard") |
| 155 | + |
| 156 | + # 'a' is at index 11 (second row, first position) |
| 157 | + a_x = keyboard_area.x1 + button_width // 2 |
| 158 | + a_y = keyboard_area.y1 + button_height + button_height // 2 |
| 159 | + |
| 160 | + print(f"Estimated 'a' button position: ({a_x}, {a_y})") |
| 161 | + |
| 162 | + # Click the 'a' button |
| 163 | + print(f"Clicking 'a' button at ({a_x}, {a_y})") |
| 164 | + simulate_click(a_x, a_y) |
| 165 | + wait_for_render(10) |
| 166 | + |
| 167 | + # Check textarea content |
| 168 | + text_after_a = textarea.get_text() |
| 169 | + print(f"Textarea after clicking 'a': '{text_after_a}'") |
| 170 | + |
| 171 | + # The 'a' button should work correctly |
| 172 | + self.assertEqual(text_after_a, "a", |
| 173 | + "Clicking 'a' button should add 'a' to textarea (should PASS)") |
| 174 | + |
| 175 | + print("\nSummary:") |
| 176 | + print(f" 'q' button result: '{text_after_q}' (expected 'q')") |
| 177 | + print(f" 'a' button result: '{text_after_a}' (expected 'a')") |
| 178 | + if text_after_q != "q" and text_after_a == "a": |
| 179 | + print(" BUG CONFIRMED: 'q' doesn't work but 'a' does!") |
| 180 | + |
| 181 | + def test_keyboard_button_discovery(self): |
| 182 | + """ |
| 183 | + Debug test: Discover all buttons on the keyboard. |
| 184 | +
|
| 185 | + This test helps understand the keyboard layout and button structure. |
| 186 | + It prints all found buttons and their text. |
| 187 | + """ |
| 188 | + print("\n=== Discovering keyboard buttons ===") |
| 189 | + |
| 190 | + # Create keyboard without textarea to inspect it |
| 191 | + keyboard = MposKeyboard(self.screen) |
| 192 | + keyboard.align(lv.ALIGN.BOTTOM_MID, 0, 0) |
| 193 | + wait_for_render(10) |
| 194 | + |
| 195 | + # Iterate through button indices to find all buttons |
| 196 | + print("\nEnumerating keyboard buttons by index:") |
| 197 | + found_buttons = [] |
| 198 | + |
| 199 | + for i in range(100): # Check first 100 indices |
| 200 | + try: |
| 201 | + text = keyboard.get_button_text(i) |
| 202 | + if text: # Skip None/empty |
| 203 | + found_buttons.append((i, text)) |
| 204 | + # Only print first 20 to avoid clutter |
| 205 | + if i < 20: |
| 206 | + print(f" Button {i}: '{text}'") |
| 207 | + except: |
| 208 | + # No more buttons |
| 209 | + break |
| 210 | + |
| 211 | + if len(found_buttons) > 20: |
| 212 | + print(f" ... (showing first 20 of {len(found_buttons)} buttons)") |
| 213 | + |
| 214 | + print(f"\nTotal buttons found: {len(found_buttons)}") |
| 215 | + |
| 216 | + # Try to find specific letters |
| 217 | + letters_to_test = ['q', 'w', 'e', 'r', 'a', 's', 'd', 'f'] |
| 218 | + print("\nLooking for specific letters:") |
| 219 | + |
| 220 | + for letter in letters_to_test: |
| 221 | + found = False |
| 222 | + for idx, text in found_buttons: |
| 223 | + if text == letter: |
| 224 | + print(f" '{letter}' at index {idx}") |
| 225 | + found = True |
| 226 | + break |
| 227 | + if not found: |
| 228 | + print(f" '{letter}' NOT FOUND") |
| 229 | + |
| 230 | + # Verify we can find at least some buttons |
| 231 | + self.assertTrue(len(found_buttons) > 0, |
| 232 | + "Should find at least some buttons on keyboard") |
| 233 | + |
| 234 | + |
| 235 | +if __name__ == "__main__": |
| 236 | + unittest.main() |
0 commit comments