|
10 | 10 | SPDX-License-Identifier: GPL-3.0-or-later
|
11 | 11 | """
|
12 | 12 |
|
| 13 | +import contextlib |
13 | 14 | import tkinter as tk
|
14 |
| -import unittest |
| 15 | +from collections.abc import Generator |
15 | 16 | from tkinter import ttk
|
16 | 17 | from unittest.mock import patch
|
17 | 18 |
|
18 |
| -from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import ( |
19 |
| - AutoResizeCombobox, |
20 |
| - update_combobox_width, |
21 |
| -) |
| 19 | +import pytest |
22 | 20 |
|
| 21 | +from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import AutoResizeCombobox, update_combobox_width |
23 | 22 |
|
24 |
| -class TestUpdateComboboxWidth(unittest.TestCase): |
| 23 | + |
| 24 | +@pytest.fixture(scope="session") |
| 25 | +def root() -> Generator[tk.Tk, None, None]: |
| 26 | + """Create and clean up Tk root window for testing.""" |
| 27 | + # Try to reuse existing root or create new one |
| 28 | + try: |
| 29 | + root = tk._default_root # type: ignore[attr-defined] |
| 30 | + if root is None: |
| 31 | + root = tk.Tk() |
| 32 | + except (AttributeError, tk.TclError): |
| 33 | + root = tk.Tk() |
| 34 | + |
| 35 | + root.withdraw() # Hide the main window during tests |
| 36 | + |
| 37 | + # Patch the iconphoto method to prevent errors with mock PhotoImage |
| 38 | + original_iconphoto = root.iconphoto |
| 39 | + |
| 40 | + def mock_iconphoto(*args, **kwargs) -> None: |
| 41 | + pass |
| 42 | + |
| 43 | + root.iconphoto = mock_iconphoto # type: ignore[method-assign] |
| 44 | + |
| 45 | + yield root |
| 46 | + |
| 47 | + # Restore original method and destroy root |
| 48 | + root.iconphoto = original_iconphoto # type: ignore[method-assign] |
| 49 | + |
| 50 | + # Only destroy if we're the last test |
| 51 | + with contextlib.suppress(tk.TclError): |
| 52 | + root.quit() # Close the event loop |
| 53 | + |
| 54 | + |
| 55 | +@pytest.fixture |
| 56 | +def test_combobox(root: tk.Tk) -> ttk.Combobox: |
| 57 | + """Create a test combobox for width testing.""" |
| 58 | + frame = ttk.Frame(root) |
| 59 | + frame.pack() |
| 60 | + return ttk.Combobox(frame) |
| 61 | + |
| 62 | + |
| 63 | +class TestUpdateComboboxWidth: |
25 | 64 | """Test cases for the update_combobox_width function."""
|
26 | 65 |
|
27 |
| - def test_update_combobox_width(self) -> None: |
28 |
| - combobox = ttk.Combobox(values=["short", "longer", "longest"]) |
29 |
| - update_combobox_width(combobox) |
30 |
| - assert combobox.cget("width") == 7 |
| 66 | + def test_combobox_width_adjusts_to_longest_value(self, test_combobox: ttk.Combobox) -> None: |
| 67 | + """ |
| 68 | + Combobox width adjusts automatically to accommodate the longest value. |
| 69 | +
|
| 70 | + GIVEN: A combobox with values of different lengths |
| 71 | + WHEN: The update_combobox_width function is called |
| 72 | + THEN: The width should be set to accommodate the longest value |
| 73 | + """ |
| 74 | + # Arrange (Given): Set combobox values with different lengths |
| 75 | + test_combobox["values"] = ["short", "longer", "longest"] |
| 76 | + |
| 77 | + # Act (When): Update the combobox width |
| 78 | + update_combobox_width(test_combobox) |
| 79 | + |
| 80 | + # Assert (Then): Width should accommodate longest value (7 characters) |
| 81 | + assert test_combobox.cget("width") == 7 |
| 82 | + |
| 83 | + def test_combobox_uses_minimum_width_when_values_empty(self, test_combobox: ttk.Combobox) -> None: |
| 84 | + """ |
| 85 | + Combobox uses minimum width when no values are present. |
| 86 | +
|
| 87 | + GIVEN: A combobox with no values |
| 88 | + WHEN: The update_combobox_width function is called |
| 89 | + THEN: The width should be set to the minimum value (4) |
| 90 | + """ |
| 91 | + # Arrange (Given): Set empty values |
| 92 | + test_combobox["values"] = [] |
| 93 | + |
| 94 | + # Act (When): Update the combobox width |
| 95 | + update_combobox_width(test_combobox) |
| 96 | + |
| 97 | + # Assert (Then): Should use minimum width |
| 98 | + assert test_combobox.cget("width") == 4 |
31 | 99 |
|
32 |
| - def test_update_combobox_width_empty_values(self) -> None: |
33 |
| - combobox = ttk.Combobox(values=[]) |
34 |
| - update_combobox_width(combobox) |
35 |
| - # Should use the minimum width (4) when no values |
36 |
| - assert combobox.cget("width") == 4 |
| 100 | + def test_combobox_uses_minimum_width_for_very_short_values(self, test_combobox: ttk.Combobox) -> None: |
| 101 | + """ |
| 102 | + Combobox uses minimum width when all values are very short. |
37 | 103 |
|
38 |
| - def test_update_combobox_width_very_short_values(self) -> None: |
39 |
| - combobox = ttk.Combobox(values=["a", "b", "c"]) |
40 |
| - update_combobox_width(combobox) |
41 |
| - # Should use the minimum width (4) when values are short |
42 |
| - assert combobox.cget("width") == 4 |
| 104 | + GIVEN: A combobox with very short values |
| 105 | + WHEN: The update_combobox_width function is called |
| 106 | + THEN: The width should be set to the minimum value (4) |
| 107 | + """ |
| 108 | + # Arrange (Given): Set very short values |
| 109 | + test_combobox["values"] = ["a", "b", "c"] |
43 | 110 |
|
| 111 | + # Act (When): Update the combobox width |
| 112 | + update_combobox_width(test_combobox) |
44 | 113 |
|
45 |
| -class TestAutoResizeCombobox(unittest.TestCase): |
| 114 | + # Assert (Then): Should use minimum width |
| 115 | + assert test_combobox.cget("width") == 4 |
| 116 | + |
| 117 | + |
| 118 | +@pytest.fixture |
| 119 | +def auto_resize_combobox(root: tk.Tk) -> AutoResizeCombobox: |
| 120 | + """Create an AutoResizeCombobox for testing.""" |
| 121 | + frame = ttk.Frame(root) |
| 122 | + frame.pack() |
| 123 | + return AutoResizeCombobox(frame, values=["one", "two", "three"], selected_element="two", tooltip="Test Tooltip") |
| 124 | + |
| 125 | + |
| 126 | +class TestAutoResizeCombobox: |
46 | 127 | """Test cases for the AutoResizeCombobox class."""
|
47 | 128 |
|
48 |
| - def setUp(self) -> None: |
49 |
| - self.root = tk.Tk() |
50 |
| - self.root.withdraw() # Hide the main window during tests |
51 |
| - self.combobox = AutoResizeCombobox( |
52 |
| - self.root, values=["one", "two", "three"], selected_element="two", tooltip="Test Tooltip" |
53 |
| - ) |
| 129 | + def test_user_can_see_initial_selection_in_combobox(self, auto_resize_combobox: AutoResizeCombobox) -> None: |
| 130 | + """ |
| 131 | + User can see the initially selected value in the combobox. |
| 132 | +
|
| 133 | + GIVEN: An AutoResizeCombobox with predefined values and initial selection |
| 134 | + WHEN: The combobox is created with "two" as selected element |
| 135 | + THEN: The combobox should display "two" as the current value |
| 136 | + """ |
| 137 | + # Arrange (Given): AutoResizeCombobox created with initial selection |
| 138 | + # Act (When): Check the current value (already set during creation) |
| 139 | + # Assert (Then): Should display the initially selected value |
| 140 | + assert auto_resize_combobox.get() == "two" |
54 | 141 |
|
55 |
| - def tearDown(self) -> None: |
56 |
| - self.root.destroy() |
| 142 | + def test_user_can_update_combobox_values_and_selection(self, auto_resize_combobox: AutoResizeCombobox) -> None: |
| 143 | + """ |
| 144 | + User can update both the available values and current selection. |
57 | 145 |
|
58 |
| - def test_initial_selection(self) -> None: |
59 |
| - assert self.combobox.get() == "two" |
| 146 | + GIVEN: An existing AutoResizeCombobox with initial values |
| 147 | + WHEN: New values and selection are set using set_entries_tuple |
| 148 | + THEN: The combobox should reflect the new values and selection |
| 149 | + """ |
| 150 | + # Arrange (Given): Existing combobox with initial values |
| 151 | + new_values = ["four", "five", "six"] |
| 152 | + new_selection = "five" |
60 | 153 |
|
61 |
| - def test_update_values(self) -> None: |
62 |
| - self.combobox.set_entries_tuple(["four", "five", "six"], "five") |
63 |
| - assert self.combobox.get() == "five" |
64 |
| - assert self.combobox["values"] == ("four", "five", "six") |
| 154 | + # Act (When): Update values and selection |
| 155 | + auto_resize_combobox.set_entries_tuple(new_values, new_selection) |
65 | 156 |
|
66 |
| - def test_set_entries_with_spaces(self) -> None: |
67 |
| - """Test values with spaces.""" |
68 |
| - values = ["option one", "option two", "option three"] |
69 |
| - self.combobox.set_entries_tuple(values, "option two") |
70 |
| - assert self.combobox["values"] == tuple(values) |
71 |
| - assert self.combobox.get() == "option two" |
| 157 | + # Assert (Then): Should display new values and selection |
| 158 | + assert auto_resize_combobox.get() == new_selection |
| 159 | + assert auto_resize_combobox["values"] == tuple(new_values) |
| 160 | + |
| 161 | + def test_combobox_handles_values_with_spaces_correctly(self, auto_resize_combobox: AutoResizeCombobox) -> None: |
| 162 | + """ |
| 163 | + Combobox preserves values with spaces exactly as provided. |
| 164 | +
|
| 165 | + GIVEN: Values containing various amounts of spaces |
| 166 | + WHEN: These values are set in the combobox |
| 167 | + THEN: All spaces should be preserved exactly |
| 168 | + """ |
| 169 | + # Arrange (Given): Values with different space patterns |
| 170 | + values_with_spaces = ["option one", "option two", "option three"] |
| 171 | + selected_value = "option two" |
| 172 | + |
| 173 | + # Act (When): Set values with spaces |
| 174 | + auto_resize_combobox.set_entries_tuple(values_with_spaces, selected_value) |
| 175 | + |
| 176 | + # Assert (Then): Spaces should be preserved |
| 177 | + assert auto_resize_combobox["values"] == tuple(values_with_spaces) |
| 178 | + assert auto_resize_combobox.get() == selected_value |
72 | 179 |
|
73 | 180 | @patch("ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox.logging_error")
|
74 |
| - def test_set_entries_invalid_selection(self, mock_logging_error) -> None: |
75 |
| - """Test when selected element is not in values list.""" |
76 |
| - values = ["one", "two", "three"] |
77 |
| - self.combobox.set_entries_tuple(values, "four") |
| 181 | + def test_system_logs_error_when_invalid_selection_provided( |
| 182 | + self, mock_logging_error, auto_resize_combobox: AutoResizeCombobox |
| 183 | + ) -> None: |
| 184 | + """ |
| 185 | + System logs an error when user provides invalid selection. |
| 186 | +
|
| 187 | + GIVEN: A combobox with valid values |
| 188 | + WHEN: An invalid selection that's not in the values list is provided |
| 189 | + THEN: An error should be logged and the selection should remain unchanged |
| 190 | + """ |
| 191 | + # Arrange (Given): Valid values and invalid selection |
| 192 | + valid_values = ["one", "two", "three"] |
| 193 | + invalid_selection = "four" |
| 194 | + original_value = auto_resize_combobox.get() |
78 | 195 |
|
79 |
| - # Should log an error |
| 196 | + # Act (When): Try to set invalid selection |
| 197 | + auto_resize_combobox.set_entries_tuple(valid_values, invalid_selection) |
| 198 | + |
| 199 | + # Assert (Then): Error logged and value unchanged |
80 | 200 | mock_logging_error.assert_called_once()
|
81 |
| - # Selected value should not be set |
82 |
| - assert self.combobox.get() == "two" # Maintains previous value |
| 201 | + assert auto_resize_combobox.get() == original_value |
83 | 202 |
|
84 | 203 | @patch("ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox.logging_warning")
|
85 |
| - def test_set_entries_no_selection(self, mock_logging_warning) -> None: |
86 |
| - """Test when no selection is provided.""" |
87 |
| - values = ["one", "two", "three"] |
88 |
| - self.combobox.set_entries_tuple(values, "") |
| 204 | + def test_system_logs_warning_when_no_selection_provided( |
| 205 | + self, mock_logging_warning, auto_resize_combobox: AutoResizeCombobox |
| 206 | + ) -> None: |
| 207 | + """ |
| 208 | + System logs a warning when no selection is provided. |
| 209 | +
|
| 210 | + GIVEN: A combobox with valid values |
| 211 | + WHEN: An empty selection is provided |
| 212 | + THEN: A warning should be logged |
| 213 | + """ |
| 214 | + # Arrange (Given): Valid values and empty selection |
| 215 | + valid_values = ["one", "two", "three"] |
| 216 | + empty_selection = "" |
| 217 | + |
| 218 | + # Act (When): Set empty selection |
| 219 | + auto_resize_combobox.set_entries_tuple(valid_values, empty_selection) |
89 | 220 |
|
90 |
| - # Should log a warning |
| 221 | + # Assert (Then): Warning should be logged |
91 | 222 | mock_logging_warning.assert_called_once()
|
92 | 223 |
|
93 | 224 | @patch("ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox.update_combobox_width")
|
94 |
| - def test_set_entries_empty_values(self, mock_update_width) -> None: |
95 |
| - """Test behavior with empty values list.""" |
96 |
| - self.combobox.set_entries_tuple([], "") |
| 225 | + def test_width_update_skipped_for_empty_values(self, mock_update_width, auto_resize_combobox: AutoResizeCombobox) -> None: |
| 226 | + """ |
| 227 | + Width update is not called when values list is empty. |
| 228 | +
|
| 229 | + GIVEN: A combobox that supports width updating |
| 230 | + WHEN: An empty values list is provided |
| 231 | + THEN: The width update function should not be called |
| 232 | + """ |
| 233 | + # Arrange (Given): Empty values list |
| 234 | + empty_values = [] |
| 235 | + |
| 236 | + # Act (When): Set empty values |
| 237 | + auto_resize_combobox.set_entries_tuple(empty_values, "") |
97 | 238 |
|
98 |
| - # Width update should not be called with empty values |
| 239 | + # Assert (Then): Width update should not be called |
99 | 240 | mock_update_width.assert_not_called()
|
100 | 241 |
|
101 | 242 | @patch("ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox.show_tooltip")
|
102 |
| - def test_tooltip_display(self, mock_show_tooltip) -> None: |
103 |
| - """Test tooltip is shown when provided.""" |
104 |
| - self.combobox.set_entries_tuple(["one", "two"], "one", "Help text") |
| 243 | + def test_tooltip_displays_when_help_text_provided( |
| 244 | + self, mock_show_tooltip, auto_resize_combobox: AutoResizeCombobox |
| 245 | + ) -> None: |
| 246 | + """ |
| 247 | + Tooltip is displayed when help text is provided. |
105 | 248 |
|
106 |
| - # Tooltip should be shown |
107 |
| - mock_show_tooltip.assert_called_once_with(self.combobox, "Help text") |
| 249 | + GIVEN: A combobox that supports tooltips |
| 250 | + WHEN: Values are set with tooltip text provided |
| 251 | + THEN: The tooltip should be displayed with the help text |
| 252 | + """ |
| 253 | + # Arrange (Given): Values and help text |
| 254 | + values = ["one", "two"] |
| 255 | + selection = "one" |
| 256 | + help_text = "Help text" |
| 257 | + |
| 258 | + # Act (When): Set values with tooltip |
| 259 | + auto_resize_combobox.set_entries_tuple(values, selection, help_text) |
| 260 | + |
| 261 | + # Assert (Then): Tooltip should be shown |
| 262 | + mock_show_tooltip.assert_called_once_with(auto_resize_combobox, help_text) |
108 | 263 |
|
109 | 264 | @patch("ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox.show_tooltip")
|
110 |
| - def test_no_tooltip_when_none(self, mock_show_tooltip) -> None: |
111 |
| - """Test tooltip is not shown when None.""" |
112 |
| - self.combobox.set_entries_tuple(["one", "two"], "one", None) |
| 265 | + def test_tooltip_not_displayed_when_no_help_text( |
| 266 | + self, mock_show_tooltip, auto_resize_combobox: AutoResizeCombobox |
| 267 | + ) -> None: |
| 268 | + """ |
| 269 | + Tooltip is not displayed when no help text is provided. |
113 | 270 |
|
114 |
| - # Tooltip should not be shown |
115 |
| - mock_show_tooltip.assert_not_called() |
| 271 | + GIVEN: A combobox that supports tooltips |
| 272 | + WHEN: Values are set with None as tooltip text |
| 273 | + THEN: The tooltip should not be displayed |
| 274 | + """ |
| 275 | + # Arrange (Given): Values without help text |
| 276 | + values = ["one", "two"] |
| 277 | + selection = "one" |
| 278 | + no_help_text = None |
116 | 279 |
|
| 280 | + # Act (When): Set values without tooltip |
| 281 | + auto_resize_combobox.set_entries_tuple(values, selection, no_help_text) |
117 | 282 |
|
118 |
| -if __name__ == "__main__": |
119 |
| - unittest.main() |
| 283 | + # Assert (Then): Tooltip should not be shown |
| 284 | + mock_show_tooltip.assert_not_called() |
0 commit comments