-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy pathtest_frontend_tkinter_component_editor_base.py
More file actions
executable file
·1856 lines (1498 loc) · 82.4 KB
/
test_frontend_tkinter_component_editor_base.py
File metadata and controls
executable file
·1856 lines (1498 loc) · 82.4 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
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Behavior-driven tests for ComponentEditorWindowBase.
This file is part of ArduPilot Methodic Configurator. https://github.com/ArduPilot/MethodicConfigurator
SPDX-FileCopyrightText: 2024-2026 Amilcar do Carmo Lucas <amilcar.lucas@iav.de>
SPDX-License-Identifier: GPL-3.0-or-later
"""
import tkinter as tk
from argparse import ArgumentParser
from collections.abc import Generator
from tkinter import ttk
from typing import Union, cast, get_args, get_origin
from unittest.mock import MagicMock, patch
import pytest
from test_data_model_vehicle_components_common import REALISTIC_VEHICLE_DATA, ComponentDataModelFixtures
from ardupilot_methodic_configurator import _
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
from ardupilot_methodic_configurator.data_model_vehicle_components import ComponentDataModel
from ardupilot_methodic_configurator.frontend_tkinter_component_editor_base import (
VEHICLE_IMAGE_HEIGHT_PIX,
VEHICLE_IMAGE_WIDTH_PIX,
WINDOW_WIDTH_PIX,
ComponentEditorWindowBase,
EntryWidget,
argument_parser,
)
from ardupilot_methodic_configurator.frontend_tkinter_pair_tuple_combobox import PairTupleCombobox
# pylint: disable=protected-access, too-many-lines, redefined-outer-name, unused-argument, too-few-public-methods
def setup_common_editor_mocks(editor) -> ComponentEditorWindowBase:
"""Set up common mock attributes and methods for editor fixtures."""
# Set up all required attributes manually
editor.root = MagicMock()
editor.main_frame = MagicMock()
editor.scroll_frame = MagicMock()
editor.scroll_frame.view_port = MagicMock()
editor.version = "1.0.0"
# Mock filesystem and methods with proper schema loading
editor.local_filesystem = MagicMock(spec=LocalFilesystem)
editor.local_filesystem.vehicle_dir = "dummy_vehicle_dir"
editor.local_filesystem.get_component_property_description = MagicMock(return_value=("Test description", False))
editor.local_filesystem.vehicle_image_exists = MagicMock(return_value=False)
editor.local_filesystem.vehicle_image_filepath = MagicMock(return_value="test.jpg")
editor.local_filesystem.save_component_to_system_templates = MagicMock()
# Mock the vehicle_components_fs attribute structure
mock_vehicle_components_fs = MagicMock()
mock_vehicle_components_fs.data = MagicMock()
mock_vehicle_components_fs.json_filename = "vehicle_components.json"
editor.local_filesystem.vehicle_components_fs = mock_vehicle_components_fs
# Mock the vehicle_components methods that are accessed directly
editor.local_filesystem.load_schema = MagicMock(return_value={"properties": {}})
editor.local_filesystem.get_component_property_description = MagicMock(return_value=("Test description", False))
# Setup test data and data model
editor.entry_widgets = {}
# Create data model with realistic test data
component_datatypes = ComponentDataModelFixtures.create_component_datatypes()
schema = ComponentDataModelFixtures.create_schema()
editor.data_model = ComponentDataModel(REALISTIC_VEHICLE_DATA, component_datatypes, schema)
# Mock specific methods that are used in tests
editor.data_model.set_component_value = MagicMock()
editor.data_model.update_component = MagicMock()
# Override methods that might cause UI interactions in tests
# Mock _add_widget completely to avoid UI creation
editor._add_widget = MagicMock()
editor.put_image_in_label = MagicMock(return_value=MagicMock())
editor.add_entry_or_combobox = MagicMock(return_value=MagicMock())
editor.complexity_var = MagicMock()
return editor
def add_editor_helper_methods(editor) -> None:
"""Add common helper methods for testing that bypass UI operations."""
# Use add_widget as a public proxy for _add_widget for easier testing
def add_widget_proxy(parent, key, value, path) -> None:
return editor._add_widget(parent, key, value, path)
editor.add_widget = add_widget_proxy
# Add helper methods for testing that bypass UI operations
def test_populate_frames() -> None:
# This is a test-friendly version that doesn't involve actual UI widgets
components = editor.data_model.get_all_components()
for key, value in components.items():
editor._add_widget(editor.scroll_frame.view_port, key, value, [])
editor.populate_frames = test_populate_frames
class SharedTestArgumentParser:
"""Shared test cases for the argument_parser function to avoid duplication."""
def test_argument_parser(self) -> None:
"""Test argument_parser function."""
with patch("sys.argv", ["test_script", "--vehicle-dir", "test_dir", "--vehicle-type", "ArduCopter"]):
args = argument_parser()
assert hasattr(args, "vehicle_dir")
assert hasattr(args, "vehicle_type")
assert hasattr(args, "skip_component_editor")
def test_argument_parser_with_skip_component_editor(self) -> None:
"""Test argument_parser with skip-component-editor flag."""
with patch(
"sys.argv", ["test_script", "--vehicle-dir", "test_dir", "--vehicle-type", "ArduCopter", "--skip-component-editor"]
):
args = argument_parser()
assert args.skip_component_editor is True
@pytest.fixture
def editor_with_mocked_root() -> Generator[ComponentEditorWindowBase, None, None]:
"""Create a mock ComponentEditorWindowBase for testing."""
# Create the class without initialization
with patch.object(ComponentEditorWindowBase, "__init__", return_value=None):
editor = ComponentEditorWindowBase() # pylint: disable=no-value-for-parameter # ty: ignore[missing-argument]
# Set up common mocks and helper methods
setup_common_editor_mocks(editor)
add_editor_helper_methods(editor)
yield editor
class TestArgumentParserBehavior:
"""Test argument parser behavior and functionality."""
def test_argument_parser_creates_all_required_attributes(self) -> None:
"""Test that argument parser creates all required command line attributes."""
with patch("sys.argv", ["test_script", "--vehicle-dir", "test_dir", "--vehicle-type", "ArduCopter"]):
args = argument_parser()
# Verify all expected attributes exist
required_attrs = ["vehicle_dir", "vehicle_type", "skip_component_editor", "loglevel"]
for attr in required_attrs:
assert hasattr(args, attr), f"Missing required attribute: {attr}"
def test_argument_parser_handles_skip_component_editor_flag(self) -> None:
"""Test that skip-component-editor flag is properly handled."""
with patch(
"sys.argv", ["test_script", "--vehicle-dir", "test", "--vehicle-type", "ArduCopter", "--skip-component-editor"]
):
args = argument_parser()
assert args.skip_component_editor is True
with patch("sys.argv", ["test_script", "--vehicle-dir", "test", "--vehicle-type", "ArduCopter"]):
args = argument_parser()
assert args.skip_component_editor is False
def test_argument_parser_handles_different_log_levels(self) -> None:
"""Test that different log levels are properly parsed."""
log_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
for level in log_levels:
with patch(
"sys.argv",
["test_script", "--vehicle-dir", "test", "--vehicle-type", "ArduCopter", "--loglevel", level],
):
args = argument_parser()
assert args.loglevel == level
@pytest.fixture
def mock_filesystem() -> MagicMock:
"""Fixture providing a mock filesystem with realistic test data."""
filesystem = MagicMock(spec=LocalFilesystem)
filesystem.vehicle_dir = "test_vehicle"
filesystem.doc_dict = {}
filesystem.file_parameters = {}
filesystem.vehicle_image_exists.return_value = False
filesystem.vehicle_image_filepath.return_value = "test.jpg"
filesystem.save_component_to_system_templates = MagicMock()
# Mock schema loading to return valid data
filesystem.load_schema.return_value = {"properties": {}}
filesystem.load_vehicle_components_json_data.return_value = REALISTIC_VEHICLE_DATA
return filesystem
@pytest.fixture
def mock_data_model() -> MagicMock:
"""Fixture providing a mock data model with realistic behavior."""
data_model = MagicMock(spec=ComponentDataModel)
data_model.is_valid_component_data.return_value = True
data_model.has_components.return_value = True
data_model.get_all_components.return_value = REALISTIC_VEHICLE_DATA
data_model.extract_component_data_from_entries.return_value = {"test": "data"}
data_model.save_to_filesystem.return_value = (False, "")
return data_model
@pytest.fixture
def configured_editor(mock_filesystem: MagicMock, mock_data_model: MagicMock) -> ComponentEditorWindowBase:
"""Fixture providing a properly configured editor for behavior testing."""
return ComponentEditorWindowBase.create_for_testing(
version="1.0.0", local_filesystem=mock_filesystem, data_model=mock_data_model
)
class TestUserArgumentParsingWorkflows:
"""Test user workflows for command line argument parsing."""
def test_user_can_parse_basic_command_arguments(self) -> None:
"""
User can provide basic command line arguments to start the application.
GIVEN: A user wants to start the component editor with basic settings
WHEN: They provide vehicle directory and type arguments
THEN: The arguments should be parsed correctly with default values
"""
# Arrange: Set up command line arguments
test_args = ["test_script", "--vehicle-dir", "test_dir", "--vehicle-type", "ArduCopter"]
# Act: Parse the arguments
with patch("sys.argv", test_args):
args = argument_parser()
# Assert: Verify arguments are parsed correctly
assert args.vehicle_dir == "test_dir"
assert args.vehicle_type == "ArduCopter"
assert args.skip_component_editor is False
def test_user_can_skip_component_editor_when_needed(self) -> None:
"""
User can skip the component editor interface when components are pre-configured.
GIVEN: A user has already configured their vehicle components
WHEN: They provide the skip-component-editor flag
THEN: The skip flag should be properly set to True
"""
# Arrange: Set up command line arguments with skip flag
test_args = ["test_script", "--vehicle-dir", "test", "--vehicle-type", "ArduCopter", "--skip-component-editor"]
# Act: Parse the arguments
with patch("sys.argv", test_args):
args = argument_parser()
# Assert: Verify skip flag is enabled
assert args.skip_component_editor is True
def test_user_can_configure_different_log_levels_for_debugging(self) -> None:
"""
User can set different logging levels for troubleshooting purposes.
GIVEN: A user needs to debug the application behavior
WHEN: They specify different log levels
THEN: Each log level should be properly parsed
"""
log_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
for level in log_levels:
# Arrange: Set up command line arguments with specific log level
test_args = ["test_script", "--vehicle-dir", "test", "--vehicle-type", "ArduCopter", "--loglevel", level]
# Act: Parse the arguments
with patch("sys.argv", test_args):
args = argument_parser()
# Assert: Verify log level is set correctly
assert args.loglevel == level
class TestDataValidationWorkflows:
"""Test user workflows for data validation."""
def test_user_sees_no_errors_when_all_data_is_valid(self, editor_with_mocked_root: ComponentEditorWindowBase) -> None:
"""
User receives no error messages when all component data is valid.
GIVEN: A user has filled in all component fields with valid data
WHEN: The system validates all entered data
THEN: No error messages should be displayed and validation should pass
"""
# Arrange: Set up valid entry widgets with proper data
mock_entry = MagicMock(spec=ttk.Entry)
mock_entry.get.return_value = "1000"
mock_combobox = MagicMock(spec=ttk.Combobox)
mock_combobox.get.return_value = "PWM"
editor_with_mocked_root.entry_widgets = {
("Motor", "Specifications", "KV"): mock_entry,
("RC Receiver", "FC Connection", "Protocol"): mock_combobox,
}
# Mock data model to return valid validation
editor_with_mocked_root.data_model.validate_all_data = MagicMock(return_value=(True, []))
# Act: User triggers validation
result = editor_with_mocked_root.validate_data_and_highlight_errors_in_red()
# Assert: No errors should be returned
assert result == ""
editor_with_mocked_root.data_model.validate_all_data.assert_called_once()
def test_user_sees_error_highlighting_for_invalid_entry_values(
self, editor_with_mocked_root: ComponentEditorWindowBase
) -> None:
"""
User sees visual feedback when entry fields contain invalid values.
GIVEN: A user has entered invalid data in text entry fields
WHEN: The system validates the data
THEN: Invalid entries should be highlighted in red and error messages displayed
"""
# Arrange: Set up invalid entry data
mock_invalid_entry = MagicMock(spec=ttk.Entry)
mock_invalid_entry.get.return_value = "99999" # Invalid high value
editor_with_mocked_root.entry_widgets = {
("Motor", "Specifications", "KV"): mock_invalid_entry,
}
# Mock validation to return errors
editor_with_mocked_root.data_model.validate_all_data = MagicMock(return_value=(False, ["KV value too high"]))
editor_with_mocked_root.data_model.validate_entry_limits = MagicMock(
return_value=("Value exceeds maximum limit", None)
)
with patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.show_error_message") as mock_error:
# Act: User triggers validation
result = editor_with_mocked_root.validate_data_and_highlight_errors_in_red()
# Assert: Entry should be styled as invalid and error shown
mock_invalid_entry.configure.assert_called_once_with(style="entry_input_invalid.TEntry")
mock_error.assert_called_once()
assert result != ""
def test_user_sees_error_highlighting_for_invalid_combobox_selections(
self, editor_with_mocked_root: ComponentEditorWindowBase
) -> None:
"""
User sees visual feedback when combobox selections are invalid.
GIVEN: A user has selected invalid options in combobox fields
WHEN: The system validates the selections
THEN: Invalid comboboxes should be highlighted in red
"""
# Arrange: Set up invalid combobox selection
mock_invalid_combobox = MagicMock(spec=PairTupleCombobox)
mock_invalid_combobox.get_selected_key.return_value = "INVALID_PROTOCOL"
mock_invalid_combobox.list_keys = ("PWM", "SBUS", "PPM")
editor_with_mocked_root.entry_widgets = {
("RC Receiver", "FC Connection", "Protocol"): mock_invalid_combobox,
}
# Mock validation to return errors
editor_with_mocked_root.data_model.validate_all_data = MagicMock(return_value=(False, ["Invalid protocol selected"]))
editor_with_mocked_root.data_model.get_combobox_values_for_path = MagicMock(return_value=("PWM", "SBUS", "PPM"))
with patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.show_error_message"):
# Act: User triggers validation
result = editor_with_mocked_root.validate_data_and_highlight_errors_in_red()
# Assert: Combobox should be styled as invalid
mock_invalid_combobox.configure.assert_called_once_with(style="comb_input_invalid.TCombobox")
assert result != ""
def test_user_sees_valid_styling_for_corrected_combobox_values(
self, editor_with_mocked_root: ComponentEditorWindowBase
) -> None:
"""
User sees positive visual feedback when combobox values become valid.
GIVEN: A user has corrected a combobox selection to a valid value
WHEN: The system validates the corrected data
THEN: The combobox should be highlighted as valid
"""
# Arrange: Set up valid combobox selection
mock_valid_combobox = MagicMock(spec=PairTupleCombobox)
mock_valid_combobox.get_selected_key.return_value = "PWM"
mock_valid_combobox.list_keys = ("PWM", "SBUS", "PPM")
editor_with_mocked_root.entry_widgets = {
("RC Receiver", "FC Connection", "Protocol"): mock_valid_combobox,
}
# Mock validation - overall fails but this combobox is valid
editor_with_mocked_root.data_model.validate_all_data = MagicMock(return_value=(False, ["Other validation error"]))
editor_with_mocked_root.data_model.get_combobox_values_for_path = MagicMock(return_value=("PWM", "SBUS", "PPM"))
with patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.show_error_message"):
# Act: User triggers validation
editor_with_mocked_root.validate_data_and_highlight_errors_in_red()
# Assert: Combobox should be styled as valid
mock_valid_combobox.configure.assert_called_once_with(style="comb_input_valid.TCombobox")
def test_user_sees_limited_error_messages_when_many_errors_exist(
self, editor_with_mocked_root: ComponentEditorWindowBase
) -> None:
"""
User sees a manageable number of error messages when many validation errors exist.
GIVEN: A user has multiple validation errors across many fields
WHEN: The system validates all data
THEN: Only the first 3 errors should be shown with a count of remaining errors
"""
# Arrange: Set up entry that will trigger validation
mock_entry = MagicMock(spec=ttk.Entry)
mock_entry.get.return_value = "invalid"
editor_with_mocked_root.entry_widgets = {
("Motor", "Specifications", "KV"): mock_entry,
}
# Mock validation to return many errors
many_errors = ["Error 1", "Error 2", "Error 3", "Error 4", "Error 5"]
editor_with_mocked_root.data_model.validate_all_data = MagicMock(return_value=(False, many_errors))
editor_with_mocked_root.data_model.validate_entry_limits = MagicMock(return_value=("Invalid value", None))
with patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.show_error_message") as mock_error:
# Act: User triggers validation
result = editor_with_mocked_root.validate_data_and_highlight_errors_in_red()
# Assert: Should show first 3 errors + count of remaining
mock_error.assert_called_once()
error_message = mock_error.call_args[0][1]
assert "Error 1" in error_message
assert "Error 2" in error_message
assert "Error 3" in error_message
assert "2 more errors" in error_message
assert result != ""
def test_user_validation_only_processes_entry_and_combobox_widgets(
self, editor_with_mocked_root: ComponentEditorWindowBase
) -> None:
"""
User data validation only processes actual input widgets, ignoring other UI elements.
GIVEN: A user interface contains various widget types including input fields
WHEN: The system validates user input data
THEN: Only Entry and Combobox widgets should be included in validation
"""
# Arrange: Set up mixed widget types
mock_entry = MagicMock(spec=ttk.Entry)
mock_entry.get.return_value = "1000"
mock_combobox = MagicMock(spec=ttk.Combobox)
mock_combobox.get.return_value = "PWM"
mock_label = MagicMock() # Non-input widget
editor_with_mocked_root.entry_widgets = {
("Motor", "Specifications", "KV"): mock_entry,
("RC Receiver", "FC Connection", "Protocol"): mock_combobox,
("Some", "Label", "Widget"): mock_label, # Should be ignored
}
editor_with_mocked_root.data_model.validate_all_data = MagicMock(return_value=(True, []))
# Act: User triggers validation
result = editor_with_mocked_root.validate_data_and_highlight_errors_in_red()
# Assert: Only Entry and Combobox values should be validated
expected_values = {
("Motor", "Specifications", "KV"): "1000",
("RC Receiver", "FC Connection", "Protocol"): "PWM",
# Label widget should NOT be included
}
editor_with_mocked_root.data_model.validate_all_data.assert_called_once_with(expected_values)
assert result == ""
class TestComponentDataManagementWorkflows:
"""Test user workflows for managing component data."""
def test_user_can_extract_component_data_from_gui_inputs(self, configured_editor: ComponentEditorWindowBase) -> None:
"""
User can extract component data that they've entered through the GUI.
GIVEN: A user has filled in component data through GUI fields
WHEN: They request to extract the data for a specific component
THEN: The system should return the correctly formatted component data
"""
# Arrange: Set up mock entry widgets with realistic user input
component_name = "Motor"
mock_entry1 = MagicMock()
mock_entry1.get.return_value = "T-Motor MN3110"
mock_entry2 = MagicMock()
mock_entry2.get.return_value = "700"
configured_editor.entry_widgets = {("Motor", "Model"): mock_entry1, ("Motor", "Specifications", "KV"): mock_entry2}
expected_result = {"Model": "T-Motor MN3110", "Specifications": {"KV": "700"}}
configured_editor.data_model.extract_component_data_from_entries.return_value = expected_result
# Act: Extract component data from GUI
result = configured_editor.get_component_data_from_gui(component_name)
# Assert: Extracted data should match expected format
configured_editor.data_model.extract_component_data_from_entries.assert_called_once_with(
component_name, {("Motor", "Model"): "T-Motor MN3110", ("Motor", "Specifications", "KV"): "700"}
)
assert result == expected_result
def test_user_can_update_component_values_and_see_ui_changes(self, configured_editor: ComponentEditorWindowBase) -> None:
"""
User can update component values and immediately see the changes in the UI.
GIVEN: A user wants to modify a component value
WHEN: They update the value through the interface
THEN: Both the data model and UI widget should be updated
"""
# Arrange: Set up component path and new value
path = ("Motor", "Model")
new_value = "Updated Motor Model"
mock_entry = MagicMock()
configured_editor.entry_widgets[path] = mock_entry
# Act: Update component value and UI
configured_editor.set_component_value_and_update_ui(path, new_value)
# Assert: Both data model and UI should be updated
configured_editor.data_model.set_component_value.assert_called_once_with(path, new_value)
mock_entry.delete.assert_called_once_with(0, tk.END)
mock_entry.insert.assert_called_once_with(0, new_value)
mock_entry.config.assert_called_once_with(state="disabled")
def test_user_can_update_values_even_without_corresponding_ui_widget(
self, configured_editor: ComponentEditorWindowBase
) -> None:
"""
User can update component values even when no UI widget exists.
GIVEN: A user updates a component value programmatically
WHEN: No corresponding UI widget exists for that path
THEN: The data model should still be updated without errors
"""
# Arrange: Set up component path with no corresponding widget
path = ("Motor", "NonExistentField")
new_value = "Some Value"
# Act: Update component value without widget
configured_editor.set_component_value_and_update_ui(path, new_value)
# Assert: Data model should be updated
configured_editor.data_model.set_component_value.assert_called_once_with(path, new_value)
class TestSaveOperationWorkflows:
"""Test user workflows for saving component data."""
@pytest.fixture
def editor_for_save_tests(self, mock_filesystem: MagicMock) -> ComponentEditorWindowBase:
"""Fixture providing an editor configured for save operation testing."""
data_model = MagicMock(spec=ComponentDataModel)
data_model.save_to_filesystem.return_value = (False, "")
editor = ComponentEditorWindowBase.create_for_testing(
version="1.0.0", local_filesystem=mock_filesystem, data_model=data_model
)
# Mock UI methods to avoid actual UI operations
editor.validate_data_and_highlight_errors_in_red = MagicMock(return_value="")
return editor
def test_user_can_successfully_save_component_data(self, editor_for_save_tests: ComponentEditorWindowBase) -> None:
"""
User can successfully save their component configuration.
GIVEN: A user has completed their component configuration
WHEN: They save the configuration and it succeeds
THEN: The data should be saved
"""
# Arrange: Configure successful save operation
editor_for_save_tests.data_model.save_to_filesystem.return_value = (False, "")
# Act: Save component data
editor_for_save_tests.save_component_json()
# Assert: Save operation should be attempted
editor_for_save_tests.data_model.save_to_filesystem.assert_called_once_with(editor_for_save_tests.local_filesystem)
def test_user_receives_error_feedback_when_save_fails(self, editor_for_save_tests: ComponentEditorWindowBase) -> None:
"""
User receives clear error feedback when save operation fails.
GIVEN: A user attempts to save their configuration
WHEN: The save operation fails due to an error
THEN: An error message should be displayed to the user
"""
# Arrange: Configure failed save operation
editor_for_save_tests.data_model.save_to_filesystem.return_value = (True, "File system error")
# Act: Attempt to save with mocked error display
with patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.show_error_message") as mock_error:
editor_for_save_tests.save_component_json()
# Assert: Error message should be displayed
editor_for_save_tests.data_model.save_to_filesystem.assert_called_once_with(editor_for_save_tests.local_filesystem)
mock_error.assert_called_once()
def test_user_must_confirm_before_saving_component_data(self, editor_for_save_tests: ComponentEditorWindowBase) -> None:
"""
User must confirm that all component properties are correct before saving.
GIVEN: A user wants to save their component configuration
WHEN: They trigger the save operation
THEN: They should be prompted to confirm their data is correct
"""
# Arrange: Mock should_display to skip the confirmation popup entirely
with (
patch(
"ardupilot_methodic_configurator.frontend_tkinter_component_editor_base."
"ConfirmationPopupWindow.should_display",
return_value=False,
),
patch.object(editor_for_save_tests.root, "destroy"),
patch.object(editor_for_save_tests, "save_component_json", return_value=False) as mock_save,
):
# Act: Trigger validate and save operation
editor_for_save_tests.on_save_pressed()
# Assert: Validation and save should proceed
validation_mock = cast("MagicMock", editor_for_save_tests.validate_data_and_highlight_errors_in_red)
validation_mock.assert_called_once()
mock_save.assert_called_once()
def test_user_stays_in_component_editor_when_save_fails(self, editor_for_save_tests: ComponentEditorWindowBase) -> None:
"""
The editor window remains open when the save operation fails.
GIVEN: A user attempts to save component configuration
WHEN: The save operation fails due to invalid JSON or filesystem error
THEN: The component editor window should stay open
"""
# Arrange: Simulate failed save operation
editor_for_save_tests.validate_data_and_highlight_errors_in_red = MagicMock(return_value="")
with (
patch(
"ardupilot_methodic_configurator.frontend_tkinter_component_editor_base."
"ConfirmationPopupWindow.should_display",
return_value=False,
),
patch.object(editor_for_save_tests, "save_component_json", return_value=True) as mock_save,
patch.object(editor_for_save_tests.root, "destroy"),
):
# Act: Trigger on-save handler
editor_for_save_tests.on_save_pressed()
# Assert: Failed save does not close the window
mock_save.assert_called_once()
editor_for_save_tests.root.destroy.assert_not_called()
class TestWindowClosingWorkflows:
"""Test user workflows for closing the editor window."""
@pytest.fixture
def editor_for_closing_tests(self, mock_filesystem: MagicMock) -> ComponentEditorWindowBase:
"""Fixture providing an editor configured for window closing tests."""
editor = ComponentEditorWindowBase.create_for_testing(version="1.0.0", local_filesystem=mock_filesystem)
editor.save_component_json = MagicMock()
return editor
def test_user_can_save_before_closing_when_prompted(self, editor_for_closing_tests: ComponentEditorWindowBase) -> None:
"""
User can choose to save their work when closing the window.
GIVEN: A user wants to close the component editor
WHEN: They choose to save their changes in the confirmation dialog
THEN: The save operation should be executed before closing
"""
# Arrange: Mock user choosing to save and a successful save operation
editor_for_closing_tests.save_component_json = MagicMock(return_value=False)
with (
patch(
"ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.messagebox.askyesnocancel",
return_value=True,
),
patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.sys_exit") as mock_exit,
):
# Act: Trigger window closing
editor_for_closing_tests.on_closing()
# Assert: Save should be called and application should exit
editor_for_closing_tests.save_component_json.assert_called_once()
mock_exit.assert_called_once_with(0)
def test_user_stays_in_component_editor_when_closing_save_fails(
self,
editor_for_closing_tests: ComponentEditorWindowBase,
) -> None:
"""
The editor window remains open when closing fails due to a save error.
GIVEN: A user attempts to close the component editor and chooses to save
WHEN: The save operation fails
THEN: The component editor window should remain open and no exit should occur
"""
editor_for_closing_tests.save_component_json = MagicMock(return_value=True)
with (
patch(
"ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.messagebox.askyesnocancel",
return_value=True,
),
patch.object(editor_for_closing_tests.root, "destroy"),
patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.sys_exit") as mock_exit,
):
# Act: Trigger window closing
editor_for_closing_tests.on_closing()
# Assert: Failed save does not close or exit the application
editor_for_closing_tests.save_component_json.assert_called_once()
editor_for_closing_tests.root.destroy.assert_not_called()
mock_exit.assert_not_called()
def test_user_can_close_without_saving_when_prompted(self, editor_for_closing_tests: ComponentEditorWindowBase) -> None:
"""
User can choose to close without saving when prompted.
GIVEN: A user wants to close the component editor
WHEN: They choose not to save their changes in the confirmation dialog
THEN: The window should close without saving
"""
# Arrange: Mock user choosing not to save
with (
patch(
"ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.messagebox.askyesnocancel",
return_value=False,
),
patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.sys_exit") as mock_exit,
):
# Act: Trigger window closing
editor_for_closing_tests.on_closing()
# Assert: Save should not be called but window should close
editor_for_closing_tests.save_component_json.assert_not_called()
editor_for_closing_tests.root.destroy.assert_called_once()
mock_exit.assert_called_once_with(0)
def test_user_can_cancel_closing_operation(self, editor_for_closing_tests: ComponentEditorWindowBase) -> None:
"""
User can cancel the closing operation and continue editing.
GIVEN: A user accidentally triggers window closing
WHEN: They choose to cancel in the confirmation dialog
THEN: The window should remain open and no actions should be taken
"""
# Arrange: Mock user canceling the operation
with patch(
"ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.messagebox.askyesnocancel",
return_value=None,
):
# Act: Trigger window closing
editor_for_closing_tests.on_closing()
# Assert: Nothing should happen
editor_for_closing_tests.save_component_json.assert_not_called()
editor_for_closing_tests.root.destroy.assert_not_called()
class TestWidgetCreationWorkflows:
"""Test user workflows for widget creation and management."""
@pytest.fixture
def editor_with_realistic_data(self, mock_filesystem: MagicMock) -> ComponentEditorWindowBase:
"""Fixture providing an editor with realistic component data."""
data_model = MagicMock(spec=ComponentDataModel)
data_model.get_all_components.return_value = REALISTIC_VEHICLE_DATA
editor = ComponentEditorWindowBase.create_for_testing(
version="1.0.0", local_filesystem=mock_filesystem, data_model=data_model
)
# Mock widget creation methods to avoid UI dependencies
editor._add_widget = MagicMock()
editor.scroll_frame = MagicMock()
editor.scroll_frame.view_port = MagicMock()
return editor
def test_user_sees_all_components_populated_in_interface(
self, editor_with_realistic_data: ComponentEditorWindowBase
) -> None:
"""
User sees all their vehicle components populated in the interface.
GIVEN: A user has multiple vehicle components configured
WHEN: The interface populates the component widgets
THEN: Each component should be processed and displayed
"""
# Act: Populate the interface frames
editor_with_realistic_data.populate_frames()
# Assert: All components should be processed
call_count = editor_with_realistic_data._add_widget.call_count
expected_components = len(REALISTIC_VEHICLE_DATA)
assert call_count == expected_components
def test_user_can_interact_with_different_widget_types(
self, editor_with_realistic_data: ComponentEditorWindowBase
) -> None:
"""
User can interact with different types of component widgets.
GIVEN: A user has various types of component data (dictionaries and leaf values)
WHEN: They interact with the widget creation system
THEN: The system should handle both dict and non-dict values appropriately
"""
# Arrange: Test both dictionary and leaf value scenarios
test_parent = MagicMock()
# Act: Test dict value (should call _add_non_leaf_widget logic)
editor_with_realistic_data.add_widget(test_parent, "TestComponent", {"nested": "data"}, [])
# Act: Test leaf value (should call _add_leaf_widget logic)
editor_with_realistic_data.add_widget(test_parent, "TestValue", "simple_value", [])
# Assert: Widget addition should be called for both cases
assert editor_with_realistic_data._add_widget.call_count == 2
def test_populate_frames_yields_to_event_loop_every_five_components(
self, editor_with_realistic_data: ComponentEditorWindowBase
) -> None:
"""
populate_frames yields to the Tk event loop every fifth component to keep the UI responsive.
GIVEN: An editor with more than five components to display
WHEN: populate_frames is called
THEN: update_idletasks is called once per complete group of five components
"""
num_components = 15 # expect floor(15 / 5) == 3 yields
components = {f"Component_{i}": {"value": i} for i in range(num_components)}
editor_with_realistic_data.data_model.get_all_components.return_value = components
editor_with_realistic_data.populate_frames()
expected_yields = num_components // 5
assert editor_with_realistic_data.scroll_frame.view_port.update_idletasks.call_count == expected_yields
class TestComplexityComboboxWorkflows:
"""Test user workflows for GUI complexity management."""
@pytest.fixture
def editor_for_complexity_tests(self, mock_filesystem: MagicMock) -> ComponentEditorWindowBase:
"""Fixture providing an editor configured for complexity testing."""
editor = ComponentEditorWindowBase.create_for_testing(version="1.0.0", local_filesystem=mock_filesystem)
# Mock the complexity variable and UI refresh methods
editor.complexity_var = MagicMock()
editor.complexity_var.get.return_value = "simple"
editor.scroll_frame = MagicMock()
editor.scroll_frame.view_port = MagicMock()
editor.populate_frames = MagicMock()
return editor
@patch("ardupilot_methodic_configurator.frontend_tkinter_component_editor_base.ProgramSettings")
def test_user_can_change_gui_complexity_level(
self, mock_settings: MagicMock, editor_for_complexity_tests: ComponentEditorWindowBase
) -> None:
"""
User can change the GUI complexity level to match their expertise.
GIVEN: A user wants to adjust the interface complexity
WHEN: They change the complexity setting
THEN: The setting should be saved and interface should refresh
"""
# Arrange: Set up complexity change
editor_for_complexity_tests.complexity_var.get.return_value = "normal"
# Act: Trigger complexity change
editor_for_complexity_tests._on_complexity_changed()
# Assert: Setting should be saved and display refreshed
mock_settings.set_setting.assert_called_once_with("gui_complexity", "normal")
editor_for_complexity_tests.populate_frames.assert_called_once()
def test_user_sees_interface_refresh_after_complexity_change(
self, editor_for_complexity_tests: ComponentEditorWindowBase
) -> None:
"""
User sees the interface refresh when complexity level changes.
GIVEN: A user has changed the GUI complexity level
WHEN: The interface processes the complexity change
THEN: The component display should be refreshed with new settings
"""
# Act: Trigger interface refresh
editor_for_complexity_tests._refresh_component_display()
# Assert: Display should be refreshed
editor_for_complexity_tests.populate_frames.assert_called_once()
editor_for_complexity_tests.scroll_frame.view_port.update_idletasks.assert_called_once()
class TestModuleConstantsAndTypes:
"""Test module-level constants and type definitions."""
def test_window_dimensions_are_reasonable_for_user_interface(self) -> None:
"""
Window dimensions provide reasonable space for user interaction.
GIVEN: A user needs adequate space to work with component configuration
WHEN: The application defines window dimensions
THEN: The dimensions should be practical for desktop use
"""
# Assert: Window dimensions should be reasonable
assert WINDOW_WIDTH_PIX > 600 # Minimum reasonable width
assert VEHICLE_IMAGE_WIDTH_PIX > 50 # Visible image size
def test_entry_widget_type_alias_supports_expected_widget_types(self) -> None:
"""
Entry widget type alias includes all expected UI widget types.
GIVEN: A user interacts with different types of input widgets
WHEN: The system defines widget types
THEN: The type alias should include common tkinter input widgets
"""
# Assert: Type alias should include expected types
origin = get_origin(EntryWidget)
args = get_args(EntryWidget)
assert origin is Union
assert len(args) >= 2 # Should include at least Entry and Combobox
def test_argparse_arguments_include_component_editor_options(self) -> None:
"""
Argument parser includes options relevant to component editor functionality.
GIVEN: A user needs to configure component editor behavior
WHEN: Command line arguments are defined
THEN: Component editor specific options should be available
"""
# Arrange: Create test parser
parser = ArgumentParser()
# Act: Add component editor arguments
ComponentEditorWindowBase.add_argparse_arguments(parser)
# Assert: Component editor arguments should be added
# This tests the method exists and can be called
assert hasattr(ComponentEditorWindowBase, "add_argparse_arguments")
class TestCreateForTestingFactory:
"""Test the factory method for creating test instances."""
def test_factory_creates_instance_with_minimal_dependencies(self) -> None:
"""
Factory method creates usable instances with minimal dependencies.
GIVEN: A developer needs to create test instances
WHEN: They use the create_for_testing factory method
THEN: A properly configured instance should be created
"""
# Act: Create instance using factory
editor = ComponentEditorWindowBase.create_for_testing()
# Assert: Instance should be created with expected attributes
assert isinstance(editor, ComponentEditorWindowBase)
assert hasattr(editor, "data_model")
assert hasattr(editor, "local_filesystem")
def test_factory_accepts_custom_parameters_for_flexible_testing(self) -> None:
"""
Factory method accepts custom parameters for flexible test scenarios.
GIVEN: A developer needs specific test configurations
WHEN: They provide custom parameters to the factory
THEN: The instance should use the provided parameters
"""
# Arrange: Create custom test dependencies
custom_filesystem = MagicMock(spec=LocalFilesystem)
custom_data_model = MagicMock(spec=ComponentDataModel)
custom_version = "test_version"
# Act: Create instance with custom parameters
editor = ComponentEditorWindowBase.create_for_testing(
version=custom_version, local_filesystem=custom_filesystem, data_model=custom_data_model
)
# Assert: Custom parameters should be used
assert editor.version == custom_version
assert editor.local_filesystem == custom_filesystem
assert editor.data_model == custom_data_model