"""Paginated native controls retain callbacks, fresh state, and network authority.""" import ast from collections.abc import Callable from enum import Enum from pathlib import Path import sys from types import SimpleNamespace as NS, ModuleType import unittest from unittest.mock import Mock, patch import pyray as rl from openpilot.starpilot.ui.feature_settings_state import ( FEATURE_ROW_TOP, FEATURE_ROW_HEIGHT, FeatureInput, FeatureUiAction, feature_row_top, value_buttons, value_done_rect, ) from openpilot.starpilot.ui.starpilot_settings_adapter_large import StarPilotSettingsAdapterLarge from openpilot.starpilot.ui.network_panel import NetworkPanelBridge from openpilot.starpilot.ui.tests.test_feature_settings_visuals import fake_fonts from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos from openpilot.system.ui.lib.wifi_manager import Network, SecurityType, WifiManager from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.list_view import ButtonAction, DualButtonAction, ItemAction, MultipleButtonAction, ToggleAction from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.network import NetworkUI, PanelType, UIState, WifiManagerUI def item(title, action=None): return NS(title=title, action_item=action, enabled=True, is_visible=True, callback=Mock(), description="Details", description_opened_callback=None) def button_action(): action = Mock(spec=ButtonAction) action.text, action.value, action.enabled = "OPEN", "Status", True return action class NativeSettingsPageTests(unittest.TestCase): def test_initial_cruise_editor_uses_real_control_and_params_in_both_units(self): import tempfile from openpilot.common.params import Params from openpilot.selfdrive.ui.layouts.settings import initial_cruise_speed as initial_speed with tempfile.TemporaryDirectory() as directory: params = Params(directory) ui = NS(params=params, is_metric=True, initial_cruise_speed=80, CP=NS(pcmCruise=False), has_longitudinal_control=True) def refresh(): ui.is_metric = params.get_bool("IsMetric") ui.initial_cruise_speed = params.get("InitialCruiseSpeedKph", return_default=True) row.is_visible = ui.CP is not None and ui.has_longitudinal_control and not ui.CP.pcmCruise ui.update_params = refresh with patch.object(initial_speed, "ui_state", ui): for metric in (True, False): with self.subTest(metric=metric): params.put_bool("IsMetric", metric, block=True) params.put("InitialCruiseSpeedKph", 80, block=True) ui.is_metric, ui.initial_cruise_speed = metric, 80 with patch.object(gui_app, 'font', return_value=rl.Font()): control = initial_speed.InitialCruiseSpeedControl() row = item("Initial Max Speed", control) pane = self.panel([row]) pane.title = "Toggles" pane.panel._update_toggles = refresh refresh() before = params.get("InitialCruiseSpeedKph") displayed = pane.snapshot().rows[0] self.assertTrue(displayed.available) self.assertTrue(displayed.value.endswith("km/h" if metric else "mph")) self.assertTrue(displayed.presets) self.click(pane) self.assertTrue(pane.snapshot().editor) self.choose(pane, "+") self.assertGreater(params.get("InitialCruiseSpeedKph"), before) editor = pane.snapshot() minimum, maximum = editor.rows[0].choices[0], editor.rows[0].choices[-1] self.choose(pane, minimum) self.assertEqual(params.get("InitialCruiseSpeedKph"), 40) self.choose(pane, "-") self.assertEqual(params.get("InitialCruiseSpeedKph"), 40) self.choose(pane, maximum) self.assertEqual(params.get("InitialCruiseSpeedKph"), 145) self.choose(pane, "+") self.assertEqual(params.get("InitialCruiseSpeedKph"), 145) # Native callback rejects an input tied to an earlier displayed value. stale = pane.snapshot().rows[0] params.put("InitialCruiseSpeedKph", 100, block=True) pane._emit(FeatureUiAction("change", stale, -1)) self.assertEqual(params.get("InitialCruiseSpeedKph"), 100) current = pane.snapshot().rows[0] control.set_enabled(False) pane._emit(FeatureUiAction("change", current, -1)) self.assertEqual(params.get("InitialCruiseSpeedKph"), 100) control.set_enabled(True) current = pane.snapshot().rows[0] ui.CP.pcmCruise = True pane._emit(FeatureUiAction("change", current, -1)) self.assertEqual(params.get("InitialCruiseSpeedKph"), 100) self.assertEqual(pane.snapshot().rows, ()) ui.CP.pcmCruise = False refresh() # Done uses the existing touch router and keeps the parent panel alive. self.choose(pane, "-") state = pane.snapshot() x, y, width, height = value_done_rect(state) pos = MousePos(x + width / 2, y + height / 2) pane._handle_mouse_press(pos) pane._handle_mouse_release(pos) self.assertFalse(pane.snapshot().editor) self.assertTrue(pane.active) pane.hide_event() def panel(self, items): owner = NS(_scroller=NS(_items=items), show_event=Mock(), hide_event=Mock(), _update_state=Mock()) pane = StarPilotSettingsAdapterLarge(owner, fake_fonts(), "Device", Mock()) pane.show_event() return pane def click(self, pane, x=1930, index=0): pos = MousePos(x + pane.origin[0], FEATURE_ROW_TOP + (index + .5) * FEATURE_ROW_HEIGHT + pane.origin[1]) pane._handle_mouse_press(pos) pane._handle_mouse_release(pos) def choose(self, pane, value): state = pane.snapshot() _, (x, y, width, height), _ = next(button for button in value_buttons(state, state.rows[0], feature_row_top(state)) if button[0] == value) pos = MousePos(x + width / 2 + pane.origin[0], y + height / 2 + pane.origin[1]) pane._handle_mouse_press(pos) pane._handle_mouse_release(pos) def test_native_render_forwards_current_drag(self): pane = self.panel([item(str(index), button_action()) for index in range(7)]) pane._handle_mouse_press(MousePos(1000, 200)) pane._handle_mouse_event(MouseEvent(MousePos(920, 200), 0, False, False, True, 1)) self.assertEqual(pane.input.drag_x, -80) with patch.object(pane.view, "render") as render, patch("openpilot.starpilot.ui.starpilot_settings_adapter_large.placed_at"): pane._render(rl.Rectangle(0, 0, 2160, 1080)) render.assert_called_once_with(pane.snapshot(), -80) pane.input.cancel() pane._render(rl.Rectangle(0, 0, 2160, 1080)) self.assertEqual(render.call_args.args[1], 0) def test_native_panels_forward_feedback_and_clear_it_when_hidden_or_disabled(self): pane = self.panel([item("Update", button_action())]) state = pane.snapshot() pos = MousePos(1930, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2) pane._handle_mouse_press(pos) self.assertEqual(pane.view.pressed_action(state), FeatureUiAction("action", state.rows[0], 0)) pane.set_enabled(False) self.assertIsNone(pane.view.pressed_action(state)) pane.hide_event() pane.show_event() self.assertIsNone(pane.view.pressed_action(pane.snapshot())) def test_press_during_transition_finishes_it_without_activating_replaced_rows(self): rows = [item(str(index), button_action()) for index in range(7)] pane = self.panel(rows) start = pane.snapshot() pane.view._page_frame(start, 0, False) pane._emit(FeatureUiAction("scroll")) with patch("openpilot.starpilot.ui.feature_settings.time.monotonic", return_value=0): pane.view._page_frame(pane.snapshot(), 0, False) self.click(pane) rows[5].callback.assert_not_called() self.assertIsNone(pane.view._transition) self.click(pane) rows[5].callback.assert_not_called() pane.view._page_frame(pane.snapshot(), 0, False) self.click(pane) rows[5].callback.assert_called_once() # Footer navigation remains usable during the transition. pane.view._page_frame(start, 0, False) pos = MousePos(1720, 1015) pane._handle_mouse_press(pos) pane._handle_mouse_release(pos) self.assertEqual(pane.scroll, 5) pane.hide_event() self.assertIsNone(pane.view._last_state) pane.show_event() self.assertIsNone(pane.view._last_state) def test_every_row_reachable_and_page_clamps_when_visibility_changes(self): items = [item(str(index), button_action()) for index in range(12)] pane = self.panel(items) reached = [] for _page in range(3): state = pane.snapshot() reached.extend(row.label for row in state.rows[state.scroll:state.scroll + 5]) pane._emit(FeatureUiAction("scroll")) self.assertEqual(reached, [str(index) for index in range(12)]) self.assertEqual(pane.scroll, 10) for row in items[5:]: row.is_visible = False self.assertEqual(pane.snapshot().scroll, 0) pane.show_event() self.assertEqual(pane.scroll, 0) self.assertFalse(hasattr(pane, "scroll_panel")) def test_swipes_started_during_transition_preserve_recognition_and_suppress_row_actions(self): for direction in (1, -1): for distance, duration, accepted in ((200, .5, True), (80, .075, True), (80, .5, False)): with self.subTest(direction=direction, distance=distance, duration=duration): rows = [item(str(index), button_action()) for index in range(15)] pane = self.panel(rows) pane.scroll = 0 if direction == 1 else 10 with patch("openpilot.starpilot.ui.feature_settings.time.monotonic", return_value=0): pane.view._page_frame(pane.snapshot(), 0, False) pane.scroll = 5 pane.view._page_frame(pane.snapshot(), 0, False) self.assertIsNotNone(pane.view._transition) x = 1930 if direction == 1 else 1800 pane._handle_mouse_press(MousePos(x, 200)) for timestamp, fraction in ((0, 0), (duration / 2, .5), (duration, 1)): pane._handle_mouse_event(MouseEvent(MousePos(x - direction * distance * fraction, 200), 0, False, False, True, timestamp)) pane._handle_mouse_release(MousePos(x - direction * distance, 200)) self.assertEqual(pane.scroll, 5 + direction * 5 if accepted else 5) self.assertTrue(all(not row.callback.called for row in rows)) def test_buttons_cancel_on_disabled_hidden_changed_page_and_drag(self): row = item("Update", button_action()) pane = self.panel([row, *[item(str(index), button_action()) for index in range(6)]]) pos = MousePos(1930, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2) for change in (lambda: setattr(row.action_item, "enabled", False), lambda: setattr(row, "is_visible", False), lambda: setattr(pane, "scroll", 5)): pane._handle_mouse_press(pos) change() pane._handle_mouse_release(pos) row.callback.assert_not_called() row.action_item.enabled, row.is_visible, pane.scroll = True, True, 0 pane._handle_mouse_press(pos) pane._handle_mouse_event(MouseEvent(MousePos(1930, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2 + 40), 0, False, False, True, 1)) pane._handle_mouse_release(pos) row.callback.assert_not_called() self.click(pane) row.callback.assert_called_once_with() def test_footer_and_actions_use_the_rendered_origin_in_both_sidebar_modes(self): rows = [item(str(index), button_action()) for index in range(7)] pane = self.panel(rows) for width, left in ((1560, 550), (2060, 50)): with patch.object(Widget, "render"): pane.render(rl.Rectangle(left + 40, 55, width, 1030)) pos = MousePos(1720 + pane.origin[0], 1015 + pane.origin[1]) pane._handle_mouse_press(pos) pane._handle_mouse_release(pos) self.assertEqual(pane.scroll, 5) self.click(pane) pos = MousePos(900 + pane.origin[0], 1015 + pane.origin[1]) pane._handle_mouse_press(pos) pane._handle_mouse_release(pos) self.assertEqual(pane.scroll, 0) self.assertEqual(rows[5].callback.call_count, 2) def test_widget_dispatch_keeps_pagination_and_buttons_clickable_without_scrolling(self): rows = [item(str(index), button_action()) for index in range(7)] pane = self.panel(rows) bounds = rl.Rectangle(550, 25, 1560, 1030) with (patch.object(pane, '_render'), patch.object(gui_app, '_show_touches', False), patch('openpilot.system.ui.widgets.device', NS(awake=True)), patch.object(rl, 'get_mouse_wheel_move', return_value=-10) as wheel): for x, y in ((1720, 1015), (1930, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2), (900, 1015)): for pressed, released in ((True, False), (False, True)): event = MouseEvent(MousePos(x, y), 0, pressed, released, pressed, 1) with patch.object(gui_app, '_mouse_events', [event]): pane.render(bounds) rows[5].callback.assert_called_once_with() self.assertEqual(pane.scroll, 0) wheel.assert_not_called() def test_widget_swipes_use_rendered_origin_and_never_activate_rows(self): for width, left in ((1560, 550), (2060, 50)): with self.subTest(width=width): rows = [item(str(index), button_action()) for index in range(13)] rows[5].action_item.enabled = False pane = self.panel(rows) bounds = rl.Rectangle(left + 40, 55, width, 1030) with (patch.object(pane, '_render'), patch.object(gui_app, '_show_touches', False), patch('openpilot.system.ui.widgets.device', NS(awake=True)), patch.object(gui_app, '_mouse_events', []), patch.object(gui_app, 'push_widget') as dialog): pane.render(bounds) for start, end, index, expected in ((2000, 1700, 0, 5), (2000, 1700, 0, 10), (2000, 1700, 4, 10), (1800, 2100, 4, 5), (1800, 2100, 0, 0), (1800, 2100, 0, 0)): y = FEATURE_ROW_TOP + (index + .5) * FEATURE_ROW_HEIGHT + pane.origin[1] for x, pressed, released in ((start, True, False), (end, False, False), (end, False, True)): event = MouseEvent(MousePos(x + pane.origin[0], y), 0, pressed, released, not released, 1) with patch.object(gui_app, '_mouse_events', [event]): pane.render(bounds) self.assertEqual(pane.scroll, expected) dialog.assert_not_called() for row in rows: row.callback.assert_not_called() def test_batched_flicks_use_event_times_in_both_sidebar_modes(self): for width, left in ((1560, 550), (2060, 50)): rows = [item(str(index), button_action()) for index in range(13)] pane = self.panel(rows) bounds = rl.Rectangle(left + 40, 55, width, 1030) with (patch.object(pane, '_render'), patch.object(gui_app, '_show_touches', False), patch('openpilot.system.ui.widgets.device', NS(awake=True)), patch.object(gui_app, '_mouse_events', [])): pane.render(bounds) for duration, last_down, expected in ((.075, 80, 5), (.5, 80, 5), (.075, 55, 5)): events = [MouseEvent(MousePos(2000 - distance + pane.origin[0], 200 + pane.origin[1]), 0, index == 0, index == 3, index != 3, 1 + timestamp) for index, (timestamp, distance) in enumerate(((0, 0), (duration / 2, last_down / 2), (duration, last_down), (duration + .01, 80)))] with patch.object(gui_app, '_mouse_events', events): pane.render(bounds) self.assertEqual(pane.scroll, expected) for row in rows: row.callback.assert_not_called() def test_widget_swipes_cross_panel_boundaries_without_activating_rows(self): for width, left in ((1560, 550), (2060, 50)): outside = 400 if width == 1560 else 10 edge = 545 if width == 1560 else 45 cases = ((900, (outside,), outside, 0, 5), (900, (), outside, 0, 5), # release outside without a move (900, (outside,), 600, 0, 5), # reenter on release (1900, (2145,), 2145, 5, 0), (edge + 15, (edge - 35,), edge - 35, 0, 0), # short drift (outside, (900,), 900, 0, 0), # first page (outside, (600, 900), 900, 5, 0 if width == 1560 else 5), (outside, (), 900, 5, 0 if width == 1560 else 5), # enter on release (outside, (900,), outside, 5, 5), # finish in sidebar (outside, (900,), 2145, 5, 5), # finish beyond panel (edge - 26, (edge + 15, edge + 54), edge + 54, 5, 0 if width == 1560 else 5), # fast entry flick (edge + 60, (edge + 25, edge - 10, edge - 45), edge - 45, 0, 5)) # fast flick for start, moves, end, initial, expected in cases: with self.subTest(width=width, start=start, end=end, moves=moves): rows = [item(str(index), button_action()) for index in range(7)] pane = self.panel(rows) pane.scroll = initial with (patch.object(pane, '_render'), patch.object(gui_app, '_show_touches', False), patch('openpilot.system.ui.widgets.device', NS(awake=True)), patch.object(gui_app, '_mouse_events', []), patch.object(gui_app, 'push_widget') as dialog, patch.object(pane.view, 'reset', wraps=pane.view.reset) as reset): pane.render(rl.Rectangle(left + 40, 55, width, 1030)) positions = (start, *moves, end) for index, x in enumerate(positions): released = index == len(positions) - 1 event = MouseEvent(MousePos(x + pane.origin[0], 200 + pane.origin[1]), 0, index == 0, released, not released, 1 + index * .025) with patch.object(gui_app, '_mouse_events', [event]): pane.render() self.assertEqual(pane.scroll, expected) self.assertIsNone(pane.input.held) if start < (520 if width == 1560 else 20): reset.assert_not_called() dialog.assert_not_called() for row in rows: row.callback.assert_not_called() def test_sidebar_swipes_respect_parent_clipping_and_widget_input_gates(self): for clipped, valid, enabled, awake, slot in ((True, True, True, True, 0), (False, False, True, True, 0), (False, True, False, True, 0), (False, True, True, False, 0), (False, True, True, True, 1)): with self.subTest(clipped=clipped, valid=valid, enabled=enabled, awake=awake, slot=slot): rows = [item(str(index), button_action()) for index in range(7)] pane = self.panel(rows) pane.scroll = 5 with (patch.object(pane, '_render'), patch.object(gui_app, '_show_touches', False), patch('openpilot.system.ui.widgets.device', NS(awake=awake)), patch.object(gui_app, '_mouse_events', [])): pane.render(rl.Rectangle(550, 25, 1560, 1030)) if clipped: pane.set_parent_rect(pane.rect) pane.set_enabled(enabled) pane.set_touch_valid_callback(lambda valid=valid: valid) events = [MouseEvent(MousePos(x, 200), slot, i == 0, i == 2, i != 2, 1 + i * .025) for i, x in enumerate((400, 900, 900))] with patch.object(gui_app, '_mouse_events', events): pane.render() self.assertEqual(pane.scroll, 5) self.assertIsNone(pane.input.held) for row in rows: row.callback.assert_not_called() def test_widget_release_does_not_restore_a_disabled_tap_or_repeat_an_action(self): row = item("Update", button_action()) pane = self.panel([row]) pos = MousePos(1930, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2) with (patch.object(pane, '_render'), patch.object(gui_app, '_show_touches', False), patch('openpilot.system.ui.widgets.device', NS(awake=True)), patch.object(gui_app, '_mouse_events', [])): pane.render(rl.Rectangle(550, 25, 1560, 1030)) with patch.object(gui_app, '_mouse_events', [MouseEvent(pos, 0, True, False, True, 1)]): pane.render() pane.set_enabled(False) pane.render() pane.set_enabled(True) with patch.object(gui_app, '_mouse_events', [MouseEvent(pos, 0, False, True, False, 2)]): pane.render() row.callback.assert_not_called() self.assertIsNone(pane.input.held) for pressed, released in ((True, False), (False, True), (False, True)): with patch.object(gui_app, '_mouse_events', [MouseEvent(pos, 0, pressed, released, pressed, 3)]): pane.render() row.callback.assert_called_once_with() def test_toggle_calls_its_native_callback_once_and_respects_displayed_value(self): action = ToggleAction(callback=Mock()) row = item("Automatic updates", action) row.callback = None pane = self.panel([row]) pane._handle_mouse_press(MousePos(2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2)) action.set_state(True) pane._handle_mouse_release(MousePos(2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2)) action.toggle._callback.assert_not_called() self.click(pane, 2000) action.toggle._callback.assert_called_once_with(False) def test_power_buttons_remain_distinct_and_hidden_shutdown_is_not_exposed(self): with patch('openpilot.system.ui.lib.application.gui_app.font', return_value=rl.Font()): action = DualButtonAction("Reboot", "Power Off", left_callback=Mock(), right_callback=Mock()) pane = self.panel([item("", action)]) self.click(pane, 1500) self.click(pane) action.left_button._click_callback.assert_called_once_with() action.right_button._click_callback.assert_called_once_with() action.right_button.set_visible(False) self.assertEqual(pane.snapshot().rows[0].actions, (("Reboot", True),)) def test_choices_reuse_feature_controls_and_cancel_when_stale_disabled_or_inactive(self): with patch('openpilot.system.ui.lib.application.gui_app.font', return_value=rl.Font()): action = MultipleButtonAction(["Default", "Metered", "Unmetered"], 255, callback=Mock()) pane = self.panel([item("Metered", action)]) self.assertEqual(pane.snapshot().rows[0].choices, tuple(action.buttons)) pos = MousePos(2000, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT / 2) pane._handle_mouse_press(pos) action.selected_button = 1 pane._handle_mouse_release(pos) action.callback.assert_not_called() self.click(pane, 2000) self.assertTrue(pane.snapshot().editor) self.choose(pane, 'Unmetered') action.callback.assert_called_once_with(2) self.choose(pane, 'Metered') self.assertEqual(action.callback.call_args.args, (1,)) action.callback.reset_mock() action.set_enabled(False) self.choose(pane, 'Unmetered') action.set_enabled(True) pane.hide_event() self.click(pane, 2000) action.callback.assert_not_called() def test_developer_ssh_preserves_native_fetcher_loading_and_add_remove_actions(self): # Execute the actual SSH control with its Params/network dependencies isolated. source = Path('openpilot/selfdrive/ui/widgets/ssh_key.py') nodes: list[ast.stmt] = [node for node in ast.parse(source.read_text()).body if isinstance(node, ast.ClassDef) and node.name in ('SshKeyActionState', 'SshKeyAction')] params = NS(get=lambda _: '', remove=Mock()) module = ModuleType('openpilot.selfdrive.ui.widgets.ssh_key') module.__dict__.update(Enum=Enum, ItemAction=ItemAction, Params=lambda: params, SshKeyFetcher=lambda _: Mock(), Keyboard=lambda **_: Mock(), gui_app=gui_app, Button=Button, ButtonStyle=ButtonStyle, FontWeight=NS(NORMAL=0), Callable=Callable, DialogResult=DialogResult, rl=rl, MousePos=MousePos, tr=lambda value: value, tr_noop=lambda value: value, BUTTON_BORDER_RADIUS=50, BUTTON_FONT_SIZE=35) exec(compile(ast.Module(body=nodes, type_ignores=[]), str(source), 'exec'), module.__dict__) with patch.dict(sys.modules, {module.__name__: module}), patch.object(gui_app, 'font', return_value=rl.Font()): action = module.SshKeyAction() pane = self.panel([item('SSH Keys', action)]) pane.title = 'Developer' self.assertEqual(pane.snapshot().rows[0].actions, (('ADD', True),)) with patch.object(gui_app, 'push_widget') as pushed: self.click(pane) pushed.assert_called_once_with(action._keyboard) action._state = module.SshKeyActionState.LOADING self.assertEqual(pane.snapshot().rows[0].actions, (('LOADING', False),)) self.click(pane) action._fetcher.clear.assert_not_called() pane._update_state() action._fetcher.update.assert_called_once() action._state, action._username = module.SshKeyActionState.REMOVE, 'my-user' self.assertEqual(pane.snapshot().rows[0].value, 'my-user') self.click(pane) action._fetcher.clear.assert_called_once() def test_toggles_keep_native_fresh_state_request_guards(self): action = ToggleAction(callback=Mock()) row = item('Enable openpilot', action) with patch.object(gui_app, 'font', return_value=rl.Font()): personality = MultipleButtonAction(['Aggressive', 'Standard', 'Relaxed'], 255, selected_index=1, callback=Mock()) pane = self.panel([row, item('Driving Personality', personality)]) pane.title = 'Toggles' pane.panel._toggles = {'OpenpilotEnabledToggle': row} pane.panel.request_toggle, pane.panel.request_personality = Mock(return_value=False), Mock(return_value=False) self.click(pane, 2000) pane.panel.request_toggle.assert_called_once_with('OpenpilotEnabledToggle', True) action.toggle._callback.assert_not_called() self.assertFalse(action.get_state()) self.click(pane, 2000, index=1) self.choose(pane, 'Relaxed') pane.panel.request_personality.assert_called_once_with(2) personality.callback.assert_not_called() self.assertEqual(personality.selected_button, 1) def test_toggles_saved_state_matches_display_after_changes(self): # Run the real controller methods with only device services isolated. source = Path('openpilot/selfdrive/ui/layouts/settings/toggles.py') layout = next(node for node in ast.parse(source.read_text()).body if isinstance(node, ast.ClassDef) and node.name == 'TogglesLayout') methods = {'_update_toggles', '_update_state', '_toggle_callback', 'request_toggle', 'request_personality', '_set_longitudinal_personality'} layout.bases = [] layout.body = [node for node in layout.body if isinstance(node, ast.FunctionDef) and node.name in methods] ui = NS(CP=None, engaged=False, update_params=Mock(), sm=NS(updated={'selfdriveState': False})) app = NS(push_widget=Mock()) namespace = ModuleType('toggles_controller_test') namespace.__dict__.update(tr=lambda text: text, ui_state=ui) exec(compile(ast.Module(body=[layout], type_ignores=[]), str(source), 'exec'), namespace.__dict__) owner = namespace.TogglesLayout() saved = {'IsLdwEnabled': False, 'LongitudinalPersonality': 1} owner._params = NS(get_bool=lambda key: bool(saved.get(key, False)), get=lambda key, **_: saved.get(key), put_bool=lambda key, value, **_: saved.__setitem__(key, value), put=lambda key, value, **_: saved.__setitem__(key, value)) toggle = item('Lane Departure Warnings', ToggleAction()) experimental = item('Experimental Mode', ToggleAction()) experimental.set_description = Mock() with patch.object(gui_app, 'font', return_value=rl.Font()): personality = item('Driving Personality', MultipleButtonAction(['Aggressive', 'Standard', 'Relaxed'], 255, selected_index=1)) owner._toggles = {'IsLdwEnabled': toggle, 'ExperimentalMode': experimental} owner._toggle_defs = dict.fromkeys(owner._toggles, ('', '', '', False)) owner._locked_toggles = set() owner._slc_offsets = NS(_raw=lambda key: b'0') owner._update_experimental_mode_icon = Mock() owner._long_personality_setting = personality owner._initial_cruise_speed = NS(action_item=NS(refresh=Mock())) owner._scroller = NS(_items=[toggle, personality, experimental]) pane = self.panel(owner._scroller._items) pane.panel, pane.title = owner, 'Toggles' for desired in (True, False): self.click(pane, 2000) pane._update_state() self.assertEqual(saved['IsLdwEnabled'], desired) self.assertEqual(pane.snapshot().rows[0].value, 'On' if desired else 'Off') self.click(pane, 2000, index=1) for expected in (2, 0, 1): self.choose(pane, personality.action_item.buttons[expected]) pane._update_state() self.assertEqual(saved['LongitudinalPersonality'], expected) self.assertEqual(pane.snapshot().rows[0].value, personality.action_item.buttons[expected]) pane._emit(FeatureUiAction('back')) owner._toggle_defs['IsLdwEnabled'] = ('', '', '', True) ui.engaged = True self.click(pane, 2000) self.assertFalse(saved['IsLdwEnabled']) self.assertEqual(pane.snapshot().rows[0].value, 'Off') for desired in (True, False): self.click(pane, 2000, index=2) pane._update_state() self.assertEqual(saved['ExperimentalMode'], desired) self.assertEqual(pane.snapshot().rows[2].value, 'On' if desired else 'Off') app.push_widget.assert_not_called() def test_native_choice_editor_preserves_position_done_and_touch_guards(self): with patch.object(gui_app, 'font', return_value=rl.Font()): action = MultipleButtonAction(['Default', 'Metered', 'Unmetered'], 255, callback=Mock()) pane = self.panel([*(item(str(i), button_action()) for i in range(5)), item('Wi-Fi Network Metered', action)]) pane.scroll = 5 self.click(pane) state = pane.snapshot() row = state.rows[0] self.assertTrue(state.editor) self.assertEqual(state.parent_title, 'Device') _, (x, y, width, height), _ = next(button for button in value_buttons(state, row, feature_row_top(state)) if button[0] == 'Unmetered') pos = MousePos(x + width / 2, y + height / 2) pane._handle_mouse_press(pos) action.set_selected_button(1) pane._handle_mouse_release(pos) action.callback.assert_not_called() state = pane.snapshot() x, y, width, height = value_done_rect(state) done = MousePos(x + width / 2, y + height / 2) pane._handle_mouse_press(done) pane._handle_mouse_release(done) self.assertFalse(pane.snapshot().editor) self.assertEqual(pane.scroll, 5) pane.on_back.assert_not_called() def network_panel(self): manager = Mock(spec=WifiManager, is_connection_saved=lambda ssid: ssid == "Saved", connected_ssid="Saved", wifi_state=NS(ssid="Saved"), set_active=Mock(), process_callbacks=Mock(), activate_connection=Mock(), connect_to_network=Mock(), forget_connection=Mock()) wifi = WifiManagerUI.__new__(WifiManagerUI) Widget.__init__(wifi) wifi._wifi_manager, wifi._action_guard = manager, None wifi.state, wifi._state_network = UIState.IDLE, None wifi._networks = [Network("Saved", 90, SecurityType.OPEN, False), Network("Open", 50, SecurityType.OPEN, False), Network("Locked", 40, SecurityType.WPA, False)] owner = NetworkUI.__new__(NetworkUI) Widget.__init__(owner) owner._wifi_panel, owner._advanced_panel = wifi, Mock() owner._advanced_panel._scroller = NS(_items=[]) owner._current_panel, owner._action_guard = PanelType.WIFI, None owner._children = [wifi] pane = StarPilotSettingsAdapterLarge(owner, fake_fonts(), "Network", Mock()) allowed = [True] bridge = NetworkPanelBridge(owner, lambda: allowed[0]) bridge.presentation = pane self.assertTrue(bridge.enter()) return pane, bridge, wifi, manager, allowed def test_network_scan_connect_password_forget_and_authority_use_existing_owner(self): pane, bridge, wifi, manager, allowed = self.network_panel() manager.set_active.assert_called_once_with(True) self.assertIsNone(FeatureInput.target(1500, FEATURE_ROW_TOP + FEATURE_ROW_HEIGHT * 1.5, pane.snapshot())) # connected network: Connect disabled self.click(pane, index=1) self.assertEqual(wifi.state, UIState.SHOW_FORGET_CONFIRM) wifi.state, wifi._state_network = UIState.IDLE, None self.click(pane, index=2) manager.connect_to_network.assert_called_once_with('Open', '') wifi.state, wifi._state_network = UIState.IDLE, None self.click(pane, index=3) self.assertEqual(wifi.state, UIState.NEEDS_AUTH) allowed[0] = False wifi.forget_network(wifi._networks[0]) manager.forget_connection.assert_not_called() bridge.leave() manager.set_active.assert_called_with(False) self.assertFalse(pane.active) def test_network_tabs_reset_pages_and_back_returns_to_wifi(self): pane, bridge, wifi, manager, allowed = self.network_panel() self.click(pane) self.assertEqual(pane.panel._current_panel, PanelType.ADVANCED) pane.scroll = 5 pane._emit(FeatureUiAction("back")) self.assertEqual(pane.panel._current_panel, PanelType.WIFI) self.assertEqual(pane.scroll, 0) pane.on_back.assert_not_called() pane._emit(FeatureUiAction("back")) pane.on_back.assert_called_once_with() bridge.leave() if __name__ == '__main__': unittest.main()