1 # Common tests for test_tkinter/test_widgets.py and test_ttk/test_widgets.py 2 3 import unittest 4 import sys 5 import tkinter 6 from tkinter.ttk import Scale 7 from tkinter.test.support import (AbstractTkTest, tcl_version, requires_tcl, 8 get_tk_patchlevel, pixels_conv, tcl_obj_eq) 9 import test.support 10 11 12 noconv = False 13 if get_tk_patchlevel() < (8, 5, 11): 14 noconv = str 15 16 pixels_round = round 17 if get_tk_patchlevel()[:3] == (8, 5, 11): 18 # Issue #19085: Workaround a bug in Tk 19 # http://core.tcl.tk/tk/info/3497848 20 pixels_round = int 21 22 23 _sentinel = object() 24 25 class AbstractWidgetTest(AbstractTkTest): 26 _conv_pixels = staticmethod(pixels_round) 27 _conv_pad_pixels = None 28 _stringify = False 29 30 @property 31 def scaling(self): 32 try: 33 return self._scaling 34 except AttributeError: 35 self._scaling = float(self.root.call('tk', 'scaling')) 36 return self._scaling 37 38 def _str(self, value): 39 if not self._stringify and self.wantobjects and tcl_version >= (8, 6): 40 return value 41 if isinstance(value, tuple): 42 return ' '.join(map(self._str, value)) 43 return str(value) 44 45 def assertEqual2(self, actual, expected, msg=None, eq=object.__eq__): 46 if eq(actual, expected): 47 return 48 self.assertEqual(actual, expected, msg) 49 50 def checkParam(self, widget, name, value, *, expected=_sentinel, 51 conv=False, eq=None): 52 widget[name] = value 53 if expected is _sentinel: 54 expected = value 55 if conv: 56 expected = conv(expected) 57 if self._stringify or not self.wantobjects: 58 if isinstance(expected, tuple): 59 expected = tkinter._join(expected) 60 else: 61 expected = str(expected) 62 if eq is None: 63 eq = tcl_obj_eq 64 self.assertEqual2(widget[name], expected, eq=eq) 65 self.assertEqual2(widget.cget(name), expected, eq=eq) 66 # XXX 67 if not isinstance(widget, Scale): 68 t = widget.configure(name) 69 self.assertEqual(len(t), 5) 70 self.assertEqual2(t[4], expected, eq=eq) 71 72 def checkInvalidParam(self, widget, name, value, errmsg=None, *, 73 keep_orig=True): 74 orig = widget[name] 75 if errmsg is not None: 76 errmsg = errmsg.format(value) 77 with self.assertRaises(tkinter.TclError) as cm: 78 widget[name] = value 79 if errmsg is not None: 80 self.assertEqual(str(cm.exception), errmsg) 81 if keep_orig: 82 self.assertEqual(widget[name], orig) 83 else: 84 widget[name] = orig 85 with self.assertRaises(tkinter.TclError) as cm: 86 widget.configure({name: value}) 87 if errmsg is not None: 88 self.assertEqual(str(cm.exception), errmsg) 89 if keep_orig: 90 self.assertEqual(widget[name], orig) 91 else: 92 widget[name] = orig 93 94 def checkParams(self, widget, name, *values, **kwargs): 95 for value in values: 96 self.checkParam(widget, name, value, **kwargs) 97 98 def checkIntegerParam(self, widget, name, *values, **kwargs): 99 self.checkParams(widget, name, *values, **kwargs) 100 self.checkInvalidParam(widget, name, '', 101 errmsg='expected integer but got ""') 102 self.checkInvalidParam(widget, name, '10p', 103 errmsg='expected integer but got "10p"') 104 self.checkInvalidParam(widget, name, 3.2, 105 errmsg='expected integer but got "3.2"') 106 107 def checkFloatParam(self, widget, name, *values, conv=float, **kwargs): 108 for value in values: 109 self.checkParam(widget, name, value, conv=conv, **kwargs) 110 self.checkInvalidParam(widget, name, '', 111 errmsg='expected floating-point number but got ""') 112 self.checkInvalidParam(widget, name, 'spam', 113 errmsg='expected floating-point number but got "spam"') 114 115 def checkBooleanParam(self, widget, name): 116 for value in (False, 0, 'false', 'no', 'off'): 117 self.checkParam(widget, name, value, expected=0) 118 for value in (True, 1, 'true', 'yes', 'on'): 119 self.checkParam(widget, name, value, expected=1) 120 self.checkInvalidParam(widget, name, '', 121 errmsg='expected boolean value but got ""') 122 self.checkInvalidParam(widget, name, 'spam', 123 errmsg='expected boolean value but got "spam"') 124 125 def checkColorParam(self, widget, name, *, allow_empty=None, **kwargs): 126 self.checkParams(widget, name, 127 '#ff0000', '#00ff00', '#0000ff', '#123456', 128 'red', 'green', 'blue', 'white', 'black', 'grey', 129 **kwargs) 130 self.checkInvalidParam(widget, name, 'spam', 131 errmsg='unknown color name "spam"') 132 133 def checkCursorParam(self, widget, name, **kwargs): 134 self.checkParams(widget, name, 'arrow', 'watch', 'cross', '',**kwargs) 135 if tcl_version >= (8, 5): 136 self.checkParam(widget, name, 'none') 137 self.checkInvalidParam(widget, name, 'spam', 138 errmsg='bad cursor spec "spam"') 139 140 def checkCommandParam(self, widget, name): 141 def command(*args): 142 pass 143 widget[name] = command 144 self.assertTrue(widget[name]) 145 self.checkParams(widget, name, '') 146 147 def checkEnumParam(self, widget, name, *values, errmsg=None, **kwargs): 148 self.checkParams(widget, name, *values, **kwargs) 149 if errmsg is None: 150 errmsg2 = ' %s "{}": must be %s%s or %s' % ( 151 name, 152 ', '.join(values[:-1]), 153 ',' if len(values) > 2 else '', 154 values[-1]) 155 self.checkInvalidParam(widget, name, '', 156 errmsg='ambiguous' + errmsg2) 157 errmsg = 'bad' + errmsg2 158 self.checkInvalidParam(widget, name, 'spam', errmsg=errmsg) 159 160 def checkPixelsParam(self, widget, name, *values, 161 conv=None, keep_orig=True, **kwargs): 162 if conv is None: 163 conv = self._conv_pixels 164 for value in values: 165 expected = _sentinel 166 conv1 = conv 167 if isinstance(value, str): 168 if conv1 and conv1 is not str: 169 expected = pixels_conv(value) * self.scaling 170 conv1 = round 171 self.checkParam(widget, name, value, expected=expected, 172 conv=conv1, **kwargs) 173 self.checkInvalidParam(widget, name, '6x', 174 errmsg='bad screen distance "6x"', keep_orig=keep_orig) 175 self.checkInvalidParam(widget, name, 'spam', 176 errmsg='bad screen distance "spam"', keep_orig=keep_orig) 177 178 def checkReliefParam(self, widget, name): 179 self.checkParams(widget, name, 180 'flat', 'groove', 'raised', 'ridge', 'solid', 'sunken') 181 errmsg='bad relief "spam": must be '\ 182 'flat, groove, raised, ridge, solid, or sunken' 183 if tcl_version < (8, 6): 184 errmsg = None 185 self.checkInvalidParam(widget, name, 'spam', 186 errmsg=errmsg) 187 188 def checkImageParam(self, widget, name): 189 image = tkinter.PhotoImage(master=self.root, name='image1') 190 self.checkParam(widget, name, image, conv=str) 191 self.checkInvalidParam(widget, name, 'spam', 192 errmsg='image "spam" doesn\'t exist') 193 widget[name] = '' 194 195 def checkVariableParam(self, widget, name, var): 196 self.checkParam(widget, name, var, conv=str) 197 198 def assertIsBoundingBox(self, bbox): 199 self.assertIsNotNone(bbox) 200 self.assertIsInstance(bbox, tuple) 201 if len(bbox) != 4: 202 self.fail('Invalid bounding box: %r' % (bbox,)) 203 for item in bbox: 204 if not isinstance(item, int): 205 self.fail('Invalid bounding box: %r' % (bbox,)) 206 break 207 208 209 def test_keys(self): 210 widget = self.create() 211 keys = widget.keys() 212 # XXX 213 if not isinstance(widget, Scale): 214 self.assertEqual(sorted(keys), sorted(widget.configure())) 215 for k in keys: 216 widget[k] 217 # Test if OPTIONS contains all keys 218 if test.support.verbose: 219 aliases = { 220 'bd': 'borderwidth', 221 'bg': 'background', 222 'fg': 'foreground', 223 'invcmd': 'invalidcommand', 224 'vcmd': 'validatecommand', 225 } 226 keys = set(keys) 227 expected = set(self.OPTIONS) 228 for k in sorted(keys - expected): 229 if not (k in aliases and 230 aliases[k] in keys and 231 aliases[k] in expected): 232 print('%s.OPTIONS doesn\'t contain "%s"' % 233 (self.__class__.__name__, k)) 234 235 236 class StandardOptionsTests: 237 STANDARD_OPTIONS = ( 238 'activebackground', 'activeborderwidth', 'activeforeground', 'anchor', 239 'background', 'bitmap', 'borderwidth', 'compound', 'cursor', 240 'disabledforeground', 'exportselection', 'font', 'foreground', 241 'highlightbackground', 'highlightcolor', 'highlightthickness', 242 'image', 'insertbackground', 'insertborderwidth', 243 'insertofftime', 'insertontime', 'insertwidth', 244 'jump', 'justify', 'orient', 'padx', 'pady', 'relief', 245 'repeatdelay', 'repeatinterval', 246 'selectbackground', 'selectborderwidth', 'selectforeground', 247 'setgrid', 'takefocus', 'text', 'textvariable', 'troughcolor', 248 'underline', 'wraplength', 'xscrollcommand', 'yscrollcommand', 249 ) 250 251 def test_activebackground(self): 252 widget = self.create() 253 self.checkColorParam(widget, 'activebackground') 254 255 def test_activeborderwidth(self): 256 widget = self.create() 257 self.checkPixelsParam(widget, 'activeborderwidth', 258 0, 1.3, 2.9, 6, -2, '10p') 259 260 def test_activeforeground(self): 261 widget = self.create() 262 self.checkColorParam(widget, 'activeforeground') 263 264 def test_anchor(self): 265 widget = self.create() 266 self.checkEnumParam(widget, 'anchor', 267 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw', 'center') 268 269 def test_background(self): 270 widget = self.create() 271 self.checkColorParam(widget, 'background') 272 if 'bg' in self.OPTIONS: 273 self.checkColorParam(widget, 'bg') 274 275 def test_bitmap(self): 276 widget = self.create() 277 self.checkParam(widget, 'bitmap', 'questhead') 278 self.checkParam(widget, 'bitmap', 'gray50') 279 filename = test.support.findfile('python.xbm', subdir='imghdrdata') 280 self.checkParam(widget, 'bitmap', '@' + filename) 281 # Cocoa Tk widgets don't detect invalid -bitmap values 282 # See https://core.tcl.tk/tk/info/31cd33dbf0 283 if not ('aqua' in self.root.tk.call('tk', 'windowingsystem') and 284 'AppKit' in self.root.winfo_server()): 285 self.checkInvalidParam(widget, 'bitmap', 'spam', 286 errmsg='bitmap "spam" not defined') 287 288 def test_borderwidth(self): 289 widget = self.create() 290 self.checkPixelsParam(widget, 'borderwidth', 291 0, 1.3, 2.6, 6, -2, '10p') 292 if 'bd' in self.OPTIONS: 293 self.checkPixelsParam(widget, 'bd', 0, 1.3, 2.6, 6, -2, '10p') 294 295 def test_compound(self): 296 widget = self.create() 297 self.checkEnumParam(widget, 'compound', 298 'bottom', 'center', 'left', 'none', 'right', 'top') 299 300 def test_cursor(self): 301 widget = self.create() 302 self.checkCursorParam(widget, 'cursor') 303 304 def test_disabledforeground(self): 305 widget = self.create() 306 self.checkColorParam(widget, 'disabledforeground') 307 308 def test_exportselection(self): 309 widget = self.create() 310 self.checkBooleanParam(widget, 'exportselection') 311 312 def test_font(self): 313 widget = self.create() 314 self.checkParam(widget, 'font', 315 '-Adobe-Helvetica-Medium-R-Normal--*-120-*-*-*-*-*-*') 316 self.checkInvalidParam(widget, 'font', '', 317 errmsg='font "" doesn\'t exist') 318 319 def test_foreground(self): 320 widget = self.create() 321 self.checkColorParam(widget, 'foreground') 322 if 'fg' in self.OPTIONS: 323 self.checkColorParam(widget, 'fg') 324 325 def test_highlightbackground(self): 326 widget = self.create() 327 self.checkColorParam(widget, 'highlightbackground') 328 329 def test_highlightcolor(self): 330 widget = self.create() 331 self.checkColorParam(widget, 'highlightcolor') 332 333 def test_highlightthickness(self): 334 widget = self.create() 335 self.checkPixelsParam(widget, 'highlightthickness', 336 0, 1.3, 2.6, 6, '10p') 337 self.checkParam(widget, 'highlightthickness', -2, expected=0, 338 conv=self._conv_pixels) 339 340 @unittest.skipIf(sys.platform == 'darwin', 341 'crashes with Cocoa Tk (issue19733)') 342 def test_image(self): 343 widget = self.create() 344 self.checkImageParam(widget, 'image') 345 346 def test_insertbackground(self): 347 widget = self.create() 348 self.checkColorParam(widget, 'insertbackground') 349 350 def test_insertborderwidth(self): 351 widget = self.create() 352 self.checkPixelsParam(widget, 'insertborderwidth', 353 0, 1.3, 2.6, 6, -2, '10p') 354 355 def test_insertofftime(self): 356 widget = self.create() 357 self.checkIntegerParam(widget, 'insertofftime', 100) 358 359 def test_insertontime(self): 360 widget = self.create() 361 self.checkIntegerParam(widget, 'insertontime', 100) 362 363 def test_insertwidth(self): 364 widget = self.create() 365 self.checkPixelsParam(widget, 'insertwidth', 1.3, 2.6, -2, '10p') 366 367 def test_jump(self): 368 widget = self.create() 369 self.checkBooleanParam(widget, 'jump') 370 371 def test_justify(self): 372 widget = self.create() 373 self.checkEnumParam(widget, 'justify', 'left', 'right', 'center', 374 errmsg='bad justification "{}": must be ' 375 'left, right, or center') 376 self.checkInvalidParam(widget, 'justify', '', 377 errmsg='ambiguous justification "": must be ' 378 'left, right, or center') 379 380 def test_orient(self): 381 widget = self.create() 382 self.assertEqual(str(widget['orient']), self.default_orient) 383 self.checkEnumParam(widget, 'orient', 'horizontal', 'vertical') 384 385 def test_padx(self): 386 widget = self.create() 387 self.checkPixelsParam(widget, 'padx', 3, 4.4, 5.6, -2, '12m', 388 conv=self._conv_pad_pixels) 389 390 def test_pady(self): 391 widget = self.create() 392 self.checkPixelsParam(widget, 'pady', 3, 4.4, 5.6, -2, '12m', 393 conv=self._conv_pad_pixels) 394 395 def test_relief(self): 396 widget = self.create() 397 self.checkReliefParam(widget, 'relief') 398 399 def test_repeatdelay(self): 400 widget = self.create() 401 self.checkIntegerParam(widget, 'repeatdelay', -500, 500) 402 403 def test_repeatinterval(self): 404 widget = self.create() 405 self.checkIntegerParam(widget, 'repeatinterval', -500, 500) 406 407 def test_selectbackground(self): 408 widget = self.create() 409 self.checkColorParam(widget, 'selectbackground') 410 411 def test_selectborderwidth(self): 412 widget = self.create() 413 self.checkPixelsParam(widget, 'selectborderwidth', 1.3, 2.6, -2, '10p') 414 415 def test_selectforeground(self): 416 widget = self.create() 417 self.checkColorParam(widget, 'selectforeground') 418 419 def test_setgrid(self): 420 widget = self.create() 421 self.checkBooleanParam(widget, 'setgrid') 422 423 def test_state(self): 424 widget = self.create() 425 self.checkEnumParam(widget, 'state', 'active', 'disabled', 'normal') 426 427 def test_takefocus(self): 428 widget = self.create() 429 self.checkParams(widget, 'takefocus', '0', '1', '') 430 431 def test_text(self): 432 widget = self.create() 433 self.checkParams(widget, 'text', '', 'any string') 434 435 def test_textvariable(self): 436 widget = self.create() 437 var = tkinter.StringVar(self.root) 438 self.checkVariableParam(widget, 'textvariable', var) 439 440 def test_troughcolor(self): 441 widget = self.create() 442 self.checkColorParam(widget, 'troughcolor') 443 444 def test_underline(self): 445 widget = self.create() 446 self.checkIntegerParam(widget, 'underline', 0, 1, 10) 447 448 def test_wraplength(self): 449 widget = self.create() 450 self.checkPixelsParam(widget, 'wraplength', 100) 451 452 def test_xscrollcommand(self): 453 widget = self.create() 454 self.checkCommandParam(widget, 'xscrollcommand') 455 456 def test_yscrollcommand(self): 457 widget = self.create() 458 self.checkCommandParam(widget, 'yscrollcommand') 459 460 # non-standard but common options 461 462 def test_command(self): 463 widget = self.create() 464 self.checkCommandParam(widget, 'command') 465 466 def test_indicatoron(self): 467 widget = self.create() 468 self.checkBooleanParam(widget, 'indicatoron') 469 470 def test_offrelief(self): 471 widget = self.create() 472 self.checkReliefParam(widget, 'offrelief') 473 474 def test_overrelief(self): 475 widget = self.create() 476 self.checkReliefParam(widget, 'overrelief') 477 478 def test_selectcolor(self): 479 widget = self.create() 480 self.checkColorParam(widget, 'selectcolor') 481 482 def test_selectimage(self): 483 widget = self.create() 484 self.checkImageParam(widget, 'selectimage') 485 486 @requires_tcl(8, 5) 487 def test_tristateimage(self): 488 widget = self.create() 489 self.checkImageParam(widget, 'tristateimage') 490 491 @requires_tcl(8, 5) 492 def test_tristatevalue(self): 493 widget = self.create() 494 self.checkParam(widget, 'tristatevalue', 'unknowable') 495 496 def test_variable(self): 497 widget = self.create() 498 var = tkinter.DoubleVar(self.root) 499 self.checkVariableParam(widget, 'variable', var) 500 501 502 class IntegerSizeTests: 503 def test_height(self): 504 widget = self.create() 505 self.checkIntegerParam(widget, 'height', 100, -100, 0) 506 507 def test_width(self): 508 widget = self.create() 509 self.checkIntegerParam(widget, 'width', 402, -402, 0) 510 511 512 class PixelSizeTests: 513 def test_height(self): 514 widget = self.create() 515 self.checkPixelsParam(widget, 'height', 100, 101.2, 102.6, -100, 0, '3c') 516 517 def test_width(self): 518 widget = self.create() 519 self.checkPixelsParam(widget, 'width', 402, 403.4, 404.6, -402, 0, '5i') 520 521 522 def add_standard_options(*source_classes): 523 # This decorator adds test_xxx methods from source classes for every xxx 524 # option in the OPTIONS class attribute if they are not defined explicitly. 525 def decorator(cls): 526 for option in cls.OPTIONS: 527 methodname = 'test_' + option 528 if not hasattr(cls, methodname): 529 for source_class in source_classes: 530 if hasattr(source_class, methodname): 531 setattr(cls, methodname, 532 getattr(source_class, methodname)) 533 break 534 else: 535 def test(self, option=option): 536 widget = self.create() 537 widget[option] 538 raise AssertionError('Option "%s" is not tested in %s' % 539 (option, cls.__name__)) 540 test.__name__ = methodname 541 setattr(cls, methodname, test) 542 return cls 543 return decorator 544 545 def setUpModule(): 546 if test.support.verbose: 547 tcl = tkinter.Tcl() 548 print('patchlevel =', tcl.call('info', 'patchlevel')) 549