From 9dbcb31d9895b99176f3e472254a0f3a0bec40d0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 9 Jul 2026 12:54:13 +0300 Subject: [PATCH] [3.13] gh-69134: Harden NotebookTest.test_traversal (GH-153403) identify(5, 5) could run before the notebook reached its requested size, so the pixel fell outside the first tab and returned ''. Guard it with a new opt-in wait_until_mapped(full_size=True). (cherry picked from commit 53b04980284b668c27fc6d5adb0de19d38efe95b) Co-authored-by: Serhiy Storchaka Co-authored-by: Claude Opus 4.8 --- Lib/test/test_tkinter/support.py | 19 ++++++++++++++----- Lib/test/test_ttk/test_widgets.py | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_tkinter/support.py b/Lib/test/test_tkinter/support.py index 84c83306e57ebae..ea2f9012a705498 100644 --- a/Lib/test/test_tkinter/support.py +++ b/Lib/test/test_tkinter/support.py @@ -93,7 +93,7 @@ def destroy_default_root(): tkinter._default_root.destroy() tkinter._default_root = None -def wait_until_mapped(widget, timeout=None): +def wait_until_mapped(widget, timeout=None, *, full_size=False): """Wait until *widget* is actually mapped and laid out by the window manager, so that realized-geometry queries (winfo_width(), identify(), coords(), ...) return meaningful values. @@ -103,6 +103,10 @@ def wait_until_mapped(widget, timeout=None): ``support.LOOPBACK_TIMEOUT``). Unlike Misc.wait_visibility(), this never blocks indefinitely, so it is safe under a window manager that never maps the window (see gh-69134, gh-74941, bpo-40722). + + If *full_size* is true, also wait until the realized size reaches the + requested size, so that per-pixel queries near an edge (e.g. identify()) + are reliable even under load. """ if timeout is None: timeout = support.LOOPBACK_TIMEOUT @@ -110,10 +114,15 @@ def wait_until_mapped(widget, timeout=None): widget.update_idletasks() while True: widget.update() # drain pending Map/Configure events - if (widget.winfo_ismapped() - and widget.winfo_width() > 1 - and widget.winfo_height() > 1): - return True + if widget.winfo_ismapped(): + if full_size: + w_ok = widget.winfo_width() >= widget.winfo_reqwidth() > 1 + h_ok = widget.winfo_height() >= widget.winfo_reqheight() > 1 + else: + w_ok = widget.winfo_width() > 1 + h_ok = widget.winfo_height() > 1 + if w_ok and h_ok: + return True if time.monotonic() >= deadline: return False time.sleep(0.01) diff --git a/Lib/test/test_ttk/test_widgets.py b/Lib/test/test_ttk/test_widgets.py index 5054ad8b9adf3fc..f134f88aedf6cbc 100644 --- a/Lib/test/test_ttk/test_widgets.py +++ b/Lib/test/test_ttk/test_widgets.py @@ -1192,8 +1192,16 @@ def test_traversal(self): self.nb.select(0) - focus_identify_as = 'focus' if sys.platform != 'darwin' else '' - self.assertEqual(self.nb.identify(5, 5), focus_identify_as) + if sys.platform == 'darwin': + focus_identify_as = '' + elif sys.platform == 'win32': + focus_identify_as = 'focus' + else: + focus_identify_as = 'focus' if tk_version < (8, 7) else 'padding' + # identify() at (5, 5) needs the tab realized there; under focus + # contention the mapped size can lag, so wait for the full size. + if wait_until_mapped(self.nb, full_size=True): + self.assertEqual(self.nb.identify(5, 5), focus_identify_as) simulate_mouse_click(self.nb, 5, 5) self.nb.focus_force() self.nb.event_generate('') @@ -1209,7 +1217,8 @@ def test_traversal(self): self.nb.tab(self.child2, text='e', underline=0) self.nb.enable_traversal() self.nb.focus_force() - self.assertEqual(self.nb.identify(5, 5), focus_identify_as) + if wait_until_mapped(self.nb, full_size=True): + self.assertEqual(self.nb.identify(5, 5), focus_identify_as) simulate_mouse_click(self.nb, 5, 5) # on macOS Emacs-style keyboard shortcuts are region-dependent; # let's use the regular arrow keys instead