Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions Lib/test/test_tkinter/support.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import functools
import time
import tkinter
import unittest
from test import support
Expand Down Expand Up @@ -45,6 +46,20 @@ def tearDown(self):
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:

Expand Down Expand Up @@ -78,6 +93,32 @@ def destroy_default_root():
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)."""
Expand Down
28 changes: 17 additions & 11 deletions Lib/test/test_tkinter/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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 (
Expand Down Expand Up @@ -754,10 +755,11 @@ def test_invoke(self):
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):
Expand Down Expand Up @@ -2096,9 +2098,11 @@ def test_delta(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')


Expand Down Expand Up @@ -2218,10 +2222,12 @@ def test_identify(self):
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')
Expand Down
6 changes: 3 additions & 3 deletions Lib/test/test_ttk/test_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def check_positions(scale, scale_pos, label, label_pos):
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]
Expand Down Expand Up @@ -138,7 +138,7 @@ def test_horizontal_range(self):
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
Expand Down Expand Up @@ -181,7 +181,7 @@ def test_resize(self):
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
Expand Down
37 changes: 24 additions & 13 deletions Lib/test/test_ttk/test_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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)

Expand Down Expand Up @@ -78,11 +78,13 @@ def setUp(self):
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)
Expand Down Expand Up @@ -385,9 +387,11 @@ def test_identify(self):
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)
Expand Down Expand Up @@ -506,7 +510,7 @@ def test_virtual_event(self):
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()
Expand All @@ -525,7 +529,7 @@ def test_configure_postcommand(self):

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)
Expand Down Expand Up @@ -875,8 +879,10 @@ def test_get(self):
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'])
Expand Down Expand Up @@ -918,7 +924,10 @@ def test_set(self):
# 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)

Expand Down Expand Up @@ -1269,6 +1278,7 @@ def create(self, **kwargs):
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
Expand All @@ -1279,6 +1289,7 @@ def _click_increment_arrow(self):
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
Expand Down
Loading