Skip to content

Commit af2f848

Browse files
committed
more fixes
1 parent 3d29130 commit af2f848

3 files changed

Lines changed: 505 additions & 19 deletions

File tree

ardupilot_methodic_configurator/frontend_tkinter_component_editor_base.py

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def argument_parser() -> Namespace:
7070
VEICLE_IMAGE_WIDTH_PIX = 100
7171

7272

73-
class ComponentEditorWindowBase(BaseWindow):
73+
class ComponentEditorWindowBase(BaseWindow): # pylint: disable=too-many-instance-attributes
7474
"""
7575
A class for editing JSON files in the ArduPilot methodic configurator.
7676
@@ -298,33 +298,55 @@ def _add_widget(self, parent: tk.Widget, key: str, value: ComponentValue, path:
298298
else: # JSON leaf elements, add Entry widget
299299
self._add_leaf_widget(parent, key, value, path)
300300

301-
def _add_non_leaf_widget(self, parent: tk.Widget, key: str, value: dict, path: list[str]) -> None:
302-
"""Add a non-leaf widget (frame containing other widgets) to the UI."""
303-
is_toplevel = parent == self.scroll_frame.view_port
304-
pady = 5 if is_toplevel else 3
305-
301+
def _prepare_non_leaf_widget_config(self, key: str, value: dict, path: list[str]) -> dict:
302+
"""Prepare configuration for non-leaf widget creation. Pure function for easy testing."""
303+
is_toplevel = len(path) == 0 # More explicit than checking parent type
306304
current_path = (*path, key)
307305
description, is_optional = self.local_filesystem.get_component_property_description(current_path)
308306
description = _(description) if description else ""
309307

310-
if is_optional:
311-
frame = ttk.LabelFrame(parent, text=_(key), style="Optional.TLabelframe")
308+
# Enhance tooltip for optional fields
309+
if is_optional and description:
310+
description += _("\nThis is optional and can be left blank")
311+
312+
return {
313+
"key": key,
314+
"value": value,
315+
"path": current_path,
316+
"description": description,
317+
"is_optional": is_optional,
318+
"is_toplevel": is_toplevel,
319+
"pady": 5 if is_toplevel else 3,
320+
}
321+
322+
def _create_non_leaf_widget_ui(self, parent: tk.Widget, config: dict) -> ttk.LabelFrame:
323+
"""Create the UI elements for a non-leaf widget. Separated for better testability."""
324+
if config["is_optional"]:
325+
frame = ttk.LabelFrame(parent, text=_(config["key"]), style="Optional.TLabelframe")
312326
else:
313-
frame = ttk.LabelFrame(parent, text=_(key))
327+
frame = ttk.LabelFrame(parent, text=_(config["key"]))
314328

315329
frame.pack(
316-
fill=tk.X, side=tk.TOP if is_toplevel else tk.LEFT, pady=pady, padx=5, anchor=tk.NW if is_toplevel else tk.N
330+
fill=tk.X,
331+
side=tk.TOP if config["is_toplevel"] else tk.LEFT,
332+
pady=config["pady"],
333+
padx=5,
334+
anchor=tk.NW if config["is_toplevel"] else tk.N,
317335
)
318336

319-
# Enhance tooltip for optional fields
320-
if is_optional and description:
321-
description += _("\nThis is optional and can be left blank")
322-
323337
# Add tooltip based on schema description
324-
if description:
325-
show_tooltip(frame, description, position_below=False)
338+
if config["description"]:
339+
show_tooltip(frame, config["description"], position_below=False)
326340

327-
if is_toplevel and key in self.data_model.get_all_components():
341+
return frame
342+
343+
def _add_non_leaf_widget(self, parent: tk.Widget, key: str, value: dict, path: list[str]) -> None:
344+
"""Add a non-leaf widget (frame containing other widgets) to the UI."""
345+
# Extract business logic to separate method for better testability
346+
widget_config = self._prepare_non_leaf_widget_config(key, value, path)
347+
frame = self._create_non_leaf_widget_ui(parent, widget_config)
348+
349+
if widget_config["is_toplevel"] and key in self.data_model.get_all_components():
328350
self._add_template_controls(frame, key)
329351

330352
for sub_key, sub_value in value.items():
@@ -459,7 +481,7 @@ def create_for_testing(
459481
version: str = "test",
460482
local_filesystem: Optional[LocalFilesystem] = None,
461483
data_model: Optional[ComponentDataModel] = None,
462-
root_tk: Optional[tk.Tk] = None,
484+
root_tk: Optional[tk.Tk] = None, # pylint: disable=unused-argument
463485
) -> "ComponentEditorWindowBase":
464486
"""
465487
Factory method for creating instances suitable for testing.
@@ -478,7 +500,7 @@ def create_for_testing(
478500
479501
"""
480502
# Import MagicMock at the method level to avoid import issues
481-
from unittest.mock import MagicMock
503+
from unittest.mock import MagicMock # pylint: disable=import-outside-toplevel
482504

483505
if local_filesystem is None:
484506
# Create a minimal mock filesystem for testing

tests/test_frontend_tkinter_component_editor_base.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
from unittest.mock import MagicMock, patch
1717

1818
import pytest
19+
from test_data_model_vehicle_components_common import REALISTIC_VEHICLE_DATA
1920

2021
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
22+
from ardupilot_methodic_configurator.backend_filesystem_vehicle_components import VehicleComponents
2123
from ardupilot_methodic_configurator.data_model_vehicle_components import ComponentDataModel
2224
from ardupilot_methodic_configurator.frontend_tkinter_component_editor_base import (
2325
VEICLE_IMAGE_WIDTH_PIX,
@@ -30,6 +32,106 @@
3032
# pylint: disable=protected-access
3133

3234

35+
def setup_common_editor_mocks(editor) -> ComponentEditorWindowBase:
36+
"""Set up common mock attributes and methods for editor fixtures."""
37+
# Set up all required attributes manually
38+
editor.root = MagicMock()
39+
editor.main_frame = MagicMock()
40+
editor.scroll_frame = MagicMock()
41+
editor.scroll_frame.view_port = MagicMock()
42+
editor.version = "1.0.0"
43+
44+
# Mock filesystem and methods with proper schema loading
45+
editor.local_filesystem = MagicMock(spec=LocalFilesystem)
46+
editor.local_filesystem.vehicle_dir = "dummy_vehicle_dir"
47+
editor.local_filesystem.get_component_property_description = MagicMock(return_value=("Test description", False))
48+
editor.local_filesystem.vehicle_image_exists = MagicMock(return_value=False)
49+
editor.local_filesystem.vehicle_image_filepath = MagicMock(return_value="test.jpg")
50+
editor.local_filesystem.save_component_to_system_templates = MagicMock()
51+
52+
# Mock the vehicle_components attribute
53+
mock_vehicle_components = MagicMock()
54+
# Make schema loading return a valid empty schema
55+
mock_vehicle_components.load_schema.return_value = {"properties": {}}
56+
mock_vehicle_components.get_component_property_description = MagicMock(return_value=("Test description", False))
57+
editor.local_filesystem.vehicle_components = mock_vehicle_components
58+
59+
# Setup test data and data model
60+
editor.entry_widgets = {}
61+
62+
# Create data model with realistic test data
63+
vehicle_components = VehicleComponents()
64+
component_datatypes = vehicle_components.get_all_value_datatypes()
65+
editor.data_model = ComponentDataModel(REALISTIC_VEHICLE_DATA, component_datatypes)
66+
67+
# Mock specific methods that are used in tests
68+
editor.data_model.set_component_value = MagicMock()
69+
editor.data_model.update_component = MagicMock()
70+
71+
# Override methods that might cause UI interactions in tests
72+
# Mock _add_widget completely to avoid UI creation
73+
editor._add_widget = MagicMock()
74+
editor.put_image_in_label = MagicMock(return_value=MagicMock())
75+
editor.add_entry_or_combobox = MagicMock(return_value=MagicMock())
76+
77+
return editor
78+
79+
80+
def add_editor_helper_methods(editor) -> None:
81+
"""Add common helper methods for testing that bypass UI operations."""
82+
83+
# Use add_widget as a public proxy for _add_widget for easier testing
84+
def add_widget_proxy(parent, key, value, path) -> None:
85+
return editor._add_widget(parent, key, value, path)
86+
87+
editor.add_widget = add_widget_proxy
88+
89+
# Add helper methods for testing that bypass UI operations
90+
def test_populate_frames() -> None:
91+
# This is a test-friendly version that doesn't involve actual UI widgets
92+
components = editor.data_model.get_all_components()
93+
for key, value in components.items():
94+
editor._add_widget(editor.scroll_frame.view_port, key, value, [])
95+
96+
editor.populate_frames = test_populate_frames
97+
98+
99+
class SharedTestArgumentParser:
100+
"""Shared test cases for the argument_parser function to avoid duplication."""
101+
102+
def test_argument_parser(self) -> None:
103+
"""Test argument_parser function."""
104+
with patch("sys.argv", ["test_script", "--vehicle-dir", "test_dir", "--vehicle-type", "ArduCopter"]):
105+
args = argument_parser()
106+
107+
assert hasattr(args, "vehicle_dir")
108+
assert hasattr(args, "vehicle_type")
109+
assert hasattr(args, "skip_component_editor")
110+
111+
def test_argument_parser_with_skip_component_editor(self) -> None:
112+
"""Test argument_parser with skip-component-editor flag."""
113+
with patch(
114+
"sys.argv", ["test_script", "--vehicle-dir", "test_dir", "--vehicle-type", "ArduCopter", "--skip-component-editor"]
115+
):
116+
args = argument_parser()
117+
118+
assert args.skip_component_editor is True
119+
120+
121+
@pytest.fixture
122+
def editor_with_mocked_root() -> ComponentEditorWindowBase:
123+
"""Create a mock ComponentEditorWindowBase for testing."""
124+
# Create the class without initialization
125+
with patch.object(ComponentEditorWindowBase, "__init__", return_value=None):
126+
editor = ComponentEditorWindowBase() # pylint: disable=no-value-for-parameter
127+
128+
# Set up common mocks and helper methods
129+
setup_common_editor_mocks(editor)
130+
add_editor_helper_methods(editor)
131+
132+
yield editor
133+
134+
33135
class TestArgumentParserBehavior:
34136
"""Test argument parser behavior and functionality."""
35137

0 commit comments

Comments
 (0)