-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy pathgui_frontend_tkinter_parameter_editor_table.py
More file actions
executable file
·497 lines (407 loc) · 23.2 KB
/
Copy pathgui_frontend_tkinter_parameter_editor_table.py
File metadata and controls
executable file
·497 lines (407 loc) · 23.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
#!/usr/bin/env python3
"""
GUI tests for the ParameterEditorTable using PyAutoGUI.
This module contains automated GUI tests for the Tkinter-based parameter editor table.
Tests verify that the table creation and widget generation works correctly.
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
SPDX-FileCopyrightText: 2024-2025 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
SPDX-License-Identifier: GPL-3.0-or-later
"""
import contextlib
import tkinter as tk
from collections.abc import Generator
from tkinter import ttk
from typing import Union
from unittest.mock import Mock, patch
import pytest
from conftest import PARAMETER_EDITOR_TABLE_HEADERS_ADVANCED, PARAMETER_EDITOR_TABLE_HEADERS_SIMPLE
from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager
from ardupilot_methodic_configurator.data_model_ardupilot_parameter import ArduPilotParameter, Par
from ardupilot_methodic_configurator.data_model_par_dict import ParDict
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox
from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table import ParameterEditorTable
# pylint: disable=protected-access
def create_mock_data_model_ardupilot_parameter( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: PLR0913
name: str = "TEST_PARAM",
value: float = 1.0,
default_value: Union[float, None] = None,
comment: str = "test comment",
metadata: Union[dict, None] = None,
fc_value: Union[float, None] = None,
is_forced: bool = False,
is_calibration: bool = False,
is_readonly: bool = False,
min_value: Union[float, None] = None,
max_value: Union[float, None] = None,
) -> ArduPilotParameter:
"""Create a mock ArduPilotParameter for testing in GUI workflows."""
metadata = metadata or {}
if is_calibration:
metadata["Calibration"] = True
if is_readonly:
metadata["ReadOnly"] = True
if min_value is not None:
metadata["min"] = min_value
if max_value is not None:
metadata["max"] = max_value
metadata.setdefault("unit", "")
metadata.setdefault("doc_tooltip", "Test tooltip")
metadata.setdefault("unit_tooltip", "Unit tooltip")
par_obj = Par(value, comment)
default_par = Par(default_value if default_value is not None else 0.0, "default")
forced_par = Par(value, "forced comment") if is_forced else None
return ArduPilotParameter(
name=name, par_obj=par_obj, metadata=metadata, default_par=default_par, fc_value=fc_value, forced_par=forced_par
)
class TestParameterEditorTableUserWorkflows:
"""Test user workflows and behaviors for ParameterEditorTable GUI components."""
@pytest.fixture
def mock_parameter_editor(self) -> Mock:
"""Create a mock parameter editor with gui_complexity attribute."""
mock_editor = Mock()
mock_editor.gui_complexity = "advanced"
return mock_editor
@pytest.fixture
def parameter_table(
self, test_config_manager: ConfigurationManager, mock_parameter_editor: Mock
) -> Generator[ParameterEditorTable, None, None]:
"""Create a ParameterEditorTable instance for testing."""
# Create a root window for the table
root = tk.Tk()
root.withdraw() # Hide the root window
# Create the table
table = ParameterEditorTable(root, test_config_manager, mock_parameter_editor)
yield table
# Cleanup
with contextlib.suppress(tk.TclError):
root.destroy()
def test_user_sees_pyautogui_environment_ready_for_testing(self, gui_test_environment) -> None:
"""
User can verify that the GUI testing environment is properly configured.
GIVEN: A user wants to run automated GUI tests
WHEN: They check the PyAutoGUI environment setup
THEN: The screen capture and automation capabilities should be available
"""
# The gui_test_environment fixture handles all the assertions
def test_user_sees_upload_column_based_on_gui_complexity_level(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees the upload column displayed according to their GUI complexity preference.
GIVEN: A user is viewing the parameter editor table
WHEN: They have different GUI complexity settings
THEN: The upload column should be shown/hidden appropriately
AND: Simple users don't see advanced features
AND: Advanced/Expert users see the upload column
"""
# Test with different GUI complexity levels
assert parameter_table._should_show_upload_column("simple") is False
assert parameter_table._should_show_upload_column("advanced") is True
assert parameter_table._should_show_upload_column("expert") is True
# Test with None (should use parameter_editor.gui_complexity)
assert parameter_table._should_show_upload_column(None) is True # advanced
def test_user_sees_clear_table_headers_with_helpful_tooltips(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees clear, descriptive table headers with helpful tooltips.
GIVEN: A user is viewing the parameter editor table
WHEN: They look at the table headers
THEN: Headers should be clear and descriptive
AND: Tooltips should provide additional guidance
AND: Headers should adapt based on upload column visibility
"""
# Test without upload column
headers, tooltips = parameter_table._create_headers_and_tooltips(show_upload_column=False)
assert headers == PARAMETER_EDITOR_TABLE_HEADERS_SIMPLE
assert len(tooltips) == len(headers)
# Test with upload column
headers_with_upload, tooltips_with_upload = parameter_table._create_headers_and_tooltips(show_upload_column=True)
assert headers_with_upload == PARAMETER_EDITOR_TABLE_HEADERS_ADVANCED
assert len(tooltips_with_upload) == len(headers_with_upload)
def test_user_sees_parameter_names_displayed_with_proper_formatting(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees parameter names displayed with consistent formatting and padding.
GIVEN: A user is viewing parameters in the table
WHEN: Parameters have different name lengths
THEN: All parameter names should be displayed with consistent 16-character padding
AND: The formatting should be user-friendly and readable
"""
# Create a mock parameter
mock_param = Mock(spec=ArduPilotParameter)
mock_param.name = "TEST_PARAM"
mock_param.is_readonly = False
mock_param.is_calibration = False
mock_param.tooltip_new_value = "Test tooltip"
# Create the label
label = parameter_table._create_parameter_name(mock_param)
# Verify it's a ttk.Label
assert isinstance(label, ttk.Label)
# Verify the text (should be padded to 16 characters)
expected_text = "TEST_PARAM" + " " * (16 - len("TEST_PARAM"))
assert label.cget("text") == expected_text
def test_user_sees_flight_controller_values_with_clear_indicators(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees flight controller values clearly indicated in the table.
GIVEN: A user is comparing parameter values
WHEN: Some parameters have flight controller values and others don't
THEN: Parameters with FC values should display the actual value
AND: Parameters without FC values should show "N/A" clearly
AND: The display should be unambiguous and user-friendly
"""
# Test with parameter that has FC value
mock_param = Mock(spec=ArduPilotParameter)
mock_param.has_fc_value = True
mock_param.fc_value_as_string = "1.5"
mock_param.fc_value_equals_default_value = True
mock_param.fc_value_is_below_limit.return_value = False
mock_param.fc_value_is_above_limit.return_value = False
mock_param.fc_value_has_unknown_bits_set.return_value = False
mock_param.tooltip_fc_value = "FC value tooltip"
label = parameter_table._create_flightcontroller_value(mock_param)
assert isinstance(label, ttk.Label)
assert label.cget("text") == "1.5"
# Test with parameter that doesn't have FC value
mock_param_no_fc = Mock(spec=ArduPilotParameter)
mock_param_no_fc.has_fc_value = False
label_no_fc = parameter_table._create_flightcontroller_value(mock_param_no_fc)
assert isinstance(label_no_fc, ttk.Label)
assert label_no_fc.cget("text") == "N/A"
def test_user_sees_clear_visual_indicators_for_parameter_differences(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees clear visual indicators when parameter values differ from flight controller.
GIVEN: A user is reviewing parameter changes
WHEN: Some parameters have different values than the flight controller
THEN: Different parameters should have clear visual indicators (≠ or !=)
AND: Same parameters should have neutral indicators
AND: The indicators should be immediately recognizable
"""
# Test with different parameter
mock_param_different = Mock(spec=ArduPilotParameter)
mock_param_different.is_different_from_fc = True
label_different = parameter_table._create_value_different_label(mock_param_different)
assert isinstance(label_different, ttk.Label)
assert "≠" in label_different.cget("text") or "!=" in label_different.cget("text")
# Test with same parameter
mock_param_same = Mock(spec=ArduPilotParameter)
mock_param_same.is_different_from_fc = False
label_same = parameter_table._create_value_different_label(mock_param_same)
assert isinstance(label_same, ttk.Label)
assert label_same.cget("text") == " "
def test_user_can_delete_parameters_using_clearly_labeled_buttons(self, parameter_table: ParameterEditorTable) -> None:
"""
User can delete parameters using clearly labeled buttons.
GIVEN: A user wants to remove a parameter from their configuration
WHEN: They look for deletion functionality
THEN: They should see clearly labeled "Del" buttons
AND: The buttons should have proper click handlers
AND: The interface should be intuitive and discoverable
"""
button = parameter_table._create_delete_button("TEST_PARAM")
assert isinstance(button, ttk.Button)
assert button.cget("text") == "Del"
# Check that the button has a command (callback)
assert button.cget("command") is not None
def test_user_sees_table_adapts_to_column_configurations(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees the table layout properly adapts to different column configurations.
GIVEN: A user is viewing the parameter table
WHEN: The table shows different columns based on settings
THEN: The table layout should adapt gracefully
AND: Column weights should be configured appropriately
AND: The layout should remain usable and professional
"""
# This method configures grid column weights, but doesn't return anything
# We just verify it doesn't raise an exception
parameter_table._configure_table_columns(show_upload_column=False)
parameter_table._configure_table_columns(show_upload_column=True)
def test_user_sees_appropriate_input_widgets_for_parameter_types(self, parameter_table: ParameterEditorTable) -> None:
"""
User sees appropriate input widgets based on parameter characteristics.
GIVEN: A user is editing parameters of different types
WHEN: Parameters have different properties (multiple choice, bitmask, editable, etc.)
THEN: They should see the correct input widget type for each parameter
AND: Widgets should be configured appropriately for the parameter type
AND: Non-editable parameters should be clearly disabled
"""
# Create mock change reason widget and value different label
change_reason_widget = ttk.Entry(parameter_table.view_port)
value_different_label = ttk.Label(parameter_table.view_port)
# Test multiple choice parameter (should create combobox)
mock_multiple_choice_param = Mock(spec=ArduPilotParameter)
mock_multiple_choice_param.is_multiple_choice = True
mock_multiple_choice_param.choices_dict = {"Option1": "1", "Option2": "2"}
mock_multiple_choice_param.get_selected_value_from_dict.return_value = "Option1"
mock_multiple_choice_param.value_as_string = "Option1" # Should be the key, not the value
mock_multiple_choice_param.name = "MULTI_PARAM"
mock_multiple_choice_param.is_editable = True
mock_multiple_choice_param.new_value_equals_default_value = False
mock_multiple_choice_param.tooltip_new_value = "Multiple choice tooltip"
widget = parameter_table._create_new_value_entry(
mock_multiple_choice_param, change_reason_widget, value_different_label
)
# Should return a PairTupleCombobox for multiple choice parameters
assert isinstance(widget, PairTupleCombobox)
# Test regular parameter (should create entry)
mock_regular_param = Mock(spec=ArduPilotParameter)
mock_regular_param.is_multiple_choice = False
mock_regular_param.value_as_string = "42.5"
mock_regular_param.name = "REGULAR_PARAM"
mock_regular_param.is_editable = True
mock_regular_param.new_value_equals_default_value = True
mock_regular_param.is_below_limit.return_value = False
mock_regular_param.is_above_limit.return_value = False
mock_regular_param.has_unknown_bits_set.return_value = False
mock_regular_param.tooltip_new_value = "Regular parameter tooltip"
widget = parameter_table._create_new_value_entry(mock_regular_param, change_reason_widget, value_different_label)
# Should return a ttk.Entry for regular parameters
assert isinstance(widget, ttk.Entry)
# Test non-editable parameter (should be disabled)
mock_non_editable_param = Mock(spec=ArduPilotParameter)
mock_non_editable_param.is_multiple_choice = False
mock_non_editable_param.value_as_string = "100"
mock_non_editable_param.name = "NON_EDITABLE_PARAM"
mock_non_editable_param.is_editable = False
mock_non_editable_param.new_value_equals_default_value = False
mock_non_editable_param.is_below_limit.return_value = False
mock_non_editable_param.is_above_limit.return_value = False
mock_non_editable_param.has_unknown_bits_set.return_value = False
mock_non_editable_param.tooltip_new_value = "Non-editable parameter tooltip"
widget = parameter_table._create_new_value_entry(mock_non_editable_param, change_reason_widget, value_different_label)
# Should return a ttk.Entry that is disabled
assert isinstance(widget, ttk.Entry)
# For ttk widgets, state is a tuple, check if 'disabled' is in it
widget_state = widget.state()
assert "disabled" in widget_state
def test_user_can_interact_with_bitmask_selection_dialog(self, parameter_table: ParameterEditorTable) -> None:
"""
User can interact with bitmask selection dialogs for bitmask parameters.
GIVEN: A user is editing a bitmask parameter
WHEN: They double-click on the parameter value field
THEN: A bitmask selection window should open
AND: They should be able to select/deselect individual bit options
AND: The parameter value should update based on their selections
"""
# Create mock change reason widget and value different label
change_reason_widget = ttk.Entry(parameter_table.view_port)
value_different_label = ttk.Label(parameter_table.view_port)
# Create a mock bitmask parameter
mock_bitmask_param = Mock(spec=ArduPilotParameter)
mock_bitmask_param.name = "BITMASK_PARAM"
mock_bitmask_param.value_as_string = "5" # Binary 101, so bits 0 and 2 set
mock_bitmask_param.tooltip_new_value = "Bitmask parameter tooltip"
mock_bitmask_param.bitmask_dict = {0: "Option 1", 1: "Option 2", 2: "Option 3"}
# Mock the BitmaskHelper to return expected values
with (
patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.BitmaskHelper") as mock_helper,
patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.BaseWindow.center_window"),
patch("tkinter.Toplevel") as mock_toplevel,
patch("tkinter.Checkbutton"),
patch("tkinter.BooleanVar"),
):
mock_helper.get_checked_keys.return_value = {0, 2} # Bits 0 and 2 set
# Create a mock event for double-click
mock_event = Mock()
mock_event.widget = Mock(spec=ttk.Entry)
mock_event.widget.get.return_value = "5"
# Call the bitmask selection window method
parameter_table._open_bitmask_selection_window(
mock_event, mock_bitmask_param, change_reason_widget, value_different_label
)
# Verify that a window was created
mock_toplevel.assert_called_once()
# Verify that bitmask helper methods were called
mock_helper.get_checked_keys.assert_called_once_with(5, mock_bitmask_param.bitmask_dict)
@pytest.mark.skip(reason="Full table population requires complex parameter data setup")
def test_user_can_work_with_fully_populated_parameter_table(self, parameter_table: ParameterEditorTable) -> None: # pylint: disable=unused-argument
"""
User can work with a fully populated parameter table (integration test).
GIVEN: A user has loaded a complete parameter set
WHEN: The table is fully populated with real parameter data
THEN: All parameters should be displayed correctly
AND: User interactions should work as expected
AND: The table should handle large datasets efficiently
NOTE: This test is skipped because it requires complex setup with actual
parameter data and GUI components. Individual component tests above
verify the building blocks work correctly.
"""
pytest.skip("Full table population requires complex parameter data setup - focus on component testing instead")
def test_user_can_edit_multiple_parameters_in_complete_workflow(self, parameter_table: ParameterEditorTable) -> None:
"""
User can manage multiple parameters with visual indicators throughout workflow.
GIVEN: A user has multiple parameters with different states
WHEN: Parameters have different values (default vs changed)
THEN: Visual indicators show parameter states correctly
AND: Each parameter maintains independent state
AND: The system handles multiple parameter contexts simultaneously
"""
# Arrange: Create parameters with different value states
param_default = create_mock_data_model_ardupilot_parameter(
name="PARAM_DEFAULT",
value=10.0,
default_value=10.0, # Same as default
)
param_changed = create_mock_data_model_ardupilot_parameter(
name="PARAM_CHANGED",
value=20.0,
default_value=15.0, # Different from default
)
# Verify: Parameters have correct comparison states
assert param_default.new_value_equals_default_value is True # Default
assert param_changed.new_value_equals_default_value is False # Changed
# Verify: Configuration manager can handle multiple parameters
assert parameter_table.configuration_manager.current_file == "04_board_orientation.param"
assert parameter_table.configuration_manager.is_fc_connected is False
def test_user_can_switch_between_gui_complexity_modes_seamlessly(self, parameter_table: ParameterEditorTable) -> None:
"""
User can work with different GUI complexity modes.
GIVEN: A user switches between GUI complexity modes
WHEN: The table needs to adapt to show/hide upload column
THEN: Upload column visibility changes based on complexity level
AND: Simple mode hides advanced features
AND: Advanced/Expert modes show full functionality
"""
# Verify: Simple mode hides upload column
assert parameter_table._should_show_upload_column("simple") is False
# Verify: Advanced mode shows upload column
assert parameter_table._should_show_upload_column("advanced") is True
# Verify: Expert mode shows upload column
assert parameter_table._should_show_upload_column("expert") is True
# Verify: Change reason column index adapts to upload column visibility
change_reason_idx_simple = parameter_table._get_change_reason_column_index(show_upload_column=False)
change_reason_idx_advanced = parameter_table._get_change_reason_column_index(show_upload_column=True)
# Verify: Column index is one less without upload column
assert change_reason_idx_advanced == change_reason_idx_simple + 1
def test_user_recovers_gracefully_from_validation_errors(self, parameter_table: ParameterEditorTable) -> None:
"""
User receives clear feedback for validation errors and can recover.
GIVEN: A user enters parameter values
WHEN: Values are outside allowed ranges
THEN: System provides clear error handling
AND: Valid values are accepted
AND: Invalid values trigger appropriate error responses
"""
# Arrange: Create parameter with validation constraints
param_constrained = create_mock_data_model_ardupilot_parameter(
name="CONSTRAINED_PARAM", value=50.0, default_value=50.0, min_value=0.0, max_value=100.0
)
# Configure test file parameters
parameter_table.configuration_manager._local_filesystem.file_parameters = ParDict(
{"04_board_orientation.param": ParDict({"CONSTRAINED_PARAM": Par(50.0, "constrained")})}
)
# Act & Verify: Attempt out-of-range value with rejection
with patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.messagebox") as mock_msgbox:
mock_msgbox.askyesno.return_value = False # User rejects invalid value
result_invalid = parameter_table._handle_parameter_value_update(
param_constrained,
"150.0", # Out of range
include_range_check=True,
)
# Verify: Invalid value rejected
assert result_invalid is False
mock_msgbox.askyesno.assert_called_once()
# Act & Verify: Valid value accepted
with patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.show_tooltip"):
result_valid = parameter_table._handle_parameter_value_update(
param_constrained,
"75.0", # Valid value within range
include_range_check=True,
)
# Verify: Valid value accepted
assert result_valid is True