|
17 | 17 | import tkinter as tk |
18 | 18 | from collections.abc import Generator |
19 | 19 | from tkinter import ttk |
| 20 | +from typing import Union |
20 | 21 | from unittest.mock import Mock, patch |
21 | 22 |
|
22 | 23 | import pytest |
23 | 24 | from conftest import PARAMETER_EDITOR_TABLE_HEADERS_ADVANCED, PARAMETER_EDITOR_TABLE_HEADERS_SIMPLE |
24 | 25 |
|
25 | 26 | from ardupilot_methodic_configurator.configuration_manager import ConfigurationManager |
26 | | -from ardupilot_methodic_configurator.data_model_ardupilot_parameter import ArduPilotParameter |
| 27 | +from ardupilot_methodic_configurator.data_model_ardupilot_parameter import ArduPilotParameter, Par |
| 28 | +from ardupilot_methodic_configurator.data_model_par_dict import ParDict |
27 | 29 | from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox |
28 | 30 | from ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table import ParameterEditorTable |
29 | 31 |
|
30 | 32 | # pylint: disable=protected-access |
31 | 33 |
|
32 | 34 |
|
| 35 | +def create_mock_data_model_ardupilot_parameter( # pylint: disable=too-many-arguments,too-many-positional-arguments # noqa: PLR0913 |
| 36 | + name: str = "TEST_PARAM", |
| 37 | + value: float = 1.0, |
| 38 | + default_value: Union[float, None] = None, |
| 39 | + comment: str = "test comment", |
| 40 | + metadata: Union[dict, None] = None, |
| 41 | + fc_value: Union[float, None] = None, |
| 42 | + is_forced: bool = False, |
| 43 | + is_calibration: bool = False, |
| 44 | + is_readonly: bool = False, |
| 45 | + min_value: Union[float, None] = None, |
| 46 | + max_value: Union[float, None] = None, |
| 47 | +) -> ArduPilotParameter: |
| 48 | + """Create a mock ArduPilotParameter for testing in GUI workflows.""" |
| 49 | + metadata = metadata or {} |
| 50 | + |
| 51 | + if is_calibration: |
| 52 | + metadata["Calibration"] = True |
| 53 | + if is_readonly: |
| 54 | + metadata["ReadOnly"] = True |
| 55 | + if min_value is not None: |
| 56 | + metadata["min"] = min_value |
| 57 | + if max_value is not None: |
| 58 | + metadata["max"] = max_value |
| 59 | + |
| 60 | + metadata.setdefault("unit", "") |
| 61 | + metadata.setdefault("doc_tooltip", "Test tooltip") |
| 62 | + metadata.setdefault("unit_tooltip", "Unit tooltip") |
| 63 | + |
| 64 | + par_obj = Par(value, comment) |
| 65 | + default_par = Par(default_value if default_value is not None else 0.0, "default") |
| 66 | + forced_par = Par(value, "forced comment") if is_forced else None |
| 67 | + |
| 68 | + return ArduPilotParameter( |
| 69 | + name=name, par_obj=par_obj, metadata=metadata, default_par=default_par, fc_value=fc_value, forced_par=forced_par |
| 70 | + ) |
| 71 | + |
| 72 | + |
33 | 73 | class TestParameterEditorTableUserWorkflows: |
34 | 74 | """Test user workflows and behaviors for ParameterEditorTable GUI components.""" |
35 | 75 |
|
@@ -354,3 +394,104 @@ def test_user_can_work_with_fully_populated_parameter_table(self, parameter_tabl |
354 | 394 | verify the building blocks work correctly. |
355 | 395 | """ |
356 | 396 | pytest.skip("Full table population requires complex parameter data setup - focus on component testing instead") |
| 397 | + |
| 398 | + def test_user_can_edit_multiple_parameters_in_complete_workflow(self, parameter_table: ParameterEditorTable) -> None: |
| 399 | + """ |
| 400 | + User can manage multiple parameters with visual indicators throughout workflow. |
| 401 | +
|
| 402 | + GIVEN: A user has multiple parameters with different states |
| 403 | + WHEN: Parameters have different values (default vs changed) |
| 404 | + THEN: Visual indicators show parameter states correctly |
| 405 | + AND: Each parameter maintains independent state |
| 406 | + AND: The system handles multiple parameter contexts simultaneously |
| 407 | + """ |
| 408 | + # Arrange: Create parameters with different value states |
| 409 | + param_default = create_mock_data_model_ardupilot_parameter( |
| 410 | + name="PARAM_DEFAULT", |
| 411 | + value=10.0, |
| 412 | + default_value=10.0, # Same as default |
| 413 | + ) |
| 414 | + param_changed = create_mock_data_model_ardupilot_parameter( |
| 415 | + name="PARAM_CHANGED", |
| 416 | + value=20.0, |
| 417 | + default_value=15.0, # Different from default |
| 418 | + ) |
| 419 | + |
| 420 | + # Verify: Parameters have correct comparison states |
| 421 | + assert param_default.new_value_equals_default_value is True # Default |
| 422 | + assert param_changed.new_value_equals_default_value is False # Changed |
| 423 | + |
| 424 | + # Verify: Configuration manager can handle multiple parameters |
| 425 | + assert parameter_table.configuration_manager.current_file == "04_board_orientation.param" |
| 426 | + assert parameter_table.configuration_manager.is_fc_connected is False |
| 427 | + |
| 428 | + def test_user_can_switch_between_gui_complexity_modes_seamlessly(self, parameter_table: ParameterEditorTable) -> None: |
| 429 | + """ |
| 430 | + User can work with different GUI complexity modes. |
| 431 | +
|
| 432 | + GIVEN: A user switches between GUI complexity modes |
| 433 | + WHEN: The table needs to adapt to show/hide upload column |
| 434 | + THEN: Upload column visibility changes based on complexity level |
| 435 | + AND: Simple mode hides advanced features |
| 436 | + AND: Advanced/Expert modes show full functionality |
| 437 | + """ |
| 438 | + # Verify: Simple mode hides upload column |
| 439 | + assert parameter_table._should_show_upload_column("simple") is False |
| 440 | + |
| 441 | + # Verify: Advanced mode shows upload column |
| 442 | + assert parameter_table._should_show_upload_column("advanced") is True |
| 443 | + |
| 444 | + # Verify: Expert mode shows upload column |
| 445 | + assert parameter_table._should_show_upload_column("expert") is True |
| 446 | + |
| 447 | + # Verify: Change reason column index adapts to upload column visibility |
| 448 | + change_reason_idx_simple = parameter_table._get_change_reason_column_index(show_upload_column=False) |
| 449 | + change_reason_idx_advanced = parameter_table._get_change_reason_column_index(show_upload_column=True) |
| 450 | + |
| 451 | + # Verify: Column index is one less without upload column |
| 452 | + assert change_reason_idx_advanced == change_reason_idx_simple + 1 |
| 453 | + |
| 454 | + def test_user_recovers_gracefully_from_validation_errors(self, parameter_table: ParameterEditorTable) -> None: |
| 455 | + """ |
| 456 | + User receives clear feedback for validation errors and can recover. |
| 457 | +
|
| 458 | + GIVEN: A user enters parameter values |
| 459 | + WHEN: Values are outside allowed ranges |
| 460 | + THEN: System provides clear error handling |
| 461 | + AND: Valid values are accepted |
| 462 | + AND: Invalid values trigger appropriate error responses |
| 463 | + """ |
| 464 | + # Arrange: Create parameter with validation constraints |
| 465 | + param_constrained = create_mock_data_model_ardupilot_parameter( |
| 466 | + name="CONSTRAINED_PARAM", value=50.0, default_value=50.0, min_value=0.0, max_value=100.0 |
| 467 | + ) |
| 468 | + |
| 469 | + # Configure test file parameters |
| 470 | + parameter_table.configuration_manager._local_filesystem.file_parameters = ParDict( |
| 471 | + {"04_board_orientation.param": ParDict({"CONSTRAINED_PARAM": Par(50.0, "constrained")})} |
| 472 | + ) |
| 473 | + |
| 474 | + # Act & Verify: Attempt out-of-range value with rejection |
| 475 | + with patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.messagebox") as mock_msgbox: |
| 476 | + mock_msgbox.askyesno.return_value = False # User rejects invalid value |
| 477 | + |
| 478 | + result_invalid = parameter_table._handle_parameter_value_update( |
| 479 | + param_constrained, |
| 480 | + "150.0", # Out of range |
| 481 | + include_range_check=True, |
| 482 | + ) |
| 483 | + |
| 484 | + # Verify: Invalid value rejected |
| 485 | + assert result_invalid is False |
| 486 | + mock_msgbox.askyesno.assert_called_once() |
| 487 | + |
| 488 | + # Act & Verify: Valid value accepted |
| 489 | + with patch("ardupilot_methodic_configurator.frontend_tkinter_parameter_editor_table.show_tooltip"): |
| 490 | + result_valid = parameter_table._handle_parameter_value_update( |
| 491 | + param_constrained, |
| 492 | + "75.0", # Valid value within range |
| 493 | + include_range_check=True, |
| 494 | + ) |
| 495 | + |
| 496 | + # Verify: Valid value accepted |
| 497 | + assert result_valid is True |
0 commit comments