[3.15] gh-151678: Add more tests for tkinter.dnd (GH-152362) (GH-152367)

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 389e00f13f)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Miss Islington (bot) 2026-06-27 09:41:21 +02:00 committed by GitHub
parent 029016654e
commit fbf61fbd4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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('<Motion>', self.canvas.bind())
self.assertIn('<B1-ButtonRelease-1>', self.canvas.bind())
handler.cancel()
self.assertNotIn('<Motion>', self.canvas.bind())
self.assertNotIn('<B1-ButtonRelease-1>', 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()