From 471421967f204cd2dbb4a548df190cee0a1b8f25 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 19 Jun 2026 03:04:14 +0300 Subject: [PATCH] gh-151678: Add tests for tkinter.Menu 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) --- Lib/test/test_tkinter/test_widgets.py | 186 ++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) diff --git a/Lib/test/test_tkinter/test_widgets.py b/Lib/test/test_tkinter/test_widgets.py index 1c400e970eb02d..fd3c70c97c3d5b 100644 --- a/Lib/test/test_tkinter/test_widgets.py +++ b/Lib/test/test_tkinter/test_widgets.py @@ -1542,6 +1542,192 @@ def test_entryconfigure_variable(self): 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):