diff --git a/internal_filesystem/lib/mpos/__init__.py b/internal_filesystem/lib/mpos/__init__.py index 0f58334f..82ce41c0 100644 --- a/internal_filesystem/lib/mpos/__init__.py +++ b/internal_filesystem/lib/mpos/__init__.py @@ -34,7 +34,9 @@ from .ui.testing import ( wait_for_render, capture_screenshot, simulate_click, get_widget_coords, find_label_with_text, verify_text_present, print_screen_labels, find_text_on_screen, click_button, click_label, click_keyboard_button, find_button_with_text, - get_all_widgets_with_text + get_all_widgets_with_text, find_setting_value_label, get_setting_value_text, + verify_setting_value_text, find_dropdown_widget, get_dropdown_options, + find_dropdown_option_index, select_dropdown_option_by_text ) # UI utility functions @@ -92,7 +94,9 @@ __all__ = [ "wait_for_render", "capture_screenshot", "simulate_click", "get_widget_coords", "find_label_with_text", "verify_text_present", "print_screen_labels", "find_text_on_screen", "click_button", "click_label", "click_keyboard_button", "find_button_with_text", - "get_all_widgets_with_text", + "get_all_widgets_with_text", "find_setting_value_label", "get_setting_value_text", + "verify_setting_value_text", "find_dropdown_widget", "get_dropdown_options", + "find_dropdown_option_index", "select_dropdown_option_by_text", # Submodules "ui", "config", "net", "content", "time", "sensor_manager", "camera_manager", "sdcard", "audio", "hardware", diff --git a/internal_filesystem/lib/mpos/ui/testing.py b/internal_filesystem/lib/mpos/ui/testing.py index 0c3b2a78..2cfc600b 100644 --- a/internal_filesystem/lib/mpos/ui/testing.py +++ b/internal_filesystem/lib/mpos/ui/testing.py @@ -259,7 +259,6 @@ def get_screen_text_content(obj): pass # Error getting text return texts - def verify_text_present(obj, expected_text): """ Verify that expected text is present somewhere on screen. @@ -281,6 +280,87 @@ def verify_text_present(obj, expected_text): return find_label_with_text(obj, expected_text) is not None +def find_setting_value_label(obj, setting_title_text): + """ + Find the value label associated with a SettingsActivity setting title. + + SettingsActivity renders each setting as a container with two labels: + a title label (large) and a value label (smaller) directly below it. + This helper finds the title label, then returns the sibling value label. + + Args: + obj: LVGL object to search (typically lv.screen_active()) + setting_title_text: Text of the setting title (exact or substring) + + Returns: + LVGL label object for the value if found, None otherwise + + Example: + value_label = find_setting_value_label(lv.screen_active(), "Auth Mode") + if value_label: + assert value_label.get_text() == "(defaults to none)" + """ + title_label = find_label_with_text(obj, setting_title_text) + if not title_label: + return None + try: + parent = title_label.get_parent() + if not parent: + return None + child_count = parent.get_child_count() + for i in range(child_count): + child = parent.get_child(i) + if child is title_label: + continue + try: + if hasattr(child, "get_text"): + text = child.get_text() + if text: + return child + except: + pass + except: + pass + return None + + +def get_setting_value_text(obj, setting_title_text): + """ + Get the value text associated with a SettingsActivity setting title. + + Args: + obj: LVGL object to search (typically lv.screen_active()) + setting_title_text: Text of the setting title (exact or substring) + + Returns: + str or None: The value label text if found + """ + value_label = find_setting_value_label(obj, setting_title_text) + if value_label: + try: + return value_label.get_text() + except: + return None + return None + + +def verify_setting_value_text(obj, setting_title_text, expected_text): + """ + Verify a SettingsActivity value label matches expected text. + + Args: + obj: LVGL object to search (typically lv.screen_active()) + setting_title_text: Text of the setting title (exact or substring) + expected_text: Expected text for the value label (exact match) + + Returns: + bool: True if value label text matches expected, False otherwise + """ + value_text = get_setting_value_text(obj, setting_title_text) + return value_text == expected_text + + + def text_to_hex(text): """ Convert text to hex representation for debugging. @@ -414,6 +494,116 @@ def find_button_with_text(obj, search_text): return None +def find_dropdown_widget(obj): + """ + Find a dropdown widget in the object hierarchy. + + Args: + obj: LVGL object to search (typically lv.screen_active()) + + Returns: + LVGL dropdown object if found, None otherwise + """ + def find_dropdown_recursive(node): + try: + if node.__class__.__name__ == "dropdown" or hasattr(node, "get_selected"): + if hasattr(node, "get_options"): + return node + except: + pass + + try: + child_count = node.get_child_count() + except: + return None + + for i in range(child_count): + child = node.get_child(i) + result = find_dropdown_recursive(child) + if result: + return result + return None + + return find_dropdown_recursive(obj) + + +def get_dropdown_options(dropdown): + """ + Get dropdown options as a list of strings. + + Args: + dropdown: LVGL dropdown widget + + Returns: + list: List of option strings (order preserved) + """ + try: + options = dropdown.get_options() + if options: + lines = options.split("\n") + return [line for line in lines if line] + except: + pass + return [] + + +def find_dropdown_option_index(dropdown, option_text, allow_partial=True): + """ + Find the index of an option in a dropdown by text. + + Args: + dropdown: LVGL dropdown widget + option_text: Text to search for + allow_partial: If True, match substring (default: True) + + Returns: + int or None: Index of matching option + """ + options = get_dropdown_options(dropdown) + if options: + for idx, text in enumerate(options): + if (allow_partial and option_text in text) or (not allow_partial and option_text == text): + return idx + return None + + try: + option_count = dropdown.get_option_count() + except: + option_count = 0 + + for idx in range(option_count): + try: + text = dropdown.get_option_text(idx) + if (allow_partial and option_text in text) or (not allow_partial and option_text == text): + return idx + except: + pass + + return None + + +def select_dropdown_option_by_text(dropdown, option_text, allow_partial=True): + """ + Select a dropdown option by its text. + + Args: + dropdown: LVGL dropdown widget + option_text: Text to select + allow_partial: If True, match substring (default: True) + + Returns: + bool: True if option was found and selected + """ + idx = find_dropdown_option_index(dropdown, option_text, allow_partial=allow_partial) + if idx is None: + return False + try: + dropdown.set_selected(idx) + return True + except: + return False + + def get_keyboard_button_coords(keyboard, button_text): """ Get the coordinates of a specific button on an LVGL keyboard/buttonmatrix. diff --git a/tests/test_graphical_hotspot_password.py b/tests/test_graphical_hotspot_password.py new file mode 100644 index 00000000..28604986 --- /dev/null +++ b/tests/test_graphical_hotspot_password.py @@ -0,0 +1,155 @@ +""" +Graphical test for hotspot settings password defaults. + +This test verifies that the hotspot settings screen shows the +"(defaults to none)" value under the "Auth Mode" setting. + +Usage: + Desktop: ./tests/unittest.sh tests/test_graphical_hotspot_password.py + Device: ./tests/unittest.sh tests/test_graphical_hotspot_password.py --ondevice +""" + +import unittest +import lvgl as lv +import mpos.ui +from mpos import ( + AppManager, + wait_for_render, + print_screen_labels, + click_button, + verify_text_present, + find_setting_value_label, + get_setting_value_text, + click_label, + simulate_click, + get_widget_coords, + select_dropdown_option_by_text, + find_dropdown_widget, + SharedPreferences, +) + + +class TestGraphicalHotspotPassword(unittest.TestCase): + """Test suite for hotspot password defaults in settings UI.""" + + def _reset_hotspot_preferences(self): + """Clear hotspot preferences to ensure default values are shown.""" + prefs = SharedPreferences("com.micropythonos.settings.hotspot") + editor = prefs.edit() + editor.remove_all() + editor.commit() + + def _open_hotspot_settings_screen(self): + """Start hotspot app and open the Settings screen.""" + result = AppManager.start_app("com.micropythonos.settings.hotspot") + self.assertTrue(result, "Failed to start hotspot settings app") + wait_for_render(iterations=20) + + screen = lv.screen_active() + print("\nInitial screen labels:") + print_screen_labels(screen) + + self.assertTrue( + click_button("Settings"), + "Could not find Settings button in hotspot app", + ) + wait_for_render(iterations=40) + + screen = lv.screen_active() + print("\nSettings screen labels:") + print_screen_labels(screen) + return screen + + def tearDown(self): + """Clean up after each test method.""" + # Navigate back to launcher to close any opened apps + try: + mpos.ui.back_screen() + wait_for_render(5) + except: + pass + + def test_auth_mode_defaults_label(self): + """Verify Auth Mode shows defaults to none in hotspot settings.""" + print("\n=== Starting Hotspot Settings Auth Mode default test ===") + + self._reset_hotspot_preferences() + screen = self._open_hotspot_settings_screen() + + self.assertTrue( + verify_text_present(screen, "Auth Mode"), + "Auth Mode setting title not found on settings screen", + ) + + value_label = find_setting_value_label(screen, "Auth Mode") + self.assertIsNotNone( + value_label, + "Could not find value label for Auth Mode setting", + ) + + value_text = get_setting_value_text(screen, "Auth Mode") + print(f"Auth Mode value text: {value_text}") + self.assertEqual( + value_text, + "(defaults to none)", + "Auth Mode value text did not match expected default", + ) + + print("\n=== Hotspot settings Auth Mode default test completed ===") + + def test_auth_mode_dropdown_select_wpa2(self): + """Change Auth Mode via dropdown and verify stored value label.""" + print("\n=== Starting Hotspot Settings Auth Mode dropdown test ===") + + self._reset_hotspot_preferences() + screen = self._open_hotspot_settings_screen() + + self.assertTrue( + click_label("Auth Mode"), + "Could not click Auth Mode setting", + ) + wait_for_render(iterations=40) + + screen = lv.screen_active() + print("\nAuth Mode edit screen labels:") + print_screen_labels(screen) + + dropdown = find_dropdown_widget(screen) + self.assertIsNotNone(dropdown, "Auth Mode dropdown not found") + + coords = get_widget_coords(dropdown) + self.assertIsNotNone(coords, "Could not get dropdown coordinates") + + print(f"Clicking dropdown at ({coords['center_x']}, {coords['center_y']})") + simulate_click(coords["center_x"], coords["center_y"], press_duration_ms=100) + wait_for_render(iterations=20) + + self.assertTrue( + select_dropdown_option_by_text(dropdown, "WPA2", allow_partial=True), + "Could not select WPA2 option in dropdown", + ) + wait_for_render(iterations=20) + + self.assertTrue( + click_button("Save"), + "Could not click Save button in Auth Mode settings", + ) + wait_for_render(iterations=40) + + screen = lv.screen_active() + print("\nSettings screen labels after save:") + print_screen_labels(screen) + + value_text = get_setting_value_text(screen, "Auth Mode") + print(f"Auth Mode value text after save: {value_text}") + self.assertEqual( + value_text, + "wpa2", + "Auth Mode value did not update to wpa2", + ) + + print("\n=== Hotspot settings Auth Mode dropdown test completed ===") + + +if __name__ == "__main__": + pass diff --git a/tests/test_graphical_imu_calibration_ui_bug.py b/tests/test_graphical_imu_calibration_ui_bug.py old mode 100755 new mode 100644