From e4bd5e112821229b13e66a2da0c10757ccad05e2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 27 Jun 2026 10:09:07 +0300 Subject: [PATCH] gh-151678: Add more tests for tkinter.dnd (GH-152362) Cover the drag cursor, the Motion and ButtonRelease bindings, switching between targets, the target search up the master chain, dnd_accept() returning None, and restarting after a drag has finished. (cherry picked from commit 389e00f13fb5b20b424980b08678753441db7529) Co-authored-by: Serhiy Storchaka Co-authored-by: Claude Opus 4.8 --- Lib/test/test_tkinter/test_dnd.py | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Lib/test/test_tkinter/test_dnd.py b/Lib/test/test_tkinter/test_dnd.py index 501b0d7f78586c5..fe3685aefe48be9 100644 --- a/Lib/test/test_tkinter/test_dnd.py +++ b/Lib/test/test_tkinter/test_dnd.py @@ -60,6 +60,15 @@ def setUp(self): self.source = Source(self.log) self.target = Target(self.canvas, self.log) + def tearDown(self): + # Make sure no drag-and-drop is left active between tests: the + # recursion guard is a name-mangled attribute on the root. + try: + del self.root._DndHandler__dnd + except AttributeError: + pass + super().tearDown() + def test_drag_and_drop(self): handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) self.assertIsNotNone(handler) @@ -93,6 +102,70 @@ def test_no_recursive_start(self): def test_high_button_number_ignored(self): self.assertIsNone(dnd.dnd_start(self.source, FakeEvent(self.canvas, num=6))) + def test_restart_after_finish(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.cancel() + # Once a drag has finished a new one can start. + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIsNotNone(handler) + handler.cancel() + + def test_drag_cursor(self): + self.canvas['cursor'] = 'watch' + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + # The drag cursor is shown while dragging, the original restored after. + self.assertEqual(handler.save_cursor, 'watch') + self.assertEqual(str(self.canvas['cursor']), 'hand2') + handler.cancel() + self.assertEqual(str(self.canvas['cursor']), 'watch') + + def test_bindings_added_and_removed(self): + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.assertIn('', self.canvas.bind()) + self.assertIn('', self.canvas.bind()) + handler.cancel() + self.assertNotIn('', self.canvas.bind()) + self.assertNotIn('', self.canvas.bind()) + + def test_switch_target(self): + log1, log2 = [], [] + w1, w2 = tkinter.Frame(self.root), tkinter.Frame(self.root) + target1, target2 = Target(w1, log1), Target(w2, log2) + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + self.canvas.winfo_containing = lambda x, y: w1 + handler.on_motion(FakeEvent(self.canvas)) # Enter target1. + self.canvas.winfo_containing = lambda x, y: w2 + handler.on_motion(FakeEvent(self.canvas)) # Leave target1, enter target2. + self.assertIs(handler.target, target2) + self.assertEqual(log1, ['accept', 'enter', 'leave']) + self.assertEqual(log2, ['accept', 'enter']) + handler.cancel() + + def test_target_in_ancestor(self): + # The widget under the pointer has no dnd_accept, but an ancestor + # does: the search walks up the master chain to find it. + parent = tkinter.Frame(self.root) + target = Target(parent, self.log) + child = tkinter.Frame(parent) + self.canvas.winfo_containing = lambda x, y: child + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) + self.assertIs(handler.target, target) + self.assertEqual(self.log, ['accept', 'enter']) + handler.cancel() + + def test_accept_returning_none_continues(self): + # dnd_accept() returning None means "not me, keep looking up". + parent = tkinter.Frame(self.root) + target = Target(parent, self.log) + child = tkinter.Frame(parent) + child.dnd_accept = lambda source, event: None + self.canvas.winfo_containing = lambda x, y: child + handler = dnd.dnd_start(self.source, FakeEvent(self.canvas)) + handler.on_motion(FakeEvent(self.canvas)) + self.assertIs(handler.target, target) + handler.cancel() + if __name__ == "__main__": unittest.main()