Skip to content

Commit a23a0b4

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.14] gh-151678: Add tests for tkinter.dnd (GH-151780) (GH-151791)
Drive the drag-and-drop protocol (dnd_start and the DndHandler enter/ motion/commit, leave/cancel and end callbacks). (cherry picked from commit 2a126a5) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2f97fdb commit a23a0b4

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)