]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151678: Add interactive tests for tkinter.simpledialog (GH-151794)
authorSerhiy Storchaka <storchaka@gmail.com>
Sat, 20 Jun 2026 13:46:39 +0000 (16:46 +0300)
committerGitHub <noreply@github.com>
Sat, 20 Jun 2026 13:46:39 +0000 (13:46 +0000)
Drive the modal query dialogs with generated events to exercise the
<Return> and <Escape> key bindings and the value validation: accepting
an integer, float or string, cancelling, rejecting a non-numeric value
and rejecting a value outside the allowed range.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Lib/test/test_tkinter/test_simpledialog.py

index 313ad82e0a2c0d9792a4b97f245c8d18eb1eb28c..6cf57fde8d4c56fc07b8ae003e2a63a74a3ffe3f 100644 (file)
@@ -1,9 +1,11 @@
 import unittest
 import tkinter
+from tkinter import messagebox
 from test.support import requires, swap_attr
 from test.test_tkinter.support import setUpModule  # noqa: F401
-from test.test_tkinter.support import AbstractDefaultRootTest
-from tkinter.simpledialog import Dialog, askinteger
+from test.test_tkinter.support import AbstractDefaultRootTest, AbstractTkTest
+from tkinter.simpledialog import (Dialog, askinteger,
+                                  _QueryInteger, _QueryFloat, _QueryString)
 
 requires('gui')
 
@@ -32,5 +34,65 @@ class DefaultRootTest(AbstractDefaultRootTest, unittest.TestCase):
             self.assertRaises(RuntimeError, askinteger, "Go To Line", "Line number")
 
 
+class QueryDialogTest(AbstractTkTest, unittest.TestCase):
+    # The query dialogs are modal: their __init__ blocks in wait_window().
+    # Mock that out so the dialog stays alive and can be driven with generated
+    # events, exercising the <Return>/<Escape> bindings and the validation.
+
+    def open(self, query, **kw):
+        with swap_attr(Dialog, 'wait_window', staticmethod(lambda w: None)):
+            d = query("Title", "Prompt", parent=self.root, **kw)
+        self.addCleanup(lambda: d.winfo_exists() and d.destroy())
+        d.focus_force()
+        d.update()
+        return d
+
+    def enter(self, d, value, key='<Return>'):
+        d.entry.delete(0, 'end')
+        d.entry.insert(0, value)
+        d.event_generate(key)
+        d.update()
+
+    def test_return_accepts(self):
+        for query, value, expected in [
+            (_QueryInteger, '42', 42),
+            (_QueryFloat, '1.5', 1.5),
+            (_QueryString, 'spam', 'spam'),
+        ]:
+            with self.subTest(query=query.__name__):
+                d = self.open(query)
+                self.enter(d, value)
+                self.assertEqual(d.result, expected)
+                self.assertFalse(d.winfo_exists())  # The dialog closed.
+
+    def test_escape_cancels(self):
+        d = self.open(_QueryString)
+        self.enter(d, 'spam', '<Escape>')
+        self.assertIsNone(d.result)
+        self.assertFalse(d.winfo_exists())
+
+    def test_invalid_value(self):
+        warnings = []
+        d = self.open(_QueryInteger)
+        with swap_attr(messagebox, 'showwarning',
+                       lambda *a, **k: warnings.append(a)):
+            self.enter(d, 'not a number')
+        self.assertIsNone(d.result)
+        self.assertTrue(d.winfo_exists())  # The dialog stays open.
+        self.assertTrue(warnings)
+
+    def test_out_of_range(self):
+        warnings = []
+        d = self.open(_QueryInteger, minvalue=10, maxvalue=20)
+        with swap_attr(messagebox, 'showwarning',
+                       lambda *a, **k: warnings.append(a)):
+            self.enter(d, '5')   # Below the minimum.
+            self.assertIsNone(d.result)
+            self.enter(d, '25')  # Above the maximum.
+            self.assertIsNone(d.result)
+        self.assertTrue(d.winfo_exists())
+        self.assertEqual(len(warnings), 2)
+
+
 if __name__ == "__main__":
     unittest.main()