Skip to content

Commit 4f07d33

Browse files
gh-151678: Add tests for tkinter.dnd
Drive the drag-and-drop protocol (dnd_start and the DndHandler enter/ motion/commit, leave/cancel and end callbacks). winfo_containing(), the thin Tk wrapper used to locate the target, is stubbed so the tests exercise only the DndHandler dispatch logic and do not depend on the window being visible and unobscured. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent aa5b164 commit 4f07d33

1 file changed

Lines changed: 98 additions & 0 deletions

File tree

Lib/test/test_tkinter/test_dnd.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import unittest
2+
import tkinter
3+
from tkinter import dnd
4+
from test.support import requires
5+
from test.test_tkinter.support import setUpModule # noqa: F401
6+
from test.test_tkinter.support import AbstractTkTest
7+
8+
requires('gui')
9+
10+
11+
class Target:
12+
def __init__(self, widget, log):
13+
self.widget = widget
14+
self.log = log
15+
widget.dnd_accept = self.dnd_accept
16+
17+
def dnd_accept(self, source, event):
18+
self.log.append('accept')
19+
return self
20+
21+
def dnd_enter(self, source, event):
22+
self.log.append('enter')
23+
24+
def dnd_motion(self, source, event):
25+
self.log.append('motion')
26+
27+
def dnd_leave(self, source, event):
28+
self.log.append('leave')
29+
30+
def dnd_commit(self, source, event):
31+
self.log.append('commit')
32+
33+
34+
class Source:
35+
def __init__(self, log):
36+
self.log = log
37+
38+
def dnd_end(self, target, event):
39+
self.log.append('end')
40+
41+
42+
class FakeEvent:
43+
def __init__(self, widget, num=1):
44+
self.num = num
45+
self.widget = widget
46+
self.x = self.y = self.x_root = self.y_root = 0
47+
48+
49+
class DndTest(AbstractTkTest, unittest.TestCase):
50+
51+
def setUp(self):
52+
super().setUp()
53+
self.canvas = tkinter.Canvas(self.root)
54+
self.canvas.pack()
55+
# on_motion() locates the target with winfo_containing(). Bypass that
56+
# real screen lookup, which depends on the window being visible and
57+
# unobscured, so the test does not hinge on the window manager.
58+
self.canvas.winfo_containing = lambda x, y: self.canvas
59+
self.log = []
60+
self.source = Source(self.log)
61+
self.target = Target(self.canvas, self.log)
62+
63+
def test_drag_and_drop(self):
64+
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
65+
self.assertIsNotNone(handler)
66+
handler.on_motion(FakeEvent(self.canvas)) # Enter the target.
67+
handler.on_motion(FakeEvent(self.canvas)) # Move within the target.
68+
handler.on_release(FakeEvent(self.canvas)) # Drop on the target.
69+
self.assertEqual(self.log,
70+
['accept', 'enter', 'accept', 'motion', 'commit', 'end'])
71+
72+
def test_cancel(self):
73+
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
74+
handler.on_motion(FakeEvent(self.canvas)) # Enter the target.
75+
handler.cancel() # Leaves the target without committing.
76+
self.assertEqual(self.log, ['accept', 'enter', 'leave', 'end'])
77+
78+
def test_no_target(self):
79+
# Nothing under the pointer accepts the drag.
80+
self.canvas.winfo_containing = lambda x, y: None
81+
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
82+
handler.on_motion(FakeEvent(self.canvas))
83+
handler.on_release(FakeEvent(self.canvas))
84+
self.assertEqual(self.log, ['end'])
85+
86+
def test_no_recursive_start(self):
87+
handler = dnd.dnd_start(self.source, FakeEvent(self.canvas))
88+
self.assertIsNotNone(handler)
89+
# A drag is already in progress, so a second start is ignored.
90+
self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas)))
91+
handler.cancel()
92+
93+
def test_high_button_number_ignored(self):
94+
self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas, num=6)))
95+
96+
97+
if __name__ == "__main__":
98+
unittest.main()

0 commit comments

Comments
 (0)