Skip to content

Commit 0f3b424

Browse files
gh-153716: Harden ttk/tkinter GUI tests against display scaling
Compute probe coordinates from the widget's own realized geometry instead of hardcoding pixels such as (5, 5), which land on a different element, or miss it entirely, at a high display scaling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1fece44 commit 0f3b424

2 files changed

Lines changed: 70 additions & 40 deletions

File tree

Lib/test/test_tkinter/test_widgets.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2082,9 +2082,13 @@ def test_configure_to(self):
20822082
def test_identify(self):
20832083
widget = self.create()
20842084
widget.pack()
2085-
widget.update_idletasks()
2086-
self.assertIn(widget.identify(5, 5),
2087-
('slider', 'trough1', 'trough2', ''))
2085+
# Probe a point on the trough centreline (Scale.coords()) rather than
2086+
# a fixed pixel: (5, 5) lies outside the trough and always identifies
2087+
# as '', so it would not actually exercise identify().
2088+
if wait_until_mapped(widget):
2089+
x, y = widget.coords()
2090+
self.assertIn(widget.identify(int(x), int(y)),
2091+
('slider', 'trough1', 'trough2'))
20882092
self.assertRaises(TclError, widget.identify, 'a', 'b')
20892093

20902094

Lib/test/test_ttk/test_widgets.py

Lines changed: 63 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,12 @@ def test_identify(self):
385385
# Identifying the element under a point requires the widget to be
386386
# mapped with a real size; the rest of the checks do not.
387387
if wait_until_mapped(self.entry):
388-
self.assertIn(self.entry.identify(5, 5), self.IDENTIFY_AS)
388+
# Probe the centre of the widget: a fixed pixel such as (5, 5)
389+
# lands in the field at a normal scaling but in the surrounding
390+
# padding at a high DPI.
391+
x = self.entry.winfo_width() // 2
392+
y = self.entry.winfo_height() // 2
393+
self.assertIn(self.entry.identify(x, y), self.IDENTIFY_AS)
389394
self.assertEqual(self.entry.identify(-1, -1), "")
390395

391396
self.assertRaises(tkinter.TclError, self.entry.identify, None, 5)
@@ -1062,21 +1067,15 @@ def test_tab_identifiers(self):
10621067

10631068
self.nb.pack()
10641069
self.nb.update()
1065-
if sys.platform == 'darwin':
1066-
tb_idx = "@20,5"
1067-
else:
1068-
tb_idx = "@5,5"
1069-
self.assertEqual(self.nb.tab(tb_idx), self.nb.tab('current'))
1070+
# Address a tab by a point taken from Tk's own geometry rather than a
1071+
# fixed pixel, which can land on a different tab or miss the tabs
1072+
# entirely (e.g. on Aqua the tabs are inset) as the display scaling
1073+
# changes.
1074+
x, y = self._tab_point(self.nb.index('current'))
1075+
self.assertEqual(self.nb.tab(f'@{x},{y}'), self.nb.tab('current'))
10701076

1071-
for i in range(5, 100, 5):
1072-
try:
1073-
if self.nb.tab('@%d, 5' % i, text=None) == 'a':
1074-
break
1075-
except tkinter.TclError:
1076-
pass
1077-
1078-
else:
1079-
self.fail("Tab with text 'a' not found")
1077+
x, y = self._tab_point(self.nb.index(self.child1))
1078+
self.assertEqual(self.nb.tab(f'@{x},{y}', text=None), 'a')
10801079

10811080
def test_add_and_hidden(self):
10821081
self.assertRaises(tkinter.TclError, self.nb.hide, -1)
@@ -1206,23 +1205,43 @@ def test_configure_tabs(self):
12061205

12071206
self.assertEqual(self.nb.tabs(), ())
12081207

1208+
def _tab_point(self, index):
1209+
# Return a window coordinate that really lies on tab *index*, taken
1210+
# from Tk's own geometry (ttk::notebook index @x,y), so that it is
1211+
# correct regardless of the theme, platform and display scaling. A
1212+
# fixed pixel such as (5, 5) can land on a different tab element
1213+
# depending on the DPI, or miss the tabs entirely (e.g. on Aqua the
1214+
# tabs are inset), which would make a click there test nothing.
1215+
self.assertTrue(wait_until_mapped(self.nb, full_size=True))
1216+
w, h = self.nb.winfo_width(), self.nb.winfo_height()
1217+
# The selected pane fills the widget except for the tab strip, so skip
1218+
# that large tab-free region instead of querying every pixel in it.
1219+
pane = self.nb.nametowidget(self.nb.select())
1220+
px, py = pane.winfo_x(), pane.winfo_y()
1221+
pw, ph = pane.winfo_width(), pane.winfo_height()
1222+
for y in range(h):
1223+
inside_y = py <= y < py + ph
1224+
for x in range(w):
1225+
if inside_y and px <= x < px + pw:
1226+
continue
1227+
try:
1228+
if self.nb.index(f'@{x},{y}') == index:
1229+
return x, y
1230+
except TclError:
1231+
continue
1232+
self.fail(f'no point found on tab {index}')
1233+
12091234
def test_traversal(self):
12101235
self.nb.pack()
12111236
self.nb.update()
12121237

1213-
self.nb.select(0)
1238+
# A mouse click selects the tab it lands on.
1239+
self.nb.select(1)
1240+
self.assertEqual(self.nb.select(), str(self.child2))
1241+
simulate_mouse_click(self.nb, *self._tab_point(0))
1242+
self.assertEqual(self.nb.select(), str(self.child1))
12141243

1215-
if sys.platform == 'darwin':
1216-
focus_identify_as = ''
1217-
elif sys.platform == 'win32':
1218-
focus_identify_as = 'focus'
1219-
else:
1220-
focus_identify_as = 'focus' if tk_version < (8, 7) else 'padding'
1221-
# identify() at (5, 5) needs the tab realized there; under focus
1222-
# contention the mapped size can lag, so wait for the full size.
1223-
if wait_until_mapped(self.nb, full_size=True):
1224-
self.assertEqual(self.nb.identify(5, 5), focus_identify_as)
1225-
simulate_mouse_click(self.nb, 5, 5)
1244+
# Control-Tab and Shift-Control-Tab traverse the tabs.
12261245
self.nb.focus_force()
12271246
self.nb.event_generate('<Control-Tab>')
12281247
self.assertEqual(self.nb.select(), str(self.child2))
@@ -1237,21 +1256,25 @@ def test_traversal(self):
12371256
self.nb.tab(self.child2, text='e', underline=0)
12381257
self.nb.enable_traversal()
12391258
self.nb.focus_force()
1240-
if wait_until_mapped(self.nb, full_size=True):
1241-
self.assertEqual(self.nb.identify(5, 5), focus_identify_as)
1242-
simulate_mouse_click(self.nb, 5, 5)
1243-
# on macOS Emacs-style keyboard shortcuts are region-dependent;
1244-
# let's use the regular arrow keys instead
1259+
1260+
# A click still selects the tab it lands on after enable_traversal().
1261+
self.nb.select(1)
1262+
self.assertEqual(self.nb.select(), str(self.child2))
1263+
simulate_mouse_click(self.nb, *self._tab_point(0))
1264+
self.assertEqual(self.nb.select(), str(self.child1))
1265+
1266+
# Mnemonics traverse the tabs (macOS uses region-dependent
1267+
# Emacs-style shortcuts, so use the regular arrow keys there).
12451268
if sys.platform == 'darwin':
12461269
begin = '<Left>'
12471270
end = '<Right>'
12481271
else:
12491272
begin = '<Alt-a>'
12501273
end = '<Alt-e>'
1251-
self.nb.event_generate(begin)
1252-
self.assertEqual(self.nb.select(), str(self.child1))
12531274
self.nb.event_generate(end)
12541275
self.assertEqual(self.nb.select(), str(self.child2))
1276+
self.nb.event_generate(begin)
1277+
self.assertEqual(self.nb.select(), str(self.child1))
12551278

12561279

12571280
@add_configure_tests(IntegerSizeTests, StandardTtkOptionsTests)
@@ -2062,14 +2085,17 @@ def test_tag_bind(self):
20622085
self.tv.pack()
20632086
self.tv.update()
20642087

2088+
# Find the y coordinate of each item. Scan the whole height of the
2089+
# widget rather than a fixed pixel range, since the row height grows
2090+
# with the display scaling.
20652091
pos_y = set()
20662092
found = set()
2067-
for i in range(0, 100, 10):
2093+
for y in range(self.tv.winfo_height()):
20682094
if len(found) == 2: # item1 and item2 already found
20692095
break
2070-
item_id = self.tv.identify_row(i)
2096+
item_id = self.tv.identify_row(y)
20712097
if item_id and item_id not in found:
2072-
pos_y.add(i)
2098+
pos_y.add(y)
20732099
found.add(item_id)
20742100

20752101
self.assertEqual(len(pos_y), 2) # item1 and item2 y pos

0 commit comments

Comments
 (0)