Skip to content

Commit daf8abc

Browse files
committed
feat(autoresize combobox): Make the tests more robust when running in a uv managed environment
Converted the tests from unittests to BDD-pytests ✅ Fixed the root fixture - Changed it from function scope to session scope to prevent multiple Tk instance creation ✅ Improved cleanup - Used contextlib.suppress() for better error handling ✅ Added proper imports - Added the missing contextlib import The environment issue: Your UV-managed Python installation is missing critical Tcl/Tk library files The system Python works perfectly for Tkinter applicationss
1 parent f6101fa commit daf8abc

File tree

1 file changed

+234
-69
lines changed

1 file changed

+234
-69
lines changed

tests/test_frontend_tkinter_autoresize_combobox.py

Lines changed: 234 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,110 +10,275 @@
1010
SPDX-License-Identifier: GPL-3.0-or-later
1111
"""
1212

13+
import contextlib
1314
import tkinter as tk
14-
import unittest
15+
from collections.abc import Generator
1516
from tkinter import ttk
1617
from unittest.mock import patch
1718

18-
from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import (
19-
AutoResizeCombobox,
20-
update_combobox_width,
21-
)
19+
import pytest
2220

21+
from ardupilot_methodic_configurator.frontend_tkinter_autoresize_combobox import AutoResizeCombobox, update_combobox_width
2322

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:
2564
"""Test cases for the update_combobox_width function."""
2665

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
3199

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.
37103
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"]
43110

111+
# Act (When): Update the combobox width
112+
update_combobox_width(test_combobox)
44113

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:
46127
"""Test cases for the AutoResizeCombobox class."""
47128

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"
54141

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.
57145
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"
60153

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)
65156

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
72179

73180
@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()
78195

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
80200
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
83202

84203
@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)
89220

90-
# Should log a warning
221+
# Assert (Then): Warning should be logged
91222
mock_logging_warning.assert_called_once()
92223

93224
@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, "")
97238

98-
# Width update should not be called with empty values
239+
# Assert (Then): Width update should not be called
99240
mock_update_width.assert_not_called()
100241

101242
@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.
105248
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)
108263

109264
@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.
113270
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
116279

280+
# Act (When): Set values without tooltip
281+
auto_resize_combobox.set_entries_tuple(values, selection, no_help_text)
117282

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

Comments
 (0)