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