Skip to content

Commit 3b56438

Browse files
gh-154357: Fix tkinter, ttk and IDLE tests depending on the window manager (GH-154370)
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. * Use override-redirect toplevels on X11 in test_wm_stackorder. * Resize the toplevel to fit its content in wait_until_mapped(). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7a1d2c4 commit 3b56438

6 files changed

Lines changed: 65 additions & 29 deletions

File tree

Lib/idlelib/idle_test/test_configdialog.py

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

152152
# Test Down key.
153-
fontlist.focus_force()
154153
fontlist.update()
154+
fontlist.focus_force()
155155
fontlist.event_generate('<Key-Down>')
156156
fontlist.event_generate('<KeyRelease-Down>')
157157

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

162162
# Test Up key.
163-
fontlist.focus_force()
164163
fontlist.update()
164+
fontlist.focus_force()
165165
fontlist.event_generate('<Key-Up>')
166166
fontlist.event_generate('<KeyRelease-Up>')
167167

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: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from tkinter.commondialog import Dialog
77
from test.support import requires, swap_attr
88
from test.test_tkinter.support import setUpModule # noqa: F401
9-
from test.test_tkinter.support import AbstractTkTest
9+
from test.test_tkinter.support import AbstractDialogTest, AbstractTkTest
1010

1111
requires('gui')
1212

@@ -72,7 +72,7 @@ def test_results_preserved(self):
7272
('/a', '/b'))
7373

7474

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

@@ -164,8 +164,8 @@ def test_alt_key(self):
164164
d = self.open()
165165
invoked = []
166166
d.cancel_button.configure(command=lambda: invoked.append(True))
167-
d.top.focus_force()
168167
d.top.update()
168+
d.top.focus_force()
169169
d.top.event_generate('<Alt-c>') # "&Cancel"
170170
d.top.update()
171171
self.assertTrue(invoked)
@@ -174,8 +174,8 @@ def test_escape_cancels(self):
174174
# The Escape key cancels the dialog.
175175
d = self.open()
176176
d.how = 'spam'
177-
d.top.focus_force()
178177
d.top.update()
178+
d.top.focus_force()
179179
d.top.event_generate('<Escape>')
180180
d.top.update()
181181
self.assertIsNone(d.how)
@@ -195,8 +195,10 @@ def test_type_ahead(self):
195195
d.files.delete(0, 'end')
196196
for name in ('alpha', 'bravo', 'charlie'):
197197
d.files.insert('end', name)
198-
d.files.focus_force()
199198
d.top.update()
199+
# Force the focus right before generating the event: the window
200+
# manager can take it back.
201+
d.files.focus_force()
200202
d.files.event_generate('<Key>', keysym='c')
201203
d.top.update()
202204
sel = d.files.curselection()

Lib/test/test_tkinter/test_misc.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from test.test_tkinter.support import setUpModule # noqa: F401
1818
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
1919
requires_tk, get_tk_patchlevel,
20-
tcl_version, tk_version)
20+
tcl_version, tk_version,
21+
wait_until_mapped)
2122

2223
support.requires('gui')
2324

@@ -508,15 +509,20 @@ def test_focus_methods(self):
508509
self.root.update_idletasks()
509510
f.focus_force()
510511
self.root.update()
511-
self.assertIs(self.root.focus_get(), f)
512-
self.assertIs(self.root.focus_displayof(), f)
512+
# The window manager can take the focus away, and then focus_get()
513+
# and focus_displayof() return None.
514+
if self.root.focus_displayof() is not None:
515+
self.assertIs(self.root.focus_get(), f)
516+
self.assertIs(self.root.focus_displayof(), f)
513517
self.assertIs(f.focus_lastfor(), f)
514518
b = tkinter.Button(f)
515519
b.pack()
516520
self.root.update()
517521
b.focus_set()
518522
self.root.update()
519-
self.assertIs(self.root.focus_get(), b)
523+
if self.root.focus_displayof() is not None:
524+
self.assertIs(self.root.focus_get(), b)
525+
self.assertIs(f.focus_lastfor(), b)
520526

521527
def test_focus_methods_unresolvable(self):
522528
# The focus may be on a widget that tkinter did not create and so
@@ -1319,9 +1325,15 @@ def test_wm_transient(self):
13191325
def test_wm_stackorder(self):
13201326
t1 = tkinter.Toplevel(self.root)
13211327
t2 = tkinter.Toplevel(self.root)
1328+
if self.root._windowingsystem == 'x11':
1329+
# Bypass the window manager, which may ignore lift() or reorder
1330+
# the windows while they are being mapped.
1331+
t1.overrideredirect(True)
1332+
t2.overrideredirect(True)
13221333
t1.deiconify()
13231334
t2.deiconify()
1324-
self.root.update()
1335+
wait_until_mapped(t1)
1336+
wait_until_mapped(t2)
13251337
t1.lift(t2) # Raise t1 above t2.
13261338
self.root.update()
13271339
order = self.root.wm_stackorder()
@@ -1361,7 +1373,9 @@ def test_focus(self):
13611373

13621374
f.focus_force()
13631375
self.root.update()
1364-
self.assertEqual(len(events), 1, events)
1376+
# The window manager can take the focus away and give it back,
1377+
# which makes Tk generate additional focus events.
1378+
self.assertGreaterEqual(len(events), 1, events)
13651379
e = events[0]
13661380
self.assertIs(e.type, tkinter.EventType.FocusIn)
13671381
self.assertIs(e.widget, f)

Lib/test/test_tkinter/test_simpledialog.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from tkinter import messagebox, ttk
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, AbstractDialogTest
77
from tkinter.simpledialog import (Dialog, SimpleDialog,
88
askinteger, askfloat, askstring,
99
_QueryInteger, _QueryFloat, _QueryString,
@@ -12,7 +12,7 @@
1212
requires('gui')
1313

1414

15-
class SimpleDialogTest(AbstractTkTest, unittest.TestCase):
15+
class SimpleDialogTest(AbstractDialogTest, unittest.TestCase):
1616
# SimpleDialog's modal loop is in go(); its bindings are exercised here by
1717
# generating events on the constructed dialog, without entering the loop.
1818

@@ -45,8 +45,8 @@ def test_use_ttk(self):
4545
ttk.Style(d.root).lookup('.', 'background'))
4646
# The bindings work with the themed buttons too.
4747
self.require_mapped(d.root)
48-
d._buttons[0].focus_force()
4948
d.root.update()
49+
d._buttons[0].focus_force()
5050
d.root.event_generate('<Return>')
5151
d.root.update()
5252
self.assertEqual(d.num, 0)
@@ -144,8 +144,8 @@ def test_alt_key(self):
144144
# the matching button (cf. tk::AmpWidget in tk::MessageBox).
145145
d = self.create(buttons=['Yes', {'text': 'No', 'underline': 0}])
146146
self.require_mapped(d.root)
147-
d._buttons[0].focus_force()
148147
d.root.update()
148+
d._buttons[0].focus_force()
149149
d.root.event_generate('<Alt-n>') # "No" -> underline 0 -> "N"
150150
d.root.update()
151151
self.assertEqual(d.num, 1)
@@ -155,8 +155,8 @@ def test_return_invokes_focused_button(self):
155155
# default and the focus was not moved by keyboard traversal.
156156
d = self.create(buttons=['Yes', 'No']) # default 0
157157
self.require_mapped(d.root)
158-
d._buttons[1].focus_force()
159158
d.root.update()
159+
d._buttons[1].focus_force()
160160
d.root.event_generate('<Return>')
161161
d.root.update()
162162
self.assertEqual(d.num, 1)
@@ -165,8 +165,8 @@ def test_focus_next_then_return(self):
165165
# <Tab> moves the focus to the next button; <Return> invokes it.
166166
d = self.create(buttons=['Yes', 'No'])
167167
self.require_mapped(d.root)
168-
d._buttons[0].focus_force()
169168
d.root.update()
169+
d._buttons[0].focus_force()
170170
d._buttons[0].event_generate('<Tab>')
171171
d.root.update()
172172
d.root.event_generate('<Return>')
@@ -177,8 +177,8 @@ def test_focus_prev_then_return(self):
177177
# <Shift-Tab> moves the focus to the previous button.
178178
d = self.create(buttons=['Yes', 'No'])
179179
self.require_mapped(d.root)
180-
d._buttons[1].focus_force()
181180
d.root.update()
181+
d._buttons[1].focus_force()
182182
d._buttons[1].event_generate('<Shift-Tab>')
183183
d.root.update()
184184
d.root.event_generate('<Return>')
@@ -189,8 +189,8 @@ def test_return_activates_default(self):
189189
# <Return> with the focus off the buttons invokes the default button.
190190
d = self.create() # default 0
191191
self.require_mapped(d.root)
192-
d.root.focus_force() # the dialog, not a button, has the focus
193192
d.root.update()
193+
d.root.focus_force() # the dialog, not a button, has the focus
194194
d.root.event_generate('<Return>')
195195
d.root.update()
196196
self.assertEqual(d.num, 0)
@@ -246,7 +246,7 @@ def test_go(self):
246246
self.assertEqual(d.go(), 0)
247247

248248

249-
class DialogTest(AbstractTkTest, unittest.TestCase):
249+
class DialogTest(AbstractDialogTest, unittest.TestCase):
250250
# Dialog's button box is modelled on tk::MessageBox.
251251

252252
def open(self, **kw):
@@ -279,8 +279,8 @@ def test_use_classic(self):
279279
invoked = []
280280
cancel = d.children['cancel']
281281
cancel.configure(command=lambda: invoked.append(True))
282-
cancel.focus_force()
283282
d.update()
283+
cancel.focus_force()
284284
d.event_generate('<Return>')
285285
d.update()
286286
self.assertTrue(invoked)
@@ -359,8 +359,8 @@ def test_alt_key(self):
359359
invoked = []
360360
cancel = d.children['cancel'] # "&Cancel"
361361
cancel.configure(command=lambda: invoked.append(True))
362-
d.focus_force()
363362
d.update()
363+
d.focus_force()
364364
d.event_generate('<Alt-c>')
365365
d.update()
366366
self.assertTrue(invoked)
@@ -371,8 +371,8 @@ def test_return_invokes_focused_button(self):
371371
invoked = []
372372
cancel = d.children['cancel']
373373
cancel.configure(command=lambda: invoked.append(True))
374-
cancel.focus_force()
375374
d.update()
375+
cancel.focus_force()
376376
d.event_generate('<Return>')
377377
d.update()
378378
self.assertEqual(invoked, [True])
@@ -384,8 +384,8 @@ def test_focus_next_then_return(self):
384384
for name in ('ok', 'cancel'):
385385
d.children[name].configure(command=lambda name=name: invoked.append(name))
386386
ok = d.children['ok']
387-
ok.focus_force()
388387
d.update()
388+
ok.focus_force()
389389
ok.event_generate('<Tab>') # OK -> Cancel
390390
d.update()
391391
d.event_generate('<Return>')
@@ -399,8 +399,8 @@ def test_focus_prev_then_return(self):
399399
for name in ('ok', 'cancel'):
400400
d.children[name].configure(command=lambda name=name: invoked.append(name))
401401
cancel = d.children['cancel']
402-
cancel.focus_force()
403402
d.update()
403+
cancel.focus_force()
404404
cancel.event_generate('<Shift-Tab>') # Cancel -> OK
405405
d.update()
406406
d.event_generate('<Return>')
@@ -432,7 +432,7 @@ def mock_wait_window(w):
432432
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
433433

434434

435-
class QueryDialogTest(AbstractTkTest, unittest.TestCase):
435+
class QueryDialogTest(AbstractDialogTest, unittest.TestCase):
436436
# The query dialogs are modal: their __init__ blocks in wait_window().
437437
# Mock that out so the dialog stays alive and can be driven with generated
438438
# 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
@@ -2024,19 +2024,23 @@ def test_virtual_events(self):
20242024
lambda e: selects.append(self.tv.selection()))
20252025
self.tv.bind('<<TreeviewOpen>>', lambda e: opens.append(self.tv.focus()))
20262026
self.tv.bind('<<TreeviewClose>>', lambda e: closes.append(self.tv.focus()))
2027-
self.tv.focus_force()
20282027
self.tv.focus(parent)
20292028
self.tv.selection_set(parent)
20302029
self.tv.update()
20312030

2031+
# Force the focus right before generating the event: the window
2032+
# manager can take it back.
2033+
self.tv.focus_force()
20322034
self.tv.event_generate('<Right>') # Open the focused parent.
20332035
self.tv.update()
20342036
self.assertEqual(opens, [parent])
20352037

2038+
self.tv.focus_force()
20362039
self.tv.event_generate('<Left>') # Close it again.
20372040
self.tv.update()
20382041
self.assertEqual(closes, [parent])
20392042

2043+
self.tv.focus_force()
20402044
self.tv.event_generate('<Down>') # Move the selection.
20412045
self.tv.update()
20422046
self.assertEqual(self.tv.selection(), (item2,))

0 commit comments

Comments
 (0)