import functools
+import time
import tkinter
import unittest
from test import support
w.destroy()
self.root.withdraw()
+ def require_mapped(self, widget, timeout=None):
+ """Realize *widget*, or skip the test if the window manager will
+ not map it (e.g. a tiling WM or a headless/contended display).
+
+ Use this instead of a bare update() before querying realized
+ geometry (winfo_width(), identify(), coords(), place_info(), ...).
+ See gh-69134, gh-74941 and bpo-40722.
+ """
+ if timeout is None:
+ timeout = support.LOOPBACK_TIMEOUT
+ if not wait_until_mapped(widget, timeout):
+ self.skipTest('widget was not mapped by the window manager '
+ f'(timed out after {timeout:g}s)')
+
class AbstractDefaultRootTest:
tkinter._default_root.destroy()
tkinter._default_root = None
+def wait_until_mapped(widget, timeout=None):
+ """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.
+
+ Return True once the widget is mapped with a non-trivial size, or False
+ if that has not happened within *timeout* seconds (default:
+ ``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 timeout is None:
+ timeout = support.LOOPBACK_TIMEOUT
+ deadline = time.monotonic() + timeout
+ 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 time.monotonic() >= deadline:
+ return False
+ time.sleep(0.01)
+
+
def simulate_mouse_click(widget, x, y):
"""Generate proper events to click at the x, y position (tries to act
like an X server)."""
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import (requires_tk, tk_version,
get_tk_patchlevel, widget_eq,
+ wait_until_mapped,
AbstractDefaultRootTest)
from test.test_tkinter.widget_tests import (
def test_identify(self):
widget = self.create()
widget.pack()
- widget.update_idletasks()
- # The empty string is returned for a point over no element.
- self.assertIn(widget.identify(5, 5),
- ('entry', 'buttonup', 'buttondown', 'none', ''))
+ # Identifying the element under a point requires the widget to be
+ # mapped with a real size.
+ if wait_until_mapped(widget):
+ self.assertIn(widget.identify(5, 5),
+ ('entry', 'buttonup', 'buttondown', 'none'))
self.assertRaises(TclError, widget.identify, 'a', 'b')
def test_scan(self):
def test_identify(self):
sb = self.create()
sb.pack(fill='y', expand=True)
- sb.update_idletasks()
- self.assertIn(sb.identify(5, 5),
- ('arrow1', 'arrow2', 'slider', 'trough1', 'trough2', ''))
+ # Identifying the element under a point requires the widget to be
+ # mapped with a real size.
+ if wait_until_mapped(sb):
+ self.assertIn(sb.identify(5, 5),
+ ('arrow1', 'arrow2', 'slider', 'trough1', 'trough2'))
self.assertRaises(TclError, sb.identify, 'a', 'b')
p, b, c = self.create2()
p.configure(width=200, height=50)
p.pack()
- p.update()
- x, y = p.sash_coord(0)
- # A point over the sash reports the sash.
- self.assertIn('sash', p.identify(x + 1, y + 5))
+ # Locating the sash requires the widget to be mapped with a real
+ # size; the rest of the checks do not.
+ if wait_until_mapped(p):
+ x, y = p.sash_coord(0)
+ # A point over the sash reports the sash.
+ self.assertIn('sash', p.identify(x + 1, y + 5))
# A point over a pane reports nothing.
self.assertFalse(p.identify(2, 2))
self.assertRaises(TclError, p.identify, 'a', 'b')
def test_horizontal_range(self):
lscale = ttk.LabeledScale(self.root, from_=0, to=10)
lscale.pack()
- lscale.update()
+ self.require_mapped(lscale)
linfo_1 = lscale.label.place_info()
prev_xcoord = lscale.scale.coords()[0]
def test_variable_change(self):
x = ttk.LabeledScale(self.root)
x.pack()
- x.update()
+ self.require_mapped(x)
curr_xcoord = x.scale.coords()[0]
newval = x.value + 1
x = ttk.LabeledScale(self.root)
x.pack(expand=True, fill='both')
gc_collect() # For PyPy or other GCs.
- x.update()
+ self.require_mapped(x)
width, height = x.master.winfo_width(), x.master.winfo_height()
width_new, height_new = width * 2, height * 2
from test.test_tkinter.support import setUpModule # noqa: F401
from test.test_tkinter.support import (
AbstractTkTest, requires_tk, tk_version, get_tk_patchlevel,
- simulate_mouse_click, AbstractDefaultRootTest)
+ simulate_mouse_click, wait_until_mapped, AbstractDefaultRootTest)
from test.test_tkinter.widget_tests import (add_configure_tests,
AbstractWidgetTest, StandardOptionsTests, IntegerSizeTests, PixelSizeTests)
self.widget.pack()
def test_identify(self):
- self.widget.update()
- self.assertEqual(self.widget.identify(
- int(self.widget.winfo_width() / 2),
- int(self.widget.winfo_height() / 2)
- ), "label")
+ # Identifying the element under a point requires the widget to be
+ # mapped with a real size; the rest of the checks do not.
+ if wait_until_mapped(self.widget):
+ self.assertEqual(self.widget.identify(
+ int(self.widget.winfo_width() / 2),
+ int(self.widget.winfo_height() / 2)
+ ), "label")
self.assertEqual(self.widget.identify(-1, -1), "")
self.assertRaises(tkinter.TclError, self.widget.identify, None, 5)
self.skipTest('Test does not work on macOS Tk 9.')
# https://core.tcl-lang.org/tk/tktview/8b49e9cfa6
self.entry.pack()
- self.root.update()
- self.assertIn(self.entry.identify(5, 5), self.IDENTIFY_AS)
+ # Identifying the element under a point requires the widget to be
+ # mapped with a real size; the rest of the checks do not.
+ if wait_until_mapped(self.entry):
+ self.assertIn(self.entry.identify(5, 5), self.IDENTIFY_AS)
self.assertEqual(self.entry.identify(-1, -1), "")
self.assertRaises(tkinter.TclError, self.entry.identify, None, 5)
self.combo.bind('<<ComboboxSelected>>',
lambda evt: success.append(True))
self.combo.pack()
- self.combo.update()
+ self.require_mapped(self.combo)
height = self.combo.winfo_height()
self._show_drop_down_listbox()
self.combo['postcommand'] = lambda: success.append(True)
self.combo.pack()
- self.combo.update()
+ self.require_mapped(self.combo)
self._show_drop_down_listbox()
self.assertTrue(success)
else:
conv = float
- scale_width = self.scale.winfo_width()
- self.assertEqual(self.scale.get(scale_width, 0), self.scale['to'])
+ # Reading the value at the far edge needs the realized width.
+ if wait_until_mapped(self.scale):
+ scale_width = self.scale.winfo_width()
+ self.assertEqual(self.scale.get(scale_width, 0), self.scale['to'])
self.assertEqual(conv(self.scale.get(0, 0)), conv(self.scale['from']))
self.assertEqual(self.scale.get(), self.scale['value'])
# nevertheless, note that the max/min values we can get specifying
# x, y coords are the ones according to the current range
self.assertEqual(conv(self.scale.get(0, 0)), min)
- self.assertEqual(conv(self.scale.get(self.scale.winfo_width(), 0)), max)
+ # Reading the value at the far edge needs the realized width.
+ if wait_until_mapped(self.scale):
+ self.assertEqual(
+ conv(self.scale.get(self.scale.winfo_width(), 0)), max)
self.assertRaises(tkinter.TclError, self.scale.set, None)
return ttk.Spinbox(self.root, **kwargs)
def _click_increment_arrow(self):
+ self.require_mapped(self.spin)
width = self.spin.winfo_width()
height = self.spin.winfo_height()
x = width - 5
self.spin.update_idletasks()
def _click_decrement_arrow(self):
+ self.require_mapped(self.spin)
width = self.spin.winfo_width()
height = self.spin.winfo_height()
x = width - 5