Skip to content
4 changes: 2 additions & 2 deletions Lib/idlelib/idle_test/test_configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ def test_fontlist_key(self):
font = d.fontlist.get('active')

# Test Down key.
fontlist.focus_force()
fontlist.update()
fontlist.focus_force()
fontlist.event_generate('<Key-Down>')
fontlist.event_generate('<KeyRelease-Down>')

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

# Test Up key.
fontlist.focus_force()
fontlist.update()
fontlist.focus_force()
fontlist.event_generate('<Key-Up>')
fontlist.event_generate('<KeyRelease-Up>')

Expand Down
16 changes: 16 additions & 0 deletions Lib/test/test_tkinter/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ def require_mapped(self, widget, timeout=None):
f'(timed out after {timeout:g}s)')


class AbstractDialogTest(AbstractTkTest):
# Tk delivers generated keyboard events to the focused window. Hide the
# root window, otherwise the window manager can take the focus back from
# the dialog (gh-154357).

def setUp(self):
super().setUp()
self.root.withdraw()


class AbstractDefaultRootTest:

def setUp(self):
Expand Down Expand Up @@ -112,6 +122,7 @@ def wait_until_mapped(widget, timeout=None, *, full_size=False):
timeout = support.LOOPBACK_TIMEOUT
deadline = time.monotonic() + timeout
widget.update_idletasks()
reset = False
while True:
widget.update() # drain pending Map/Configure events
if widget.winfo_ismapped():
Expand All @@ -123,6 +134,11 @@ def wait_until_mapped(widget, timeout=None, *, full_size=False):
h_ok = widget.winfo_height() > 1
if w_ok and h_ok:
return True
if full_size and not reset:
# Tk no longer resizes the toplevel to fit its content if
# the window manager has resized it. Undo this.
widget.winfo_toplevel().wm_geometry('')
reset = True
if time.monotonic() >= deadline:
return False
time.sleep(0.01)
Expand Down
12 changes: 7 additions & 5 deletions Lib/test/test_tkinter/test_filedialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from tkinter.commondialog import Dialog
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractTkTest
from test.test_tkinter.support import AbstractDialogTest, AbstractTkTest

requires('gui')

Expand Down Expand Up @@ -72,7 +72,7 @@ def test_results_preserved(self):
('/a', '/b'))


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

Expand Down Expand Up @@ -164,8 +164,8 @@ def test_alt_key(self):
d = self.open()
invoked = []
d.cancel_button.configure(command=lambda: invoked.append(True))
d.top.focus_force()
d.top.update()
d.top.focus_force()
d.top.event_generate('<Alt-c>') # "&Cancel"
d.top.update()
self.assertTrue(invoked)
Expand All @@ -174,8 +174,8 @@ def test_escape_cancels(self):
# The Escape key cancels the dialog.
d = self.open()
d.how = 'spam'
d.top.focus_force()
d.top.update()
d.top.focus_force()
d.top.event_generate('<Escape>')
d.top.update()
self.assertIsNone(d.how)
Expand All @@ -195,8 +195,10 @@ def test_type_ahead(self):
d.files.delete(0, 'end')
for name in ('alpha', 'bravo', 'charlie'):
d.files.insert('end', name)
d.files.focus_force()
d.top.update()
# Force the focus right before generating the event: the window
# manager can take it back.
d.files.focus_force()
d.files.event_generate('<Key>', keysym='c')
d.top.update()
sel = d.files.curselection()
Expand Down
26 changes: 20 additions & 6 deletions Lib/test/test_tkinter/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
requires_tk, get_tk_patchlevel,
tcl_version, tk_version)
tcl_version, tk_version,
wait_until_mapped)

support.requires('gui')

Expand Down Expand Up @@ -508,15 +509,20 @@ def test_focus_methods(self):
self.root.update_idletasks()
f.focus_force()
self.root.update()
self.assertIs(self.root.focus_get(), f)
self.assertIs(self.root.focus_displayof(), f)
# The window manager can take the focus away, and then focus_get()
# and focus_displayof() return None.
if self.root.focus_displayof() is not None:
self.assertIs(self.root.focus_get(), f)
self.assertIs(self.root.focus_displayof(), f)
self.assertIs(f.focus_lastfor(), f)
b = tkinter.Button(f)
b.pack()
self.root.update()
b.focus_set()
self.root.update()
self.assertIs(self.root.focus_get(), b)
if self.root.focus_displayof() is not None:
self.assertIs(self.root.focus_get(), b)
self.assertIs(f.focus_lastfor(), b)

def test_focus_methods_unresolvable(self):
# The focus may be on a widget that tkinter did not create and so
Expand Down Expand Up @@ -1319,9 +1325,15 @@ def test_wm_transient(self):
def test_wm_stackorder(self):
t1 = tkinter.Toplevel(self.root)
t2 = tkinter.Toplevel(self.root)
if self.root._windowingsystem == 'x11':
# Bypass the window manager, which may ignore lift() or reorder
# the windows while they are being mapped.
t1.overrideredirect(True)
t2.overrideredirect(True)
t1.deiconify()
t2.deiconify()
self.root.update()
wait_until_mapped(t1)
wait_until_mapped(t2)
t1.lift(t2) # Raise t1 above t2.
self.root.update()
order = self.root.wm_stackorder()
Expand Down Expand Up @@ -1361,7 +1373,9 @@ def test_focus(self):

f.focus_force()
self.root.update()
self.assertEqual(len(events), 1, events)
# The window manager can take the focus away and give it back,
# which makes Tk generate additional focus events.
self.assertGreaterEqual(len(events), 1, events)
e = events[0]
self.assertIs(e.type, tkinter.EventType.FocusIn)
self.assertIs(e.widget, f)
Expand Down
30 changes: 15 additions & 15 deletions Lib/test/test_tkinter/test_simpledialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from tkinter import messagebox, ttk
from test.support import requires, swap_attr
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import AbstractDefaultRootTest, AbstractTkTest
from test.test_tkinter.support import AbstractDefaultRootTest, AbstractDialogTest
from tkinter.simpledialog import (Dialog, SimpleDialog,
askinteger, askfloat, askstring,
_QueryInteger, _QueryFloat, _QueryString,
Expand All @@ -12,7 +12,7 @@
requires('gui')


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

Expand Down Expand Up @@ -45,8 +45,8 @@ def test_use_ttk(self):
ttk.Style(d.root).lookup('.', 'background'))
# The bindings work with the themed buttons too.
self.require_mapped(d.root)
d._buttons[0].focus_force()
d.root.update()
d._buttons[0].focus_force()
d.root.event_generate('<Return>')
d.root.update()
self.assertEqual(d.num, 0)
Expand Down Expand Up @@ -144,8 +144,8 @@ def test_alt_key(self):
# the matching button (cf. tk::AmpWidget in tk::MessageBox).
d = self.create(buttons=['Yes', {'text': 'No', 'underline': 0}])
self.require_mapped(d.root)
d._buttons[0].focus_force()
d.root.update()
d._buttons[0].focus_force()
d.root.event_generate('<Alt-n>') # "No" -> underline 0 -> "N"
d.root.update()
self.assertEqual(d.num, 1)
Expand All @@ -155,8 +155,8 @@ def test_return_invokes_focused_button(self):
# default and the focus was not moved by keyboard traversal.
d = self.create(buttons=['Yes', 'No']) # default 0
self.require_mapped(d.root)
d._buttons[1].focus_force()
d.root.update()
d._buttons[1].focus_force()
d.root.event_generate('<Return>')
d.root.update()
self.assertEqual(d.num, 1)
Expand All @@ -165,8 +165,8 @@ def test_focus_next_then_return(self):
# <Tab> moves the focus to the next button; <Return> invokes it.
d = self.create(buttons=['Yes', 'No'])
self.require_mapped(d.root)
d._buttons[0].focus_force()
d.root.update()
d._buttons[0].focus_force()
d._buttons[0].event_generate('<Tab>')
d.root.update()
d.root.event_generate('<Return>')
Expand All @@ -177,8 +177,8 @@ def test_focus_prev_then_return(self):
# <Shift-Tab> moves the focus to the previous button.
d = self.create(buttons=['Yes', 'No'])
self.require_mapped(d.root)
d._buttons[1].focus_force()
d.root.update()
d._buttons[1].focus_force()
d._buttons[1].event_generate('<Shift-Tab>')
d.root.update()
d.root.event_generate('<Return>')
Expand All @@ -189,8 +189,8 @@ def test_return_activates_default(self):
# <Return> with the focus off the buttons invokes the default button.
d = self.create() # default 0
self.require_mapped(d.root)
d.root.focus_force() # the dialog, not a button, has the focus
d.root.update()
d.root.focus_force() # the dialog, not a button, has the focus
d.root.event_generate('<Return>')
d.root.update()
self.assertEqual(d.num, 0)
Expand Down Expand Up @@ -246,7 +246,7 @@ def test_go(self):
self.assertEqual(d.go(), 0)


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

def open(self, **kw):
Expand Down Expand Up @@ -279,8 +279,8 @@ def test_use_classic(self):
invoked = []
cancel = d.children['cancel']
cancel.configure(command=lambda: invoked.append(True))
cancel.focus_force()
d.update()
cancel.focus_force()
d.event_generate('<Return>')
d.update()
self.assertTrue(invoked)
Expand Down Expand Up @@ -359,8 +359,8 @@ def test_alt_key(self):
invoked = []
cancel = d.children['cancel'] # "&Cancel"
cancel.configure(command=lambda: invoked.append(True))
d.focus_force()
d.update()
d.focus_force()
d.event_generate('<Alt-c>')
d.update()
self.assertTrue(invoked)
Expand All @@ -371,8 +371,8 @@ def test_return_invokes_focused_button(self):
invoked = []
cancel = d.children['cancel']
cancel.configure(command=lambda: invoked.append(True))
cancel.focus_force()
d.update()
cancel.focus_force()
d.event_generate('<Return>')
d.update()
self.assertEqual(invoked, [True])
Expand All @@ -384,8 +384,8 @@ def test_focus_next_then_return(self):
for name in ('ok', 'cancel'):
d.children[name].configure(command=lambda name=name: invoked.append(name))
ok = d.children['ok']
ok.focus_force()
d.update()
ok.focus_force()
ok.event_generate('<Tab>') # OK -> Cancel
d.update()
d.event_generate('<Return>')
Expand All @@ -399,8 +399,8 @@ def test_focus_prev_then_return(self):
for name in ('ok', 'cancel'):
d.children[name].configure(command=lambda name=name: invoked.append(name))
cancel = d.children['cancel']
cancel.focus_force()
d.update()
cancel.focus_force()
cancel.event_generate('<Shift-Tab>') # Cancel -> OK
d.update()
d.event_generate('<Return>')
Expand Down Expand Up @@ -432,7 +432,7 @@ def mock_wait_window(w):
self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")


class QueryDialogTest(AbstractTkTest, unittest.TestCase):
class QueryDialogTest(AbstractDialogTest, unittest.TestCase):
# The query dialogs are modal: their __init__ blocks in wait_window().
# Mock that out so the dialog stays alive and can be driven with generated
# events, exercising the <Return>/<Escape> bindings and the validation.
Expand Down
6 changes: 5 additions & 1 deletion Lib/test/test_ttk/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2024,19 +2024,23 @@ def test_virtual_events(self):
lambda e: selects.append(self.tv.selection()))
self.tv.bind('<<TreeviewOpen>>', lambda e: opens.append(self.tv.focus()))
self.tv.bind('<<TreeviewClose>>', lambda e: closes.append(self.tv.focus()))
self.tv.focus_force()
self.tv.focus(parent)
self.tv.selection_set(parent)
self.tv.update()

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

self.tv.focus_force()
self.tv.event_generate('<Left>') # Close it again.
self.tv.update()
self.assertEqual(closes, [parent])

self.tv.focus_force()
self.tv.event_generate('<Down>') # Move the selection.
self.tv.update()
self.assertEqual(self.tv.selection(), (item2,))
Expand Down
Loading