|
23 | 23 | setup_common_editor_mocks, |
24 | 24 | ) |
25 | 25 |
|
26 | | -from ardupilot_methodic_configurator.data_model_vehicle_components_validation import BATTERY_CELL_VOLTAGE_PATHS |
| 26 | +from ardupilot_methodic_configurator.data_model_vehicle_components_validation import ( |
| 27 | + BATTERY_CELL_VOLTAGE_PATHS, |
| 28 | + get_connection_type_tuples_with_labels, |
| 29 | +) |
27 | 30 | from ardupilot_methodic_configurator.frontend_tkinter_component_editor import ComponentEditorWindow |
28 | 31 | from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox |
29 | 32 |
|
30 | | -# pylint: disable=protected-access,redefined-outer-name |
| 33 | +# pylint: disable=protected-access,redefined-outer-name,too-many-lines |
31 | 34 |
|
32 | 35 |
|
33 | 36 | @pytest.fixture |
@@ -57,6 +60,24 @@ def editor_with_mocked_root() -> ComponentEditorWindow: |
57 | 60 | yield editor |
58 | 61 |
|
59 | 62 |
|
| 63 | +class _FakePairTupleCombobox(PairTupleCombobox): # pylint: disable=too-many-ancestors |
| 64 | + """Lightweight PairTupleCombobox-compatible test double.""" |
| 65 | + |
| 66 | + def __init__(self, selected_key: str) -> None: # pylint: disable=super-init-not-called |
| 67 | + self._selected_key = selected_key |
| 68 | + self.list_keys: list[str] = [selected_key] if selected_key else [] |
| 69 | + self.configure = MagicMock() |
| 70 | + self.update_idletasks = MagicMock() |
| 71 | + self.set_entries_tuple = MagicMock(side_effect=self._set_entries_tuple) |
| 72 | + |
| 73 | + def _set_entries_tuple(self, entries: list[tuple[str, str]], selection: str = "") -> None: |
| 74 | + self.list_keys = [key for key, _ in entries] |
| 75 | + self._selected_key = selection or "" |
| 76 | + |
| 77 | + def get_selected_key(self) -> str: |
| 78 | + return self._selected_key |
| 79 | + |
| 80 | + |
60 | 81 | class TestComponentEditorWindow: # pylint: disable=too-many-public-methods |
61 | 82 | """Test cases for ComponentEditorWindow class.""" |
62 | 83 |
|
@@ -831,3 +852,169 @@ def mock_set_entries_tuple(entries, selection) -> None: |
831 | 852 | # If SBUS is valid for RCin/SBUS, no error should occur |
832 | 853 | mock_show_error.assert_not_called() |
833 | 854 | assert result == "" |
| 855 | + |
| 856 | + def test_esc_fc_telemetry_type_widget_refreshes_to_same_serial_port_when_fc_esc_connection_changes_to_serial( |
| 857 | + self, editor_with_mocked_root |
| 858 | + ) -> None: |
| 859 | + """ |
| 860 | + ESC->FC Telemetry Type widget is refreshed to the new serial port when FC->ESC Connection type changes. |
| 861 | +
|
| 862 | + GIVEN: ESC FC->ESC Connection Type combobox is changed to a SERIAL port (e.g. SERIAL1) |
| 863 | + WHEN: update_component_protocol_combobox_entries is called with the new SERIAL port |
| 864 | + THEN: The ESC->FC Telemetry Type combobox is updated to display only that same SERIAL port |
| 865 | + AND: The displayed selection in the widget matches the SERIAL port that was chosen |
| 866 | + """ |
| 867 | + editor = editor_with_mocked_root |
| 868 | + serial_port = "SERIAL1" |
| 869 | + fc_esc_type_path = ("ESC", "FC->ESC Connection", "Type") |
| 870 | + fc_esc_proto_path = ("ESC", "FC->ESC Connection", "Protocol") |
| 871 | + telem_type_path = ("ESC", "ESC->FC Telemetry", "Type") |
| 872 | + telem_proto_path = ("ESC", "ESC->FC Telemetry", "Protocol") |
| 873 | + |
| 874 | + fc_esc_protocol = "FETtecOneWire" |
| 875 | + |
| 876 | + # GIVEN: Data model cascade returns SERIAL1 as the only valid Telemetry Type option |
| 877 | + # (what _update_esc_fc_connection_choices produces when FC->ESC is SERIAL) |
| 878 | + def combobox_values_for_path(path) -> tuple: |
| 879 | + return { |
| 880 | + fc_esc_proto_path: (fc_esc_protocol,), |
| 881 | + telem_type_path: (serial_port,), |
| 882 | + telem_proto_path: ("None",), |
| 883 | + }.get(path, ()) |
| 884 | + |
| 885 | + editor.data_model.get_combobox_values_for_path.side_effect = combobox_values_for_path |
| 886 | + editor.data_model.get_component_value = MagicMock( |
| 887 | + side_effect=lambda path: fc_esc_protocol if path == fc_esc_proto_path else serial_port |
| 888 | + ) |
| 889 | + |
| 890 | + # GIVEN: PairTupleCombobox-compatible test doubles are registered for all three paths |
| 891 | + fc_esc_proto_widget = _FakePairTupleCombobox(fc_esc_protocol) |
| 892 | + telem_type_widget = _FakePairTupleCombobox(serial_port) |
| 893 | + telem_proto_widget = _FakePairTupleCombobox("None") |
| 894 | + |
| 895 | + editor.entry_widgets[fc_esc_proto_path] = fc_esc_proto_widget |
| 896 | + editor.entry_widgets[telem_type_path] = telem_type_widget |
| 897 | + editor.entry_widgets[telem_proto_path] = telem_proto_widget |
| 898 | + |
| 899 | + # WHEN: User changes FC->ESC Connection Type to SERIAL1 |
| 900 | + editor.update_component_protocol_combobox_entries(fc_esc_type_path, serial_port) |
| 901 | + |
| 902 | + # THEN: ESC->FC Telemetry Type widget is refreshed with only the matching SERIAL port |
| 903 | + telem_type_widget.set_entries_tuple.assert_called_once_with( |
| 904 | + get_connection_type_tuples_with_labels((serial_port,)), serial_port |
| 905 | + ) |
| 906 | + |
| 907 | + def test_esc_fc_telemetry_comboboxes_are_disabled_when_fc_esc_connection_type_is_serial( |
| 908 | + self, editor_with_mocked_root |
| 909 | + ) -> None: |
| 910 | + """ |
| 911 | + ESC->FC Telemetry comboboxes are greyed-out (disabled) when FC->ESC Connection type is SERIAL. |
| 912 | +
|
| 913 | + GIVEN: ESC FC->ESC Connection Type is changed to a SERIAL port |
| 914 | + WHEN: update_component_protocol_combobox_entries is called |
| 915 | + THEN: Both ESC->FC Telemetry Type and Protocol comboboxes are set to state="disabled" |
| 916 | + AND: The user cannot independently change the Telemetry port (it is locked to the FC->ESC port) |
| 917 | + """ |
| 918 | + editor = editor_with_mocked_root |
| 919 | + serial_port = "SERIAL2" |
| 920 | + fc_esc_type_path = ("ESC", "FC->ESC Connection", "Type") |
| 921 | + fc_esc_proto_path = ("ESC", "FC->ESC Connection", "Protocol") |
| 922 | + telem_type_path = ("ESC", "ESC->FC Telemetry", "Type") |
| 923 | + telem_proto_path = ("ESC", "ESC->FC Telemetry", "Protocol") |
| 924 | + |
| 925 | + fc_esc_protocol = "FETtecOneWire" |
| 926 | + |
| 927 | + # GIVEN: Data model cascade returns the SERIAL port for both Telemetry fields |
| 928 | + def combobox_values_for_path(path) -> tuple: |
| 929 | + return { |
| 930 | + fc_esc_proto_path: (fc_esc_protocol,), |
| 931 | + telem_type_path: (serial_port,), |
| 932 | + telem_proto_path: ("None",), |
| 933 | + }.get(path, ()) |
| 934 | + |
| 935 | + editor.data_model.get_combobox_values_for_path.side_effect = combobox_values_for_path |
| 936 | + editor.data_model.get_component_value = MagicMock( |
| 937 | + side_effect=lambda path: fc_esc_protocol if path == fc_esc_proto_path else serial_port |
| 938 | + ) |
| 939 | + |
| 940 | + # GIVEN: PairTupleCombobox-compatible test doubles registered for the telemetry paths |
| 941 | + fc_esc_proto_widget = _FakePairTupleCombobox(fc_esc_protocol) |
| 942 | + telem_type_widget = _FakePairTupleCombobox(serial_port) |
| 943 | + telem_proto_widget = _FakePairTupleCombobox("None") |
| 944 | + |
| 945 | + editor.entry_widgets[fc_esc_proto_path] = fc_esc_proto_widget |
| 946 | + editor.entry_widgets[telem_type_path] = telem_type_widget |
| 947 | + editor.entry_widgets[telem_proto_path] = telem_proto_widget |
| 948 | + |
| 949 | + # WHEN: User changes FC->ESC Connection Type to a SERIAL port |
| 950 | + editor.update_component_protocol_combobox_entries(fc_esc_type_path, serial_port) |
| 951 | + |
| 952 | + # THEN: Both ESC->FC Telemetry comboboxes are disabled (user cannot change them independently) |
| 953 | + telem_type_widget.configure.assert_any_call(state="disabled") |
| 954 | + telem_proto_widget.configure.assert_any_call(state="disabled") |
| 955 | + |
| 956 | + def test_esc_fc_telemetry_protocol_mirrors_fc_esc_protocol_when_type_changes_to_serial( |
| 957 | + self, editor_with_mocked_root |
| 958 | + ) -> None: |
| 959 | + """ |
| 960 | + ESC->FC Telemetry Protocol is mirrored to FC->ESC Protocol when FC->ESC Type changes to SERIAL. |
| 961 | +
|
| 962 | + GIVEN: FC->ESC Protocol is already set to "FETtecOneWire" |
| 963 | + WHEN: FC->ESC Connection Type is changed to SERIAL1 |
| 964 | + THEN: ESC->FC Telemetry Protocol widget is updated to display "FETtecOneWire" (not "None") |
| 965 | + AND: The widget selection is set to the same protocol, not left stale |
| 966 | + """ |
| 967 | + editor = editor_with_mocked_root |
| 968 | + serial_port = "SERIAL1" |
| 969 | + fc_esc_protocol = "FETtecOneWire" |
| 970 | + fc_esc_type_path = ("ESC", "FC->ESC Connection", "Type") |
| 971 | + fc_esc_proto_path = ("ESC", "FC->ESC Connection", "Protocol") |
| 972 | + telem_type_path = ("ESC", "ESC->FC Telemetry", "Type") |
| 973 | + telem_proto_path = ("ESC", "ESC->FC Telemetry", "Protocol") |
| 974 | + |
| 975 | + # GIVEN: Data model returns the correct cascaded values after the Type change |
| 976 | + def combobox_values_for_path(path) -> tuple: |
| 977 | + return { |
| 978 | + fc_esc_proto_path: (fc_esc_protocol,), |
| 979 | + telem_type_path: (serial_port,), |
| 980 | + telem_proto_path: ("None", fc_esc_protocol), |
| 981 | + }.get(path, ()) |
| 982 | + |
| 983 | + editor.data_model.get_combobox_values_for_path.side_effect = combobox_values_for_path |
| 984 | + # _on_esc_fc_protocol_changed reads the FC->ESC Connection Type and Protocol from data model |
| 985 | + editor.data_model.get_component_value = MagicMock( |
| 986 | + side_effect=lambda path: fc_esc_protocol if path == fc_esc_proto_path else serial_port |
| 987 | + ) |
| 988 | + |
| 989 | + fc_esc_proto_widget = _FakePairTupleCombobox(fc_esc_protocol) |
| 990 | + telem_type_widget = _FakePairTupleCombobox(serial_port) |
| 991 | + |
| 992 | + # ESC->FC Protocol was previously "None" — this is the stale value the bug leaves |
| 993 | + telem_proto_widget = _FakePairTupleCombobox("None") |
| 994 | + |
| 995 | + editor.entry_widgets[fc_esc_proto_path] = fc_esc_proto_widget |
| 996 | + editor.entry_widgets[telem_type_path] = telem_type_widget |
| 997 | + editor.entry_widgets[telem_proto_path] = telem_proto_widget |
| 998 | + |
| 999 | + # WHEN: User changes FC->ESC Connection Type to SERIAL1 |
| 1000 | + editor.update_component_protocol_combobox_entries(fc_esc_type_path, serial_port) |
| 1001 | + |
| 1002 | + # THEN: ESC->FC Telemetry Protocol widget is set to the same protocol as FC->ESC |
| 1003 | + telem_proto_widget.set_entries_tuple.assert_called_with([(fc_esc_protocol, fc_esc_protocol)], fc_esc_protocol) |
| 1004 | + |
| 1005 | + def test_update_protocol_combobox_entries_autoselects_only_option_when_selection_is_empty( |
| 1006 | + self, editor_with_mocked_root |
| 1007 | + ) -> None: |
| 1008 | + """Single-option protocol comboboxes auto-select their sole value even when no current selection exists.""" |
| 1009 | + editor = editor_with_mocked_root |
| 1010 | + protocol_path = ("ESC", "ESC->FC Telemetry", "Protocol") |
| 1011 | + protocol_widget = _FakePairTupleCombobox("") |
| 1012 | + |
| 1013 | + editor.entry_widgets[protocol_path] = protocol_widget |
| 1014 | + |
| 1015 | + result = editor.update_protocol_combobox_entries(("DroneCAN",), protocol_path) |
| 1016 | + |
| 1017 | + assert result == "" |
| 1018 | + editor.data_model.set_component_value.assert_any_call(protocol_path, "DroneCAN") |
| 1019 | + assert protocol_widget.list_keys == ["DroneCAN"] |
| 1020 | + assert protocol_widget.get_selected_key() == "DroneCAN" |
0 commit comments