Skip to content

Commit fa7835f

Browse files
[3.13] gh-154357: Fix tkinter, ttk and IDLE tests depending on the window manager (GH-154370) (GH-157065)
The window manager can take the focus from the application, ignore lift() and resize a toplevel on its own. * Hide the root window in the dialog tests, so that it does not compete for the focus. * Take the focus right before generating a key event. * Tolerate additional focus events. * Do not check focus_get() and focus_displayof() without the focus. * Resize the toplevel to fit its content in wait_until_mapped(). (cherry picked from commit 3b56438)
1 parent 19693af commit fa7835f

6 files changed

Lines changed: 46 additions & 15 deletions

File tree

Lib/idlelib/idle_test/test_configdialog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ def test_fontlist_key(self):
151151
font = d.fontlist.get('active')
152152

153153
# Test Down key.
154-
fontlist.focus_force()
155154
fontlist.update()
155+
fontlist.focus_force()
156156
fontlist.event_generate('<Key-Down>')
157157
fontlist.event_generate('<KeyRelease-Down>')
158158

@@ -161,8 +161,8 @@ def test_fontlist_key(self):
161161
self.assertIn(d.font_name.get(), down_font.lower())
162162

163163
# Test Up key.
164-
fontlist.focus_force()
165164
fontlist.update()
165+
fontlist.focus_force()
166166
fontlist.event_generate('<Key-Up>')
167167
fontlist.event_generate('<KeyRelease-Up>')
168168

Lib/test/test_tkinter/support.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,16 @@ def require_mapped(self, widget, timeout=None):
6161
f'(timed out after {timeout:g}s)')
6262

6363

64+
class AbstractDialogTest(AbstractTkTest):
65+
# Tk delivers generated keyboard events to the focused window. Hide the
66+
# root window, otherwise the window manager can take the focus back from
67+
# the dialog (gh-154357).
68+
69+
def setUp(self):
70+
super().setUp()
71+
self.root.withdraw()
72+
73+
6474
class AbstractDefaultRootTest:
6575

6676
def setUp(self):
@@ -112,6 +122,7 @@ def wait_until_mapped(widget, timeout=None, *, full_size=False):
112122
timeout = support.LOOPBACK_TIMEOUT
113123
deadline = time.monotonic() + timeout
114124
widget.update_idletasks()
125+
reset = False
115126
while True:
116127
widget.update() # drain pending Map/Configure events
117128
if widget.winfo_ismapped():
@@ -123,6 +134,11 @@ def wait_until_mapped(widget, timeout=None, *, full_size=False):
123134
h_ok = widget.winfo_height() > 1
124135
if w_ok and h_ok:
125136
return True
137+
if full_size and not reset:
138+
# Tk no longer resizes the toplevel to fit its content if
139+
# the window manager has resized it. Undo this.
140+
widget.winfo_toplevel().wm_geometry('')
141+
reset = True
126142
if time.monotonic() >= deadline:
127143
return False
128144
time.sleep(0.01)

Lib/test/test_tkinter/test_filedialog.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from tkinter.commondialog import Dialog
55
from test.support import requires, swap_attr
66
from test.test_tkinter.support import setUpModule # noqa: F401
7-
from test.test_tkinter.support import AbstractTkTest
7+
from test.test_tkinter.support import AbstractDialogTest, AbstractTkTest
88

99
requires('gui')
1010

@@ -37,7 +37,7 @@ def test_directory(self):
3737
self.check(filedialog.Directory, 'tk_chooseDirectory')
3838

3939

40-
class FileDialogTest(AbstractTkTest, unittest.TestCase):
40+
class FileDialogTest(AbstractDialogTest, unittest.TestCase):
4141
# The pure-Python FileDialog runs its own modal loop in go(); its logic is
4242
# exercised here without entering the loop.
4343

Lib/test/test_tkinter/test_misc.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,15 +439,20 @@ def test_focus_methods(self):
439439
self.root.update_idletasks()
440440
f.focus_force()
441441
self.root.update()
442-
self.assertIs(self.root.focus_get(), f)
443-
self.assertIs(self.root.focus_displayof(), f)
442+
# The window manager can take the focus away, and then focus_get()
443+
# and focus_displayof() return None.
444+
if self.root.focus_displayof() is not None:
445+
self.assertIs(self.root.focus_get(), f)
446+
self.assertIs(self.root.focus_displayof(), f)
444447
self.assertIs(f.focus_lastfor(), f)
445448
b = tkinter.Button(f)
446449
b.pack()
447450
self.root.update()
448451
b.focus_set()
449452
self.root.update()
450-
self.assertIs(self.root.focus_get(), b)
453+
if self.root.focus_displayof() is not None:
454+
self.assertIs(self.root.focus_get(), b)
455+
self.assertIs(f.focus_lastfor(), b)
451456

452457
def test_focus_methods_unresolvable(self):
453458
# The focus may be on a widget that tkinter did not create and so
@@ -1152,7 +1157,9 @@ def test_focus(self):
11521157

11531158
f.focus_force()
11541159
self.root.update()
1155-
self.assertEqual(len(events), 1, events)
1160+
# The window manager can take the focus away and give it back,
1161+
# which makes Tk generate additional focus events.
1162+
self.assertGreaterEqual(len(events), 1, events)
11561163
e = events[0]
11571164
self.assertIs(e.type, tkinter.EventType.FocusIn)
11581165
self.assertIs(e.widget, f)

Lib/test/test_tkinter/test_simpledialog.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from tkinter import messagebox
44
from test.support import requires, swap_attr
55
from test.test_tkinter.support import setUpModule # noqa: F401
6-
from test.test_tkinter.support import AbstractDefaultRootTest, AbstractTkTest
6+
from test.test_tkinter.support import (AbstractDefaultRootTest,
7+
AbstractDialogTest, AbstractTkTest)
78
from tkinter.simpledialog import (Dialog, SimpleDialog,
89
askinteger, askfloat, askstring,
910
_QueryInteger, _QueryFloat, _QueryString)
@@ -12,6 +13,10 @@
1213

1314

1415
class SimpleDialogTest(AbstractTkTest, unittest.TestCase):
16+
# The root window is not hidden here (cf. AbstractDialogTest): SimpleDialog
17+
# makes its window transient for the master unconditionally, and a
18+
# transient of a hidden window is never mapped on Windows, so that go()
19+
# would block in wait_visibility().
1520
# SimpleDialog's modal loop is in go(); its bindings are exercised here by
1621
# generating events on the constructed dialog, without entering the loop.
1722

@@ -50,8 +55,8 @@ def test_return_activates_default(self):
5055
# <Return> invokes the default button.
5156
d = self.create() # default 0
5257
self.require_mapped(d.root)
53-
d.root.focus_force()
5458
d.root.update()
59+
d.root.focus_force()
5560
d.root.event_generate('<Return>')
5661
d.root.update()
5762
self.assertEqual(d.num, 0)
@@ -61,10 +66,9 @@ def test_return_no_default(self):
6166
# open instead of activating a button.
6267
d = self.create(default=None)
6368
self.require_mapped(d.root)
64-
d.root.focus_force()
65-
d.root.update()
6669
bells = []
6770
with swap_attr(d.root, 'bell', lambda *a, **k: bells.append(True)):
71+
d.root.focus_force()
6872
d.root.event_generate('<Return>')
6973
d.root.update()
7074
self.assertTrue(bells) # rang the bell
@@ -98,7 +102,7 @@ def test_go(self):
98102
self.assertEqual(d.go(), 0)
99103

100104

101-
class DialogTest(AbstractTkTest, unittest.TestCase):
105+
class DialogTest(AbstractDialogTest, unittest.TestCase):
102106
# Dialog is a base class for custom dialogs; exercise it via _QueryInteger.
103107

104108
def open(self, **kw):
@@ -167,7 +171,7 @@ def mock_wait_window(w):
167171
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
168172

169173

170-
class QueryDialogTest(AbstractTkTest, unittest.TestCase):
174+
class QueryDialogTest(AbstractDialogTest, unittest.TestCase):
171175
# The query dialogs are modal: their __init__ blocks in wait_window().
172176
# Mock that out so the dialog stays alive and can be driven with generated
173177
# events, exercising the <Return>/<Escape> bindings and the validation.

Lib/test/test_ttk/test_widgets.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2031,19 +2031,23 @@ def test_virtual_events(self):
20312031
lambda e: selects.append(self.tv.selection()))
20322032
self.tv.bind('<<TreeviewOpen>>', lambda e: opens.append(self.tv.focus()))
20332033
self.tv.bind('<<TreeviewClose>>', lambda e: closes.append(self.tv.focus()))
2034-
self.tv.focus_force()
20352034
self.tv.focus(parent)
20362035
self.tv.selection_set(parent)
20372036
self.tv.update()
20382037

2038+
# Force the focus right before generating the event: the window
2039+
# manager can take it back.
2040+
self.tv.focus_force()
20392041
self.tv.event_generate('<Right>') # Open the focused parent.
20402042
self.tv.update()
20412043
self.assertEqual(opens, [parent])
20422044

2045+
self.tv.focus_force()
20432046
self.tv.event_generate('<Left>') # Close it again.
20442047
self.tv.update()
20452048
self.assertEqual(closes, [parent])
20462049

2050+
self.tv.focus_force()
20472051
self.tv.event_generate('<Down>') # Move the selection.
20482052
self.tv.update()
20492053
self.assertEqual(self.tv.selection(), (item2,))

0 commit comments

Comments
 (0)