From bf749089ebda53eb440efeaa400be989df8f9f4c Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 21 Jul 2026 17:54:27 +0300 Subject: [PATCH 1/2] gh-154357: Hide the root window in the tkinter dialog tests Tk delivers generated keyboard events to the window which has the focus. While the root window is visible, the window manager can take the focus back from the dialog, and the events are dropped. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_tkinter/support.py | 10 ++++++++++ Lib/test/test_tkinter/test_filedialog.py | 4 ++-- Lib/test/test_tkinter/test_simpledialog.py | 8 ++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_tkinter/support.py b/Lib/test/test_tkinter/support.py index 31feee2b8a40ee6..233a8f96ec46454 100644 --- a/Lib/test/test_tkinter/support.py +++ b/Lib/test/test_tkinter/support.py @@ -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 window which has the + # focus. 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): diff --git a/Lib/test/test_tkinter/test_filedialog.py b/Lib/test/test_tkinter/test_filedialog.py index f65ed19895a1ed4..c62b8c150e132d4 100644 --- a/Lib/test/test_tkinter/test_filedialog.py +++ b/Lib/test/test_tkinter/test_filedialog.py @@ -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') @@ -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. diff --git a/Lib/test/test_tkinter/test_simpledialog.py b/Lib/test/test_tkinter/test_simpledialog.py index be0be8d3f546311..10d0cce057ccf31 100644 --- a/Lib/test/test_tkinter/test_simpledialog.py +++ b/Lib/test/test_tkinter/test_simpledialog.py @@ -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, @@ -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. @@ -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): @@ -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 / bindings and the validation. From 5f7b0362daf74d0bf1052d8373b84c0dc58a8980 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 21 Jul 2026 18:56:03 +0300 Subject: [PATCH 2/2] gh-154357: Fix more flaky tkinter and ttk tests The window manager can reorder the toplevel windows while they are still being mapped, and can take the focus back while events are processed. Wait until the windows are mapped before changing the stacking order, and force the focus right before generating a keyboard event. Co-Authored-By: Claude Opus 4.8 (1M context) --- Lib/test/test_tkinter/test_misc.py | 6 ++++-- Lib/test/test_ttk/test_widgets.py | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_tkinter/test_misc.py b/Lib/test/test_tkinter/test_misc.py index a225079dd99091b..b95b33943c731e7 100644 --- a/Lib/test/test_tkinter/test_misc.py +++ b/Lib/test/test_tkinter/test_misc.py @@ -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') @@ -1321,7 +1322,8 @@ def test_wm_stackorder(self): t2 = tkinter.Toplevel(self.root) 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() diff --git a/Lib/test/test_ttk/test_widgets.py b/Lib/test/test_ttk/test_widgets.py index 4fa6a032e97c5f8..9fcde4d87c0b109 100644 --- a/Lib/test/test_ttk/test_widgets.py +++ b/Lib/test/test_ttk/test_widgets.py @@ -2024,19 +2024,23 @@ def test_virtual_events(self): lambda e: selects.append(self.tv.selection())) self.tv.bind('<>', lambda e: opens.append(self.tv.focus())) self.tv.bind('<>', 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 while the events are processed. + self.tv.focus_force() self.tv.event_generate('') # Open the focused parent. self.tv.update() self.assertEqual(opens, [parent]) + self.tv.focus_force() self.tv.event_generate('') # Close it again. self.tv.update() self.assertEqual(closes, [parent]) + self.tv.focus_force() self.tv.event_generate('') # Move the selection. self.tv.update() self.assertEqual(self.tv.selection(), (item2,))