Skip to content

Commit c0c9920

Browse files
miss-islingtonserhiy-storchakaclaude
authored
[3.13] gh-151678: Add tests for tkinter.Menu (GH-151685) (GH-151711)
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. (cherry picked from commit ef5c32a) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 423f620 commit c0c9920

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

Lib/test/test_tkinter/test_widgets.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1458,6 +1458,192 @@ def test_entryconfigure_variable(self):
14581458
m1.entryconfigure(1, variable=v2)
14591459
self.assertEqual(str(m1.entrycget(1, 'variable')), str(v2))
14601460

1461+
def test_add(self):
1462+
m = self.create(tearoff=False)
1463+
m.add_command(label='Command')
1464+
m.add_checkbutton(label='Checkbutton')
1465+
m.add_radiobutton(label='Radiobutton')
1466+
m.add_separator()
1467+
m.add_cascade(label='Cascade', menu=tkinter.Menu(m, tearoff=False))
1468+
self.assertEqual(m.index('end'), 4)
1469+
self.assertEqual([m.type(i) for i in range(5)],
1470+
['command', 'checkbutton', 'radiobutton',
1471+
'separator', 'cascade'])
1472+
self.assertEqual(m.entrycget(0, 'label'), 'Command')
1473+
self.assertRaisesRegex(TclError, 'bad menu entry type "spam"',
1474+
m.add, 'spam')
1475+
1476+
def test_insert(self):
1477+
m = self.create(tearoff=False)
1478+
m.add_command(label='A')
1479+
m.add_command(label='C')
1480+
m.insert_command(1, label='B')
1481+
m.insert_separator(0)
1482+
m.insert_checkbutton('end', label='D')
1483+
m.insert_radiobutton(0, label='top')
1484+
m.insert_cascade(2, label='sub',
1485+
menu=tkinter.Menu(m, tearoff=False))
1486+
self.assertEqual(
1487+
[m.type(i) for i in range(m.index('end') + 1)],
1488+
['radiobutton', 'separator', 'cascade', 'command',
1489+
'command', 'command', 'checkbutton'])
1490+
self.assertEqual(
1491+
[m.entrycget(i, 'label') for i in (3, 4, 5)],
1492+
['A', 'B', 'C'])
1493+
self.assertRaisesRegex(TclError, 'bad menu entry type "spam"',
1494+
m.insert, 0, 'spam')
1495+
self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
1496+
m.insert_command, 'spam', label='z')
1497+
1498+
def test_delete(self):
1499+
m = self.create(tearoff=False)
1500+
commands = []
1501+
for label in 'ABCDE':
1502+
m.add_command(label=label,
1503+
command=lambda label=label: commands.append(label))
1504+
# The Tcl command for a deleted item is cleaned up.
1505+
funcid = str(m.entrycget(2, 'command'))
1506+
self.assertEqual(
1507+
m.tk.splitlist(m.tk.call('info', 'commands', funcid)), (funcid,))
1508+
1509+
m.delete(2) # Delete a single item ('C').
1510+
self.assertEqual([m.entrycget(i, 'label') for i in range(4)],
1511+
['A', 'B', 'D', 'E'])
1512+
self.assertEqual(
1513+
m.tk.splitlist(m.tk.call('info', 'commands', funcid)), ())
1514+
1515+
m.delete(1, 2) # Delete a range ('B' and 'D').
1516+
self.assertEqual([m.entrycget(i, 'label') for i in range(2)],
1517+
['A', 'E'])
1518+
self.assertRaises(TypeError, m.delete)
1519+
1520+
def test_index(self):
1521+
m = self.create(tearoff=False)
1522+
self.assertIsNone(m.index('end'))
1523+
m.add_command(label='First')
1524+
m.add_command(label='Second')
1525+
self.assertEqual(m.index('end'), 1)
1526+
self.assertEqual(m.index('last'), 1)
1527+
self.assertEqual(m.index('Second'), 1)
1528+
self.assertEqual(m.index(0), 0)
1529+
# 'active' and 'none' map to None when no item is active.
1530+
self.assertIsNone(m.index('active'))
1531+
self.assertIsNone(m.index('none'))
1532+
self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
1533+
m.index, 'spam')
1534+
1535+
def test_invoke(self):
1536+
m = self.create(tearoff=False)
1537+
commands = []
1538+
m.add_command(label='Command',
1539+
command=lambda: commands.append('invoked'))
1540+
var = tkinter.IntVar(self.root)
1541+
m.add_checkbutton(label='Check', variable=var,
1542+
onvalue=1, offvalue=0)
1543+
rvar = tkinter.StringVar(self.root)
1544+
m.add_radiobutton(label='Radio', variable=rvar, value='on')
1545+
1546+
m.invoke(0)
1547+
self.assertEqual(commands, ['invoked'])
1548+
m.invoke(1)
1549+
self.assertEqual(var.get(), 1)
1550+
m.invoke(1)
1551+
self.assertEqual(var.get(), 0)
1552+
m.invoke(2)
1553+
self.assertEqual(rvar.get(), 'on')
1554+
self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
1555+
m.invoke, 'spam')
1556+
1557+
def test_xposition_yposition(self):
1558+
m = self.create(tearoff=False)
1559+
m.add_command(label='First')
1560+
m.add_command(label='Second')
1561+
m.update_idletasks()
1562+
self.assertIsInstance(m.xposition(0), int)
1563+
y0 = m.yposition(0)
1564+
y1 = m.yposition(1)
1565+
self.assertIsInstance(y0, int)
1566+
self.assertLess(y0, y1)
1567+
# An out-of-range index gives the position past the last item.
1568+
self.assertEqual(m.xposition('end'), m.xposition(1))
1569+
self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
1570+
m.xposition, 'spam')
1571+
self.assertRaisesRegex(TclError, 'bad menu entry index "spam"',
1572+
m.yposition, 'spam')
1573+
1574+
def test_post_unpost(self):
1575+
m = self.create(tearoff=False)
1576+
if m._windowingsystem != 'x11':
1577+
# Posting a menu is modal on Windows and uses a native, unmapped
1578+
# menu on Aqua, so it cannot be tested synchronously there.
1579+
self.skipTest('menu posting is not testable on this platform')
1580+
m.add_command(label='First')
1581+
m.add_command(label='Second')
1582+
self.assertFalse(m.winfo_ismapped())
1583+
1584+
m.post(0, 0)
1585+
m.update()
1586+
self.assertTrue(m.winfo_ismapped())
1587+
m.unpost()
1588+
m.update()
1589+
self.assertFalse(m.winfo_ismapped())
1590+
1591+
m.tk_popup(0, 0)
1592+
m.update()
1593+
self.assertTrue(m.winfo_ismapped())
1594+
m.unpost()
1595+
m.update()
1596+
self.assertFalse(m.winfo_ismapped())
1597+
1598+
def check_entry_option(self, m, index, option, value, expected=None):
1599+
if expected is None:
1600+
expected = value
1601+
m.entryconfigure(index, **{option: value})
1602+
self.assertEqual(str(m.entrycget(index, option)), str(expected))
1603+
self.assertEqual(str(m.entryconfigure(index, option)[4]), str(expected))
1604+
1605+
def test_entry_options(self):
1606+
m = self.create(tearoff=False)
1607+
m.add_command(label='Command')
1608+
self.check_entry_option(m, 0, 'accelerator', 'Ctrl+O')
1609+
self.check_entry_option(m, 0, 'underline', 2)
1610+
self.check_entry_option(m, 0, 'state', 'disabled')
1611+
self.check_entry_option(m, 0, 'background', 'red')
1612+
self.check_entry_option(m, 0, 'foreground', 'blue')
1613+
self.check_entry_option(m, 0, 'columnbreak', 1)
1614+
self.check_entry_option(m, 0, 'hidemargin', 1)
1615+
1616+
m.add_checkbutton(label='Checkbutton')
1617+
self.check_entry_option(m, 1, 'onvalue', 'Y')
1618+
self.check_entry_option(m, 1, 'offvalue', 'N')
1619+
self.check_entry_option(m, 1, 'indicatoron', 0)
1620+
1621+
m.add_radiobutton(label='Radiobutton')
1622+
self.check_entry_option(m, 2, 'value', 'V')
1623+
self.check_entry_option(m, 2, 'selectcolor', 'green')
1624+
1625+
def test_entry_options_invalid(self):
1626+
m = self.create(tearoff=False)
1627+
m.add_command(label='Command')
1628+
self.assertRaisesRegex(TclError, 'unknown option "-spam"',
1629+
m.entrycget, 0, 'spam')
1630+
self.assertRaisesRegex(TclError, 'unknown option "-spam"',
1631+
m.entryconfigure, 0, spam='x')
1632+
self.assertRaisesRegex(TclError, 'bad state "spam"',
1633+
m.entryconfigure, 0, state='spam')
1634+
# Tk < 9 reports "expected integer but got ...", while Tk 9, where
1635+
# underline accepts an index, reports "bad index ...".
1636+
self.assertRaisesRegex(TclError,
1637+
r'(expected integer but got|bad index) "spam"',
1638+
m.entryconfigure, 0, underline='spam')
1639+
self.assertRaisesRegex(TclError, 'unknown color name "spam"',
1640+
m.entryconfigure, 0, background='spam')
1641+
self.assertRaisesRegex(TclError, 'expected boolean value but got "spam"',
1642+
m.entryconfigure, 0, columnbreak='spam')
1643+
# onvalue applies only to checkbutton and radiobutton entries.
1644+
self.assertRaisesRegex(TclError, 'unknown option "-onvalue"',
1645+
m.entrycget, 0, 'onvalue')
1646+
14611647

14621648
@add_standard_options(PixelSizeTests, StandardOptionsTests)
14631649
class MessageTest(AbstractWidgetTest, unittest.TestCase):

0 commit comments

Comments
 (0)