]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-151678: Add tests for tkinter.Menu (GH-151685)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 19 Jun 2026 08:59:14 +0000 (11:59 +0300)
committerGitHub <noreply@github.com>
Fri, 19 Jun 2026 08:59:14 +0000 (11:59 +0300)
Cover previously-untested Menu methods in MenuTest: adding, inserting and
deleting items of every type, index resolution, invoking items, entry x/y
positions, and post/unpost/tk_popup mapping.

Also test per-entry configuration options and the errors raised for
invalid indices, entry types, option names and option values.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lib/test/test_tkinter/test_widgets.py

index 1c400e970eb02da19a43f1bb0fae5f8d0e59bfc5..fd3c70c97c3d5b270aa75a05919e1f99a3a26550 100644 (file)
@@ -1542,6 +1542,192 @@ class MenuTest(AbstractWidgetTest, unittest.TestCase):
         m1.entryconfigure(1, variable=v2)
         self.assertEqual(str(m1.entrycget(1, 'variable')), str(v2))
 
+    def test_add(self):
+        m = self.create(tearoff=False)
+        m.add_command(label='Command')
+        m.add_checkbutton(label='Checkbutton')
+        m.add_radiobutton(label='Radiobutton')
+        m.add_separator()
+        m.add_cascade(label='Cascade', menu=tkinter.Menu(m, tearoff=False))
+        self.assertEqual(m.index('end'), 4)
+        self.assertEqual([m.type(i) for i in range(5)],
+                         ['command', 'checkbutton', 'radiobutton',
+                          'separator', 'cascade'])
+        self.assertEqual(m.entrycget(0, 'label'), 'Command')
+        self.assertRaisesRegex(TclError, 'bad menu entry type "spam"',
+                               m.add, 'spam')
+
+    def test_insert(self):
+        m = self.create(tearoff=False)
+        m.add_command(label='A')
+        m.add_command(label='C')
+        m.insert_command(1, label='B')
+        m.insert_separator(0)
+        m.insert_checkbutton('end', label='D')
+        m.insert_radiobutton(0, label='top')
+        m.insert_cascade(2, label='sub',
+                         menu=tkinter.Menu(m, tearoff=False))
+        self.assertEqual(
+            [m.type(i) for i in range(m.index('end') + 1)],
+            ['radiobutton', 'separator', 'cascade', 'command',
+             'command', 'command', 'checkbutton'])
+        self.assertEqual(
+            [m.entrycget(i, 'label') for i in (3, 4, 5)],
+            ['A', 'B', 'C'])
+        self.assertRaisesRegex(TclError, 'bad menu entry type "spam"',
+                               m.insert, 0, 'spam')
+        self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
+                               m.insert_command, 'spam', label='z')
+
+    def test_delete(self):
+        m = self.create(tearoff=False)
+        commands = []
+        for label in 'ABCDE':
+            m.add_command(label=label,
+                          command=lambda label=label: commands.append(label))
+        # The Tcl command for a deleted item is cleaned up.
+        funcid = str(m.entrycget(2, 'command'))
+        self.assertEqual(
+            m.tk.splitlist(m.tk.call('info', 'commands', funcid)), (funcid,))
+
+        m.delete(2)  # Delete a single item ('C').
+        self.assertEqual([m.entrycget(i, 'label') for i in range(4)],
+                         ['A', 'B', 'D', 'E'])
+        self.assertEqual(
+            m.tk.splitlist(m.tk.call('info', 'commands', funcid)), ())
+
+        m.delete(1, 2)  # Delete a range ('B' and 'D').
+        self.assertEqual([m.entrycget(i, 'label') for i in range(2)],
+                         ['A', 'E'])
+        self.assertRaises(TypeError, m.delete)
+
+    def test_index(self):
+        m = self.create(tearoff=False)
+        self.assertIsNone(m.index('end'))
+        m.add_command(label='First')
+        m.add_command(label='Second')
+        self.assertEqual(m.index('end'), 1)
+        self.assertEqual(m.index('last'), 1)
+        self.assertEqual(m.index('Second'), 1)
+        self.assertEqual(m.index(0), 0)
+        # 'active' and 'none' map to None when no item is active.
+        self.assertIsNone(m.index('active'))
+        self.assertIsNone(m.index('none'))
+        self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
+                               m.index, 'spam')
+
+    def test_invoke(self):
+        m = self.create(tearoff=False)
+        commands = []
+        m.add_command(label='Command',
+                      command=lambda: commands.append('invoked'))
+        var = tkinter.IntVar(self.root)
+        m.add_checkbutton(label='Check', variable=var,
+                          onvalue=1, offvalue=0)
+        rvar = tkinter.StringVar(self.root)
+        m.add_radiobutton(label='Radio', variable=rvar, value='on')
+
+        m.invoke(0)
+        self.assertEqual(commands, ['invoked'])
+        m.invoke(1)
+        self.assertEqual(var.get(), 1)
+        m.invoke(1)
+        self.assertEqual(var.get(), 0)
+        m.invoke(2)
+        self.assertEqual(rvar.get(), 'on')
+        self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
+                               m.invoke, 'spam')
+
+    def test_xposition_yposition(self):
+        m = self.create(tearoff=False)
+        m.add_command(label='First')
+        m.add_command(label='Second')
+        m.update_idletasks()
+        self.assertIsInstance(m.xposition(0), int)
+        y0 = m.yposition(0)
+        y1 = m.yposition(1)
+        self.assertIsInstance(y0, int)
+        self.assertLess(y0, y1)
+        # An out-of-range index gives the position past the last item.
+        self.assertEqual(m.xposition('end'), m.xposition(1))
+        self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
+                               m.xposition, 'spam')
+        self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
+                               m.yposition, 'spam')
+
+    def test_post_unpost(self):
+        m = self.create(tearoff=False)
+        if m._windowingsystem != 'x11':
+            # Posting a menu is modal on Windows and uses a native, unmapped
+            # menu on Aqua, so it cannot be tested synchronously there.
+            self.skipTest('menu posting is not testable on this platform')
+        m.add_command(label='First')
+        m.add_command(label='Second')
+        self.assertFalse(m.winfo_ismapped())
+
+        m.post(0, 0)
+        m.update()
+        self.assertTrue(m.winfo_ismapped())
+        m.unpost()
+        m.update()
+        self.assertFalse(m.winfo_ismapped())
+
+        m.tk_popup(0, 0)
+        m.update()
+        self.assertTrue(m.winfo_ismapped())
+        m.unpost()
+        m.update()
+        self.assertFalse(m.winfo_ismapped())
+
+    def check_entry_option(self, m, index, option, value, expected=None):
+        if expected is None:
+            expected = value
+        m.entryconfigure(index, **{option: value})
+        self.assertEqual(str(m.entrycget(index, option)), str(expected))
+        self.assertEqual(str(m.entryconfigure(index, option)[4]), str(expected))
+
+    def test_entry_options(self):
+        m = self.create(tearoff=False)
+        m.add_command(label='Command')
+        self.check_entry_option(m, 0, 'accelerator', 'Ctrl+O')
+        self.check_entry_option(m, 0, 'underline', 2)
+        self.check_entry_option(m, 0, 'state', 'disabled')
+        self.check_entry_option(m, 0, 'background', 'red')
+        self.check_entry_option(m, 0, 'foreground', 'blue')
+        self.check_entry_option(m, 0, 'columnbreak', 1)
+        self.check_entry_option(m, 0, 'hidemargin', 1)
+
+        m.add_checkbutton(label='Checkbutton')
+        self.check_entry_option(m, 1, 'onvalue', 'Y')
+        self.check_entry_option(m, 1, 'offvalue', 'N')
+        self.check_entry_option(m, 1, 'indicatoron', 0)
+
+        m.add_radiobutton(label='Radiobutton')
+        self.check_entry_option(m, 2, 'value', 'V')
+        self.check_entry_option(m, 2, 'selectcolor', 'green')
+
+    def test_entry_options_invalid(self):
+        m = self.create(tearoff=False)
+        m.add_command(label='Command')
+        self.assertRaisesRegex(TclError, 'unknown option "-spam"',
+                               m.entrycget, 0, 'spam')
+        self.assertRaisesRegex(TclError, 'unknown option "-spam"',
+                               m.entryconfigure, 0, spam='x')
+        self.assertRaisesRegex(TclError, 'bad state "spam"',
+                               m.entryconfigure, 0, state='spam')
+        # Tk < 9 reports "expected integer but got ...", while Tk 9, where
+        # underline accepts an index, reports "bad index ...".
+        self.assertRaisesRegex(TclError,
+                               r'(expected integer but got|bad index) "spam"',
+                               m.entryconfigure, 0, underline='spam')
+        self.assertRaisesRegex(TclError, 'unknown color name "spam"',
+                               m.entryconfigure, 0, background='spam')
+        self.assertRaisesRegex(TclError, 'expected boolean value but got "spam"',
+                               m.entryconfigure, 0, columnbreak='spam')
+        # onvalue applies only to checkbutton and radiobutton entries.
+        self.assertRaisesRegex(TclError, 'unknown option "-onvalue"',
+                               m.entrycget, 0, 'onvalue')
+
 
 @add_configure_tests(PixelSizeTests, StandardOptionsTests)
 class MessageTest(AbstractWidgetTest, unittest.TestCase):