From 4f07d33762847993e6ebd5ffc8a20449365bf41b Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 20 Jun 2026 12:02:41 +0300 Subject: [PATCH] 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 --- Lib/test/test_tkinter/test_dnd.py | 98 +++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 Lib/test/test_tkinter/test_dnd.py diff --git a/Lib/test/test_tkinter/test_dnd.py b/Lib/test/test_tkinter/test_dnd.py new file mode 100644 index 00000000000000..501b0d7f78586c --- /dev/null +++ b/Lib/test/test_tkinter/test_dnd.py @@ -0,0 +1,98 @@ +import unittest +import tkinter +from tkinter import dnd +from test.support import requires +from test.test_tkinter.support import setUpModule # noqa: F401 +from test.test_tkinter.support import AbstractTkTest + +requires('gui') + + +class Target: + def __init__(self, widget, log): + self.widget = widget + self.log = log + widget.dnd_accept = self.dnd_accept + + def dnd_accept(self, source, event): + self.log.append('accept') + return self + + def dnd_enter(self, source, event): + self.log.append('enter') + + def dnd_motion(self, source, event): + self.log.append('motion') + + def dnd_leave(self, source, event): + self.log.append('leave') + + def dnd_commit(self, source, event): + self.log.append('commit') + + +class Source: + def __init__(self, log): + self.log = log + + def dnd_end(self, target, event): + self.log.append('end') + + +class FakeEvent: + def __init__(self, widget, num=1): + self.num = num + self.widget = widget + self.x = self.y = self.x_root = self.y_root = 0 + + +class DndTest(AbstractTkTest, unittest.TestCase): + + def setUp(self): + super().setUp() + self.canvas = tkinter.Canvas(self.root) + self.canvas.pack() + # on_motion() locates the target with winfo_containing(). Bypass that + # real screen lookup, which depends on the window being visible and + # unobscured, so the test does not hinge on the window manager. + self.canvas.winfo_containing = lambda x, y: self.canvas + self.log = [] + self.source = Source(self.log) + self.target = Target(self.canvas, self.log) + + def test_drag_and_drop(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIsNotNone(handler) + handler.on_motion(FakeEvent(self.canvas)) # Enter the target. + handler.on_motion(FakeEvent(self.canvas)) # Move within the target. + handler.on_release(FakeEvent(self.canvas)) # Drop on the target. + self.assertEqual(self.log, + ['accept', 'enter', 'accept', 'motion', 'commit', 'end']) + + def test_cancel(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) # Enter the target. + handler.cancel() # Leaves the target without committing. + self.assertEqual(self.log, ['accept', 'enter', 'leave', 'end']) + + def test_no_target(self): + # Nothing under the pointer accepts the drag. + self.canvas.winfo_containing = lambda x, y: None + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) + handler.on_release(FakeEvent(self.canvas)) + self.assertEqual(self.log, ['end']) + + def test_no_recursive_start(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIsNotNone(handler) + # A drag is already in progress, so a second start is ignored. + self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas))) + handler.cancel() + + def test_high_button_number_ignored(self): + self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas, num=6))) + + +if __name__ == "__main__": + unittest.main()